Feed icons! '/favicon.ico' download, storage and display.
This commit is contained in:
@@ -60,8 +60,8 @@ ToDo
|
|||||||
- [x] Auto fix 301 Redirect or ask user
|
- [x] Auto fix 301 Redirect or ask user
|
||||||
- [x] Make `feed://` URLs clickable
|
- [x] Make `feed://` URLs clickable
|
||||||
- [ ] Feeds with authentication
|
- [ ] Feeds with authentication
|
||||||
- [ ] Show proper feed icon
|
- [x] Show proper feed icon
|
||||||
- [ ] Download and store icon file
|
- [x] Download and store icon file
|
||||||
|
|
||||||
|
|
||||||
- [ ] Other
|
- [ ] Other
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
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 */; };
|
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 */; };
|
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; };
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
54195881218A061100581B79 /* Feed+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Feed+Ext.h"; sourceTree = "<group>"; };
|
||||||
@@ -208,6 +210,7 @@
|
|||||||
54ACC27321061B3B0020715F = {
|
54ACC27321061B3B0020715F = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
540CD14821C094A2004AB594 /* README.md */,
|
||||||
54ACC27E21061B3B0020715F /* baRSS */,
|
54ACC27E21061B3B0020715F /* baRSS */,
|
||||||
54CC042D2162532800A48795 /* baRSS-Helper */,
|
54CC042D2162532800A48795 /* baRSS-Helper */,
|
||||||
54ACC27D21061B3B0020715F /* Products */,
|
54ACC27D21061B3B0020715F /* Products */,
|
||||||
@@ -364,6 +367,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
540CD14921C094A2004AB594 /* README.md in Resources */,
|
||||||
546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */,
|
546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */,
|
||||||
54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */,
|
54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */,
|
||||||
54ACC28621061B3C0020715F /* Assets.xcassets in Resources */,
|
54ACC28621061B3C0020715F /* Assets.xcassets in Resources */,
|
||||||
|
|||||||
@@ -33,4 +33,5 @@
|
|||||||
- (NSArray<FeedArticle*>*)sortedArticles;
|
- (NSArray<FeedArticle*>*)sortedArticles;
|
||||||
- (int)markAllItemsRead;
|
- (int)markAllItemsRead;
|
||||||
- (int)markAllItemsUnread;
|
- (int)markAllItemsUnread;
|
||||||
|
- (NSImage*)iconImage16;
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -21,11 +21,14 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
#import "Feed+Ext.h"
|
#import "Feed+Ext.h"
|
||||||
|
#import "Constants.h"
|
||||||
|
#import "DrawImage.h"
|
||||||
#import "FeedMeta+Ext.h"
|
#import "FeedMeta+Ext.h"
|
||||||
#import "FeedGroup+Ext.h"
|
#import "FeedGroup+Ext.h"
|
||||||
|
#import "FeedIcon+CoreDataClass.h"
|
||||||
#import "FeedArticle+CoreDataClass.h"
|
#import "FeedArticle+CoreDataClass.h"
|
||||||
#import "Constants.h"
|
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <RSXML/RSXML.h>
|
#import <RSXML/RSXML.h>
|
||||||
|
|
||||||
@implementation Feed (Ext)
|
@implementation Feed (Ext)
|
||||||
@@ -214,4 +217,19 @@
|
|||||||
return newCount - oldCount;
|
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
|
@end
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
|
|||||||
|
|
||||||
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)context;
|
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)context;
|
||||||
- (void)setName:(NSString*)name andRefreshString:(NSString*)refreshStr;
|
- (void)setName:(NSString*)name andRefreshString:(NSString*)refreshStr;
|
||||||
|
- (NSImage*)groupIconImage16;
|
||||||
// Handle children and parents
|
// Handle children and parents
|
||||||
- (NSString*)indexPathString;
|
- (NSString*)indexPathString;
|
||||||
- (NSMutableArray<FeedGroup*>*)allParents;
|
- (NSMutableArray<FeedGroup*>*)allParents;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
#import "FeedMeta+Ext.h"
|
#import "FeedMeta+Ext.h"
|
||||||
#import "Feed+Ext.h"
|
#import "Feed+Ext.h"
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
@implementation FeedGroup (Ext)
|
@implementation FeedGroup (Ext)
|
||||||
/// Enum tpye getter see @c FeedGroupType
|
/// Enum tpye getter see @c FeedGroupType
|
||||||
- (FeedGroupType)typ { return (FeedGroupType)self.type; }
|
- (FeedGroupType)typ { return (FeedGroupType)self.type; }
|
||||||
@@ -46,6 +48,16 @@
|
|||||||
if (![self.refreshStr isEqualToString:refreshStr]) self.refreshStr = refreshStr;
|
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 -
|
#pragma mark - Handle Children And Parents -
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#ifndef Constants_h
|
#ifndef Constants_h
|
||||||
#define 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" />
|
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
|
||||||
// TODO: Disable 'update all' menu item during update?
|
// 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 *kNotificationNetworkStatusChanged = @"baRSS-notification-network-status-changed";
|
||||||
static NSString *kNotificationTotalUnreadCountChanged = @"baRSS-notification-total-unread-count-changed";
|
static NSString *kNotificationTotalUnreadCountChanged = @"baRSS-notification-total-unread-count-changed";
|
||||||
static NSString *kNotificationTotalUnreadCountReset = @"baRSS-notification-total-unread-count-reset";
|
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));
|
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));}
|
//void benchmark(char *desc, dispatch_block_t b){printf("%s: %llu ns\n", desc, dispatch_benchmark(1, b));}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup" syncable="YES"/>
|
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="FeedIcon" representedClassName="FeedIcon" syncable="YES" codeGenerationType="class">
|
<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"/>
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="icon" inverseEntity="Feed" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
|
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="195"/>
|
<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="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="FeedIcon" positionX="-202.79296875" positionY="137.71875" width="128" height="75"/>
|
||||||
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="165"/>
|
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="165"/>
|
||||||
</elements>
|
</elements>
|
||||||
|
|||||||
@@ -23,15 +23,19 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <RSXML/RSXML.h>
|
#import <RSXML/RSXML.h>
|
||||||
|
|
||||||
|
@class Feed;
|
||||||
|
|
||||||
@interface FeedDownload : NSObject
|
@interface FeedDownload : NSObject
|
||||||
// Register for network change notifications
|
// Register for network change notifications
|
||||||
+ (void)registerNetworkChangeNotification;
|
+ (void)registerNetworkChangeNotification;
|
||||||
+ (void)unregisterNetworkChangeNotification;
|
+ (void)unregisterNetworkChangeNotification;
|
||||||
// Scheduled feed update
|
// Scheduling
|
||||||
+ (void)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response))block;
|
|
||||||
+ (void)autoDownloadAndParseURL:(NSString*)url;
|
|
||||||
+ (void)scheduleUpdateForUpcomingFeeds;
|
+ (void)scheduleUpdateForUpcomingFeeds;
|
||||||
+ (void)forceUpdateAllFeeds;
|
+ (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
|
// User interaction
|
||||||
+ (BOOL)allowNetworkConnection;
|
+ (BOOL)allowNetworkConnection;
|
||||||
+ (BOOL)isPaused;
|
+ (BOOL)isPaused;
|
||||||
|
|||||||
@@ -150,14 +150,23 @@ static BOOL _nextUpdateIsForced = NO;
|
|||||||
|
|
||||||
#pragma mark - Download RSS Feed -
|
#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.
|
/// Check if any scheme is set. If not, prepend 'http://'.
|
||||||
+ (NSMutableURLRequest*)newRequestURL:(NSString*)urlStr {
|
+ (NSURL*)fixURL:(NSString*)urlStr {
|
||||||
NSURL *url = [NSURL URLWithString:urlStr];
|
NSURL *url = [NSURL URLWithString:urlStr];
|
||||||
if (!url.scheme) {
|
if (!url.scheme) {
|
||||||
url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", urlStr]]; // usually will redirect to https if necessary
|
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.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||||
req.HTTPShouldHandleCookies = NO;
|
req.HTTPShouldHandleCookies = NO;
|
||||||
// req.timeoutInterval = 30;
|
// req.timeoutInterval = 30;
|
||||||
@@ -274,9 +283,46 @@ static BOOL _nextUpdateIsForced = NO;
|
|||||||
newFeed.sortIndex = (int32_t)idx;
|
newFeed.sortIndex = (int32_t)idx;
|
||||||
[newFeed.feed calculateAndSetIndexPathString];
|
[newFeed.feed calculateAndSetIndexPathString];
|
||||||
[StoreCoordinator saveContext:moc andParent:YES];
|
[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];
|
[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 -
|
#pragma mark - Network Connection & Reachability -
|
||||||
|
|
||||||
|
|||||||
@@ -110,14 +110,21 @@
|
|||||||
Set @c scheduled to a new date if refresh interval was changed.
|
Set @c scheduled to a new date if refresh interval was changed.
|
||||||
*/
|
*/
|
||||||
- (void)applyChangesToCoreDataObject {
|
- (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];
|
BOOL intervalChanged = [meta setURL:self.previousURL refresh:self.refreshNum.intValue unit:(int16_t)self.refreshUnit.indexOfSelectedItem];
|
||||||
if (intervalChanged)
|
if (intervalChanged)
|
||||||
[meta calculateAndSetScheduled]; // updateTimer will be scheduled once preferences is closed
|
[meta calculateAndSetScheduled]; // updateTimer will be scheduled once preferences is closed
|
||||||
[self.feedGroup setName:self.name.stringValue andRefreshString:[meta readableRefreshString]];
|
[self.feedGroup setName:self.name.stringValue andRefreshString:[meta readableRefreshString]];
|
||||||
if (self.didDownloadFeed) {
|
if (self.didDownloadFeed) {
|
||||||
[meta setEtag:self.httpEtag modified:self.httpDate];
|
[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.httpEtag = [response allHeaderFields][@"Etag"];
|
||||||
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
|
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
|
||||||
[self updateTextFieldURL:response.URL.absoluteString andTitle:result.title];
|
[self updateTextFieldURL:response.URL.absoluteString andTitle:result.title];
|
||||||
// TODO: add icon download
|
|
||||||
// TODO: play error sound?
|
// TODO: play error sound?
|
||||||
[self.spinnerURL stopAnimation:nil];
|
[self.spinnerURL stopAnimation:nil];
|
||||||
[self.spinnerName stopAnimation:nil];
|
[self.spinnerName stopAnimation:nil];
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
#import "SettingsFeeds.h"
|
#import "SettingsFeeds.h"
|
||||||
#import "Constants.h"
|
#import "Constants.h"
|
||||||
#import "DrawImage.h"
|
|
||||||
#import "StoreCoordinator.h"
|
#import "StoreCoordinator.h"
|
||||||
#import "ModalFeedEdit.h"
|
#import "ModalFeedEdit.h"
|
||||||
#import "Feed+Ext.h"
|
#import "Feed+Ext.h"
|
||||||
@@ -52,12 +51,31 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
|||||||
|
|
||||||
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
|
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
|
||||||
self.dataStore.managedObjectContext.undoManager = self.undoManager;
|
self.dataStore.managedObjectContext.undoManager = self.undoManager;
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(faviconDownloadFinished:) name:kNotificationFaviconDownloadFinished object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)saveChanges {
|
- (void)dealloc {
|
||||||
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
|
[[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 {
|
- (IBAction)addFeed:(id)sender {
|
||||||
[self showModalForFeedGroup:nil isGroupEdit:NO];
|
[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.
|
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.
|
@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)
|
/// Insert @c FeedGroup item either after current selection or inside selected folder (if expanded)
|
||||||
- (FeedGroup*)insertFeedGroupAtSelection:(FeedGroupType)type {
|
- (FeedGroup*)insertFeedGroupAtSelection:(FeedGroupType)type {
|
||||||
FeedGroup *fg = [FeedGroup newGroup:type inContext:self.dataStore.managedObjectContext];
|
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
|
return cellView; // the refresh cell is already skipped with the above if condition
|
||||||
} else {
|
} else {
|
||||||
cellView.textField.objectValue = fg.name;
|
cellView.textField.objectValue = fg.name;
|
||||||
if (fg.typ == GROUP) {
|
cellView.imageView.image = (fg.typ == GROUP ? [NSImage imageNamed:NSImageNameFolder] : [fg.feed iconImage16]);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// also for refresh column
|
// also for refresh column
|
||||||
cellView.textField.textColor = (isFeed && refreshDisabled ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]);
|
cellView.textField.textColor = (isFeed && refreshDisabled ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#import "StoreCoordinator.h"
|
#import "StoreCoordinator.h"
|
||||||
#import "DrawImage.h"
|
#import "DrawImage.h"
|
||||||
#import "UserPrefs.h"
|
#import "UserPrefs.h"
|
||||||
|
#import "Feed+Ext.h"
|
||||||
#import "FeedGroup+Ext.h"
|
#import "FeedGroup+Ext.h"
|
||||||
|
|
||||||
/// User preferences for displaying menu items
|
/// 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.submenu = [self.menu submenuWithIndex:fg.sortIndex isFeed:(fg.typ == FEED)];
|
||||||
[self setTitleAndUnreadCount:fg]; // after submenu is set
|
[self setTitleAndUnreadCount:fg]; // after submenu is set
|
||||||
if (fg.typ == FEED) {
|
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 {
|
} 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.
|
Populate @c NSMenuItem based on the attributes of a @c FeedArticle.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user