diff --git a/README.md b/README.md index a667c83..2c45c3f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/baRSS.xcodeproj/project.pbxproj b/baRSS.xcodeproj/project.pbxproj index 68f6c4f..c9c622c 100644 --- a/baRSS.xcodeproj/project.pbxproj +++ b/baRSS.xcodeproj/project.pbxproj @@ -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 = ""; }; 540F704321B6C16C0022E69D /* FeedMeta+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedMeta+Ext.h"; sourceTree = ""; }; 540F704421B6C16C0022E69D /* FeedMeta+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedMeta+Ext.m"; sourceTree = ""; }; 54195881218A061100581B79 /* Feed+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Feed+Ext.h"; sourceTree = ""; }; @@ -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 */, diff --git a/baRSS/Categories/Feed+Ext.h b/baRSS/Categories/Feed+Ext.h index 64057e1..d75af57 100644 --- a/baRSS/Categories/Feed+Ext.h +++ b/baRSS/Categories/Feed+Ext.h @@ -33,4 +33,5 @@ - (NSArray*)sortedArticles; - (int)markAllItemsRead; - (int)markAllItemsUnread; +- (NSImage*)iconImage16; @end diff --git a/baRSS/Categories/Feed+Ext.m b/baRSS/Categories/Feed+Ext.m index fc60b98..fdbed87 100644 --- a/baRSS/Categories/Feed+Ext.m +++ b/baRSS/Categories/Feed+Ext.m @@ -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 #import @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 diff --git a/baRSS/Categories/FeedGroup+Ext.h b/baRSS/Categories/FeedGroup+Ext.h index 99d9db1..32794eb 100644 --- a/baRSS/Categories/FeedGroup+Ext.h +++ b/baRSS/Categories/FeedGroup+Ext.h @@ -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*)allParents; diff --git a/baRSS/Categories/FeedGroup+Ext.m b/baRSS/Categories/FeedGroup+Ext.m index 8858041..d346ba3 100644 --- a/baRSS/Categories/FeedGroup+Ext.m +++ b/baRSS/Categories/FeedGroup+Ext.m @@ -24,6 +24,8 @@ #import "FeedMeta+Ext.h" #import "Feed+Ext.h" +#import + @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 - diff --git a/baRSS/Constants.h b/baRSS/Constants.h index b522dbb..9a9d2f9 100644 --- a/baRSS/Constants.h +++ b/baRSS/Constants.h @@ -23,7 +23,7 @@ #ifndef Constants_h #define Constants_h -// TODO: Add support for media player? +// TODO: Add support for media player? image feed? // // 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));} diff --git a/baRSS/DBv1.xcdatamodeld/DBv1.xcdatamodel/contents b/baRSS/DBv1.xcdatamodeld/DBv1.xcdatamodel/contents index 12feb10..5cdf402 100644 --- a/baRSS/DBv1.xcdatamodeld/DBv1.xcdatamodel/contents +++ b/baRSS/DBv1.xcdatamodeld/DBv1.xcdatamodel/contents @@ -34,7 +34,7 @@ - + @@ -49,8 +49,8 @@ - + diff --git a/baRSS/FeedDownload.h b/baRSS/FeedDownload.h index 5aeecf0..a359ee2 100644 --- a/baRSS/FeedDownload.h +++ b/baRSS/FeedDownload.h @@ -23,15 +23,19 @@ #import #import +@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; diff --git a/baRSS/FeedDownload.m b/baRSS/FeedDownload.m index 130f938..53e9f42 100644 --- a/baRSS/FeedDownload.m +++ b/baRSS/FeedDownload.m @@ -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 - diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m index adbf1c9..cab5210 100644 --- a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m @@ -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]; diff --git a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m index 404a9d3..78b7626 100644 --- a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m @@ -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]); diff --git a/baRSS/Status Bar Menu/NSMenuItem+Ext.m b/baRSS/Status Bar Menu/NSMenuItem+Ext.m index d5c8ce9..e32cc0c 100644 --- a/baRSS/Status Bar Menu/NSMenuItem+Ext.m +++ b/baRSS/Status Bar Menu/NSMenuItem+Ext.m @@ -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. */