Feed icons! '/favicon.ico' download, storage and display.

This commit is contained in:
relikd
2018-12-12 02:16:31 +01:00
parent 13a4191b93
commit 2bd7078cbd
13 changed files with 144 additions and 67 deletions

View File

@@ -60,8 +60,8 @@ ToDo
- [x] Auto fix 301 Redirect or ask user
- [x] Make `feed://` URLs clickable
- [ ] Feeds with authentication
- [ ] Show proper feed icon
- [ ] Download and store icon file
- [x] Show proper feed icon
- [x] Download and store icon file
- [ ] Other

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
540CD14921C094A2004AB594 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 540CD14821C094A2004AB594 /* README.md */; };
540F704521B6C16C0022E69D /* FeedMeta+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 540F704421B6C16C0022E69D /* FeedMeta+Ext.m */; };
54195883218A061100581B79 /* Feed+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195882218A061100581B79 /* Feed+Ext.m */; };
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; };
@@ -73,6 +74,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
540CD14821C094A2004AB594 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
540F704321B6C16C0022E69D /* FeedMeta+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedMeta+Ext.h"; sourceTree = "<group>"; };
540F704421B6C16C0022E69D /* FeedMeta+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedMeta+Ext.m"; sourceTree = "<group>"; };
54195881218A061100581B79 /* Feed+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Feed+Ext.h"; sourceTree = "<group>"; };
@@ -208,6 +210,7 @@
54ACC27321061B3B0020715F = {
isa = PBXGroup;
children = (
540CD14821C094A2004AB594 /* README.md */,
54ACC27E21061B3B0020715F /* baRSS */,
54CC042D2162532800A48795 /* baRSS-Helper */,
54ACC27D21061B3B0020715F /* Products */,
@@ -364,6 +367,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
540CD14921C094A2004AB594 /* README.md in Resources */,
546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */,
54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */,
54ACC28621061B3C0020715F /* Assets.xcassets in Resources */,

View File

@@ -33,4 +33,5 @@
- (NSArray<FeedArticle*>*)sortedArticles;
- (int)markAllItemsRead;
- (int)markAllItemsUnread;
- (NSImage*)iconImage16;
@end

View File

@@ -21,11 +21,14 @@
// SOFTWARE.
#import "Feed+Ext.h"
#import "Constants.h"
#import "DrawImage.h"
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#import "FeedIcon+CoreDataClass.h"
#import "FeedArticle+CoreDataClass.h"
#import "Constants.h"
#import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@implementation Feed (Ext)
@@ -214,4 +217,19 @@
return newCount - oldCount;
}
/**
@return Return @c 16x16px image. Either from core data storage or generated default RSS icon.
*/
- (NSImage*)iconImage16 {
NSData *imgData = self.icon.icon;
if (imgData) {
return [[NSImage alloc] initWithData:imgData];
} else {
static NSImage *defaultRSSIcon;
if (!defaultRSSIcon)
defaultRSSIcon = [RSSIcon iconWithSize:16];
return defaultRSSIcon;
}
}
@end

View File

@@ -33,6 +33,7 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)context;
- (void)setName:(NSString*)name andRefreshString:(NSString*)refreshStr;
- (NSImage*)groupIconImage16;
// Handle children and parents
- (NSString*)indexPathString;
- (NSMutableArray<FeedGroup*>*)allParents;

View File

@@ -24,6 +24,8 @@
#import "FeedMeta+Ext.h"
#import "Feed+Ext.h"
#import <Cocoa/Cocoa.h>
@implementation FeedGroup (Ext)
/// Enum tpye getter see @c FeedGroupType
- (FeedGroupType)typ { return (FeedGroupType)self.type; }
@@ -46,6 +48,16 @@
if (![self.refreshStr isEqualToString:refreshStr]) self.refreshStr = refreshStr;
}
/// @return Return static @c 16x16px NSImageNameFolder image.
- (NSImage*)groupIconImage16 {
static NSImage *groupIcon;
if (!groupIcon) {
groupIcon = [NSImage imageNamed:NSImageNameFolder];
groupIcon.size = NSMakeSize(16, 16);
}
return groupIcon;
}
#pragma mark - Handle Children And Parents -

View File

@@ -23,7 +23,7 @@
#ifndef Constants_h
#define Constants_h
// TODO: Add support for media player?
// TODO: Add support for media player? image feed?
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
// TODO: Disable 'update all' menu item during update?
@@ -31,6 +31,7 @@ static NSString *kNotificationFeedUpdated = @"baRSS-notification-feed-updated";
static NSString *kNotificationNetworkStatusChanged = @"baRSS-notification-network-status-changed";
static NSString *kNotificationTotalUnreadCountChanged = @"baRSS-notification-total-unread-count-changed";
static NSString *kNotificationTotalUnreadCountReset = @"baRSS-notification-total-unread-count-reset";
static NSString *kNotificationFaviconDownloadFinished = @"baRSS-notification-favicon-download-finished";
extern uint64_t dispatch_benchmark(size_t count, void (^block)(void));
//void benchmark(char *desc, dispatch_block_t b){printf("%s: %llu ns\n", desc, dispatch_benchmark(1, b));}

View File

@@ -34,7 +34,7 @@
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup" syncable="YES"/>
</entity>
<entity name="FeedIcon" representedClassName="FeedIcon" syncable="YES" codeGenerationType="class">
<attribute name="icon" optional="YES" attributeType="Binary" customClassName="NSImage" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" customClassName="NSImage" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="icon" inverseEntity="Feed" syncable="YES"/>
</entity>
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
@@ -49,8 +49,8 @@
</entity>
<elements>
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="195"/>
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="150"/>
<element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/>
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="150"/>
<element name="FeedIcon" positionX="-202.79296875" positionY="137.71875" width="128" height="75"/>
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="165"/>
</elements>

View File

@@ -23,15 +23,19 @@
#import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@class Feed;
@interface FeedDownload : NSObject
// Register for network change notifications
+ (void)registerNetworkChangeNotification;
+ (void)unregisterNetworkChangeNotification;
// Scheduled feed update
+ (void)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response))block;
+ (void)autoDownloadAndParseURL:(NSString*)url;
// Scheduling
+ (void)scheduleUpdateForUpcomingFeeds;
+ (void)forceUpdateAllFeeds;
// Downloading
+ (void)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response))block;
+ (void)autoDownloadAndParseURL:(NSString*)urlStr;
+ (void)backgroundDownloadFavicon:(NSString*)urlStr forFeed:(Feed*)feed;
// User interaction
+ (BOOL)allowNetworkConnection;
+ (BOOL)isPaused;

View File

@@ -150,14 +150,23 @@ static BOOL _nextUpdateIsForced = NO;
#pragma mark - Download RSS Feed -
/// @return Base URL part. E.g., https://stackoverflow.com/a/15897956/10616114 ==> https://stackoverflow.com/
+ (NSURL*)hostURL:(NSString*)urlStr {
return [[NSURL URLWithString:@"/" relativeToURL:[self fixURL:urlStr]] absoluteURL];
}
/// @return New request with no caching policy and timeout interval of 30 seconds.
+ (NSMutableURLRequest*)newRequestURL:(NSString*)urlStr {
/// Check if any scheme is set. If not, prepend 'http://'.
+ (NSURL*)fixURL:(NSString*)urlStr {
NSURL *url = [NSURL URLWithString:urlStr];
if (!url.scheme) {
url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", urlStr]]; // usually will redirect to https if necessary
}
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
return url;
}
/// @return New request with no caching policy and timeout interval of 30 seconds.
+ (NSMutableURLRequest*)newRequestURL:(NSString*)urlStr {
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[self fixURL:urlStr]];
req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
req.HTTPShouldHandleCookies = NO;
// req.timeoutInterval = 30;
@@ -274,9 +283,46 @@ static BOOL _nextUpdateIsForced = NO;
newFeed.sortIndex = (int32_t)idx;
[newFeed.feed calculateAndSetIndexPathString];
[StoreCoordinator saveContext:moc andParent:YES];
NSString *faviconURL = newFeed.feed.link;
if (faviconURL.length == 0)
faviconURL = meta.url;
[FeedDownload backgroundDownloadFavicon:faviconURL forFeed:newFeed.feed];
[moc reset];
}
/**
Try to download @c favicon.ico and save downscaled image to persistent store.
*/
+ (void)backgroundDownloadFavicon:(NSString*)urlStr forFeed:(Feed*)feed {
NSManagedObjectID *oid = feed.objectID;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSImage *img = [self downloadFavicon:urlStr];
if (img) {
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[moc performBlock:^{
Feed *f = [moc objectWithID:oid];
if (!f.icon)
f.icon = [[FeedIcon alloc] initWithEntity:FeedIcon.entity insertIntoManagedObjectContext:moc];
f.icon.icon = [img TIFFRepresentation];
[StoreCoordinator saveContext:moc andParent:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFaviconDownloadFinished object:f.objectID];
[moc reset];
}];
}
});
}
/// Download favicon located at http://.../ @c favicon.ico and rescale image to @c 16x16.
+ (NSImage*)downloadFavicon:(NSString*)urlStr {
NSURL *favURL = [[self hostURL:urlStr] URLByAppendingPathComponent:@"favicon.ico"];
NSImage *img = [[NSImage alloc] initWithContentsOfURL:favURL];
if (!img) return nil;
return [NSImage imageWithSize:NSMakeSize(16, 16) flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
[img drawInRect:dstRect];
return YES;
}];
}
#pragma mark - Network Connection & Reachability -

View File

@@ -110,14 +110,21 @@
Set @c scheduled to a new date if refresh interval was changed.
*/
- (void)applyChangesToCoreDataObject {
FeedMeta *meta = self.feedGroup.feed.meta;
Feed *feed = self.feedGroup.feed;
FeedMeta *meta = feed.meta;
BOOL intervalChanged = [meta setURL:self.previousURL refresh:self.refreshNum.intValue unit:(int16_t)self.refreshUnit.indexOfSelectedItem];
if (intervalChanged)
[meta calculateAndSetScheduled]; // updateTimer will be scheduled once preferences is closed
[self.feedGroup setName:self.name.stringValue andRefreshString:[meta readableRefreshString]];
if (self.didDownloadFeed) {
[meta setEtag:self.httpEtag modified:self.httpDate];
[self.feedGroup.feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
}
if (!feed.icon) {
NSString *faviconURL = feed.link;
if (faviconURL.length == 0)
faviconURL = meta.url;
[FeedDownload backgroundDownloadFavicon:faviconURL forFeed:feed];
}
}
@@ -152,7 +159,6 @@
self.httpEtag = [response allHeaderFields][@"Etag"];
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
[self updateTextFieldURL:response.URL.absoluteString andTitle:result.title];
// TODO: add icon download
// TODO: play error sound?
[self.spinnerURL stopAnimation:nil];
[self.spinnerName stopAnimation:nil];

View File

@@ -22,7 +22,6 @@
#import "SettingsFeeds.h"
#import "Constants.h"
#import "DrawImage.h"
#import "StoreCoordinator.h"
#import "ModalFeedEdit.h"
#import "Feed+Ext.h"
@@ -52,12 +51,31 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
self.dataStore.managedObjectContext.undoManager = self.undoManager;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(faviconDownloadFinished:) name:kNotificationFaviconDownloadFinished object:nil];
}
- (void)saveChanges {
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
/**
Called when the backgroud download of a favicon finished.
Notification object contains the updated @c Feed (object id).
*/
- (void)faviconDownloadFinished:(NSNotification*)notify {
if ([notify.object isKindOfClass:[NSManagedObjectID class]]) {
// TODO: Bug: Freshly ownloaded images are deleted on undo. Remove delete cascade rule?
NSManagedObject *mo = [self.dataStore.managedObjectContext objectWithID:notify.object];
if (!mo) return;
[self.dataStore.managedObjectContext refreshObject:mo mergeChanges:YES];
[self.dataStore rearrangeObjects];
}
}
#pragma mark - UI Button Interaction
- (IBAction)addFeed:(id)sender {
[self showModalForFeedGroup:nil isGroupEdit:NO];
}
@@ -95,9 +113,14 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
}
#pragma mark - Insert & Edit Feed Items
#pragma mark - Insert & Edit Feed Items / Modal Dialog
/// Save core data changes of current object context to persistent store
- (void)saveChanges {
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
}
/**
Open a new modal window to edit the selected @c FeedGroup.
@note isGroupEdit @c flag will be overwritten if @c FeedGroup parameter is not @c nil.
@@ -134,9 +157,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
}];
}
#pragma mark - Helper -
/// Insert @c FeedGroup item either after current selection or inside selected folder (if expanded)
- (FeedGroup*)insertFeedGroupAtSelection:(FeedGroupType)type {
FeedGroup *fg = [FeedGroup newGroup:type inContext:self.dataStore.managedObjectContext];
@@ -270,16 +290,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
return cellView; // the refresh cell is already skipped with the above if condition
} else {
cellView.textField.objectValue = fg.name;
if (fg.typ == GROUP) {
cellView.imageView.image = [NSImage imageNamed:NSImageNameFolder];
} else {
// TODO: load icon
static NSImage *defaultRSSIcon;
if (!defaultRSSIcon)
defaultRSSIcon = [RSSIcon iconWithSize:cellView.imageView.frame.size.height];
cellView.imageView.image = defaultRSSIcon;
}
cellView.imageView.image = (fg.typ == GROUP ? [NSImage imageNamed:NSImageNameFolder] : [fg.feed iconImage16]);
}
// also for refresh column
cellView.textField.textColor = (isFeed && refreshDisabled ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]);

View File

@@ -25,6 +25,7 @@
#import "StoreCoordinator.h"
#import "DrawImage.h"
#import "UserPrefs.h"
#import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
/// User preferences for displaying menu items
@@ -106,46 +107,18 @@ typedef NS_ENUM(char, DisplaySetting) {
self.submenu = [self.menu submenuWithIndex:fg.sortIndex isFeed:(fg.typ == FEED)];
[self setTitleAndUnreadCount:fg]; // after submenu is set
if (fg.typ == FEED) {
[self configureAsFeed:fg];
self.tag = ScopeFeed;
self.toolTip = fg.feed.subtitle;
self.enabled = (fg.feed.articles.count > 0);
self.image = [fg.feed iconImage16];
} else {
[self configureAsGroup:fg];
self.tag = ScopeGroup;
self.enabled = (fg.children.count > 0);
self.image = [fg groupIconImage16];
}
}
}
/**
Configure menu item to be used as a container for @c FeedArticle entries (incl. feed icon).
*/
- (void)configureAsFeed:(FeedGroup*)fg {
self.tag = ScopeFeed;
self.toolTip = fg.feed.subtitle;
self.enabled = (fg.feed.articles.count > 0);
// set icon
dispatch_async(dispatch_get_main_queue(), ^{
static NSImage *defaultRSSIcon;
if (!defaultRSSIcon)
defaultRSSIcon = [RSSIcon iconWithSize:16];
self.image = defaultRSSIcon;
});
}
/**
Configure menu item to be used as a container for multiple feeds.
*/
- (void)configureAsGroup:(FeedGroup*)fg {
self.tag = ScopeGroup;
self.enabled = (fg.children.count > 0);
// set icon
dispatch_async(dispatch_get_main_queue(), ^{
static NSImage *groupIcon;
if (!groupIcon) {
groupIcon = [NSImage imageNamed:NSImageNameFolder];
groupIcon.size = NSMakeSize(16, 16);
}
self.image = groupIcon;
});
}
/**
Populate @c NSMenuItem based on the attributes of a @c FeedArticle.
*/