Refactoring Part 2: Unread count in Feed instead of Config

This commit is contained in:
relikd
2018-12-04 01:01:28 +01:00
parent 6223d1a169
commit ae4700faca
19 changed files with 560 additions and 445 deletions

View File

@@ -34,7 +34,7 @@ ToDo
- [ ] Choose status bar icon? - [ ] Choose status bar icon?
- [ ] Tick mark feed items based on prefs - [ ] Tick mark feed items based on prefs
- [ ] Open a few links (# editable) - [ ] Open a few links (# editable)
- [ ] Performance: Update menu partially - [x] Performance: Update menu partially
- [x] Start on login - [x] Start on login
- [x] Make it system default application - [x] Make it system default application
- [ ] Display license info (e.g., RSXML) - [ ] Display license info (e.g., RSXML)
@@ -45,10 +45,11 @@ ToDo
- [ ] Status menu - [ ] Status menu
- [ ] Update menu header after mark (un)read - [x] Update menu header after mark (un)read
- [ ] Pause updates functionality - [ ] Pause updates functionality
- [x] Update all feeds functionality - [x] Update all feeds functionality
- [ ] Hold only relevant information in memory - [x] Hold only relevant information in memory
- [ ] Icon for paused / no internet state
- [ ] Edit feed - [ ] Edit feed
@@ -68,7 +69,7 @@ ToDo
- [ ] Translate text to different languages - [ ] Translate text to different languages
- [x] Automatically update feeds with chosen interval - [x] Automatically update feeds with chosen interval
- [x] Reuse ETag and Modification date - [x] Reuse ETag and Modification date
- ~~[ ] Append only new items, keep sorting~~ - [x] Append only new items, keep sorting
- [x] Delete old ones eventually - [x] Delete old ones eventually
- [x] Pause on internet connection lost - [x] Pause on internet connection lost
- [ ] Download with ephemeral url session? - [ ] Download with ephemeral url session?
@@ -85,7 +86,7 @@ ToDo
- [ ] Notification Center - [ ] Notification Center
- [ ] Sleep timer. (e.g., disable updates during working hours) - [ ] Sleep timer. (e.g., disable updates during working hours)
- [ ] Pure image feed? (show images directly in menu) - [ ] Pure image feed? (show images directly in menu)
- [ ] Infinite storage. (load more button) - ~~[ ] Infinite storage. (load more button)~~
- [ ] Automatically open feed items? - [ ] Automatically open feed items?
- [ ] Per feed launch application (e.g., for podcasts) - [ ] Per feed launch application (e.g., for podcasts)
- [ ] Per group setting to exclude unread count from menu bar - [ ] Per group setting to exclude unread count from menu bar

View File

@@ -25,8 +25,9 @@
@class RSParsedFeed; @class RSParsedFeed;
@interface Feed (Ext) @interface Feed (Ext)
+ (Feed*)feedFromRSS:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls unread:(int*)unreadCount; - (void)updateWithRSS:(RSParsedFeed*)obj;
- (NSArray<NSString*>*)alreadyReadURLs; - (NSArray<FeedItem*>*)sortedArticles;
- (void)markAllItemsRead;
- (void)markAllItemsUnread; - (int)markAllItemsRead;
- (int)markAllItemsUnread;
@end @end

View File

@@ -27,8 +27,50 @@
@implementation Feed (Ext) @implementation Feed (Ext)
+ (FeedItem*)createFeedItemFrom:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)context { /**
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context]; Replace feed title, subtitle and link (if changed). Also adds new articles and removes old ones.
*/
- (void)updateWithRSS:(RSParsedFeed*)obj {
if (![self.title isEqualToString:obj.title]) self.title = obj.title;
if (![self.subtitle isEqualToString:obj.subtitle]) self.subtitle = obj.subtitle;
if (![self.link isEqualToString:obj.link]) self.link = obj.link;
NSMutableSet<NSString*> *urls = [[self.items valueForKeyPath:@"link"] mutableCopy];
if ([self addMissingArticles:obj updateLinks:urls]) // will remove links in 'urls' that should be kept
[self deleteArticlesWithLink:urls]; // remove old, outdated articles
}
/**
Append new articles and increment their sortIndex. Update article counter and unread counter on the way.
@param urls Input will be used to identify new articles. Output will contain URLs that aren't present in the feed anymore.
@return @c YES if new items were added, @c NO otherwise.
*/
- (BOOL)addMissingArticles:(RSParsedFeed*)obj updateLinks:(NSMutableSet<NSString*>*)urls {
int latestID = [[self.items valueForKeyPath:@"@max.sortIndex"] intValue];
__block int newOnes = 0;
[obj.articles enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(RSParsedArticle * _Nonnull article, BOOL * _Nonnull stop) {
// reverse enumeration ensures correct article order
if ([urls containsObject:article.link]) {
[urls removeObject:article.link];
} else {
newOnes += 1;
[self insertArticle:article atIndex:latestID + newOnes];
}
}];
if (newOnes == 0) return NO;
self.articleCount += newOnes;
self.unreadCount += newOnes; // new articles are by definition unread
return YES;
}
/**
Create article based on input and insert into core data storage.
*/
- (void)insertArticle:(RSParsedArticle*)entry atIndex:(int)idx {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:self.managedObjectContext];
b.sortIndex = (int32_t)idx;
b.unread = YES;
b.guid = entry.guid; b.guid = entry.guid;
b.title = entry.title; b.title = entry.title;
b.abstract = entry.abstract; b.abstract = entry.abstract;
@@ -36,54 +78,72 @@
b.author = entry.author; b.author = entry.author;
b.link = entry.link; b.link = entry.link;
b.published = entry.datePublished; b.published = entry.datePublished;
return b; [self addItemsObject:b];
} }
+ (Feed*)feedFromRSS:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls unread:(int*)unreadCount { /**
Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context]; Delete all items where @c link matches one of the URLs in the @c NSSet.
a.title = obj.title; */
a.subtitle = obj.subtitle; - (void)deleteArticlesWithLink:(NSMutableSet<NSString*>*)urls {
a.link = obj.link; if (!urls || urls.count == 0)
for (RSParsedArticle *article in obj.articles) { return;
FeedItem *b = [self createFeedItemFrom:article inContext:context];
if ([urls containsObject:b.link]) { self.articleCount -= (int32_t)urls.count;
b.unread = NO; for (FeedItem *item in self.items) {
} else { if ([urls containsObject:item.link]) {
*unreadCount += 1; [urls removeObject:item.link];
} if (item.unread)
[a addItemsObject:b]; self.unreadCount -= 1;
} // TODO: keep unread articles?
return a; [item.managedObjectContext deleteObject:item];
} if (urls.count == 0)
break;
- (NSArray<NSString*>*)alreadyReadURLs {
if (!self.items || self.items.count == 0) return nil;
NSMutableArray<NSString*> *mArr = [NSMutableArray arrayWithCapacity:self.items.count];
for (FeedItem *f in self.items) {
if (!f.unread) {
[mArr addObject:f.link];
} }
} }
return mArr;
} }
- (void)markAllItemsRead { /**
[self markAllArticlesRead:YES]; @return Articles sorted by attribute @c sortIndex with descending order (newest items first).
*/
- (NSArray<FeedItem*>*)sortedArticles {
if (self.items.count == 0)
return nil;
return [self.items sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
} }
- (void)markAllItemsUnread { /**
[self markAllArticlesRead:NO]; For all articles set @c unread @c = @c NO
@return Change in unread count. (0 or negative number)
*/
- (int)markAllItemsRead {
return [self markAllArticlesRead:YES];
} }
- (void)markAllArticlesRead:(BOOL)readFlag { /**
int count = 0; For all articles set @c unread @c = @c YES
@return Change in unread count. (0 or positive number)
*/
- (int)markAllItemsUnread {
return [self markAllArticlesRead:NO];
}
/**
Mark all articles read or unread and update @c unreadCount
@param readFlag @c YES: mark items read; @c NO: mark items unread
*/
- (int)markAllArticlesRead:(BOOL)readFlag {
for (FeedItem *i in self.items) { for (FeedItem *i in self.items) {
if (i.unread == readFlag) { if (i.unread == readFlag)
i.unread = !readFlag; i.unread = !readFlag;
++count;
}
} }
[self.config markUnread:(readFlag ? -count : +count) ancestorsOnly:NO]; int32_t oldCount = self.unreadCount;
int32_t newCount = (readFlag ? 0 : self.articleCount);
if (self.unreadCount != newCount)
self.unreadCount = newCount;
return newCount - oldCount;
} }
@end @end

View File

@@ -33,16 +33,15 @@ typedef enum int16_t {
} FeedConfigType; } FeedConfigType;
@property (getter=typ, setter=setTyp:) FeedConfigType typ; @property (getter=typ, setter=setTyp:) FeedConfigType typ;
// Handle children and parents
- (NSArray<FeedConfig*>*)sortedChildren; - (NSString*)indexPathString;
- (NSIndexPath*)indexPath; - (NSMutableArray<FeedConfig*>*)allParents;
- (void)markUnread:(int)count ancestorsOnly:(BOOL)flag; - (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
- (void)calculateAndSetScheduled; // Update feed and meta
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed*,BOOL*))block;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (void)updateRSSFeed:(RSParsedFeed*)obj; - (void)updateRSSFeed:(RSParsedFeed*)obj;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (void)calculateAndSetScheduled;
// Printing
- (NSString*)readableRefreshString; - (NSString*)readableRefreshString;
- (NSString*)readableDescription; - (NSString*)readableDescription;
@end @end

View File

@@ -31,76 +31,40 @@
/// Enum type setter see @c FeedConfigType /// Enum type setter see @c FeedConfigType
- (void)setTyp:(FeedConfigType)typ { self.type = typ; } - (void)setTyp:(FeedConfigType)typ { self.type = typ; }
/**
Sorted children array based on sort order provided in feed settings.
@return Sorted array of @c FeedConfig items. #pragma mark - Handle Children And Parents -
*/
/// @return IndexPath as semicolon separated string for sorted children starting with root index.
- (NSString*)indexPathString {
if (self.parent == nil)
return [NSString stringWithFormat:@"%d", self.sortIndex];
return [[self.parent indexPathString] stringByAppendingFormat:@".%d", self.sortIndex];
}
/// @return Children sorted by attribute @c sortIndex (same order as in preferences).
- (NSArray<FeedConfig*>*)sortedChildren { - (NSArray<FeedConfig*>*)sortedChildren {
if (self.children.count == 0) if (self.children.count == 0)
return nil; return nil;
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]]; return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
} }
/// IndexPath for sorted children starting with root index. /// @return @c NSArray of all ancestors: First object is root. Last object is the @c FeedConfig that executed the command.
- (NSIndexPath*)indexPath { - (NSMutableArray<FeedConfig*>*)allParents {
if (self.parent == nil) if (self.parent == nil)
return [NSIndexPath indexPathWithIndex:(NSUInteger)self.sortIndex]; return [NSMutableArray arrayWithObject:self];
return [[self.parent indexPath] indexPathByAddingIndex:(NSUInteger)self.sortIndex]; NSMutableArray *arr = [self.parent allParents];
[arr addObject:self];
return arr;
} }
/** /**
Change unread counter for all parents recursively. Result will never be negative. Iterate over all descenden feeds.
@param count If negative, mark items read. @param ordered If @c YES items are executed in the same order they are listed in the menu. Pass @n NO for a speed-up.
@param block Set @c cancel to @c YES to stop execution of further descendants.
@return @c NO if execution was stopped with @c cancel @c = @c YES in @c block.
*/ */
- (void)markUnread:(int)count ancestorsOnly:(BOOL)flag {
FeedConfig *par = (flag ? self.parent : self);
while (par) {
[self.managedObjectContext refreshObject:par mergeChanges:YES];
par.unreadCount += count;
NSAssert(par.unreadCount >= 0, @"ERROR ancestorsMarkUnread: Count should never be negative.");
par = par.parent;
}
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged
object:[NSNumber numberWithInt:count]];
}
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
- (NSTimeInterval)timeInterval {
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
return self.refreshNum * unit[self.refreshUnit % 5];
}
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
- (void)calculateAndSetScheduled {
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
}
/// Update FeedMeta or create new one if needed.
- (void)setEtag:(NSString*)etag modified:(NSString*)modified {
// TODO: move to separate function and add icon download
if (!self.meta) {
self.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:self.managedObjectContext];
}
self.meta.httpEtag = etag;
self.meta.httpModified = modified;
}
/// Delete any existing feed object and parse new one. Read state will be copied.
- (void)updateRSSFeed:(RSParsedFeed*)obj {
NSArray<NSString*> *readURLs = [self.feed alreadyReadURLs];
int unreadBefore = self.unreadCount;
int unreadAfter = 0;
if (self.feed)
[self.managedObjectContext deleteObject:(NSManagedObject*)self.feed];
if (obj) {
// TODO: update and dont re-create each time
self.feed = [Feed feedFromRSS:obj inContext:self.managedObjectContext alreadyRead:readURLs unread:&unreadAfter];
}
[self markUnread:(unreadAfter - unreadBefore) ancestorsOnly:NO];
}
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed*,BOOL*))block { - (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed*,BOOL*))block {
if (self.feed) { if (self.feed) {
BOOL stopEarly = NO; BOOL stopEarly = NO;
@@ -115,8 +79,46 @@
return YES; return YES;
} }
#pragma mark - Update Feed And Meta -
/// Delete any existing feed object and parse new one. Read state will be copied.
- (void)updateRSSFeed:(RSParsedFeed*)obj {
if (!self.feed) {
self.feed = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:self.managedObjectContext];
self.feed.indexPath = [self indexPathString];
}
int32_t unreadBefore = self.feed.unreadCount;
[self.feed updateWithRSS:obj];
NSNumber *cDiff = [NSNumber numberWithInteger:self.feed.unreadCount - unreadBefore];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff];
}
/// Update FeedMeta or create new one if needed.
- (void)setEtag:(NSString*)etag modified:(NSString*)modified {
if (!self.meta) {
self.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:self.managedObjectContext];
}
if (![self.meta.httpEtag isEqualToString:etag]) self.meta.httpEtag = etag;
if (![self.meta.httpModified isEqualToString:modified]) self.meta.httpModified = modified;
}
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
- (void)calculateAndSetScheduled {
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
}
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
- (NSTimeInterval)timeInterval {
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
return self.refreshNum * unit[self.refreshUnit % 5];
}
#pragma mark - Printing - #pragma mark - Printing -
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h ) /// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (NSString*)readableRefreshString { - (NSString*)readableRefreshString {
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]]; return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];

View File

@@ -26,5 +26,10 @@
static NSString *kNotificationFeedUpdated = @"baRSS-notification-feed-updated"; 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";
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));}
#define benchmark(desc,block) printf(desc": %llu ns\n", dispatch_benchmark(1, block));
#endif /* Constants_h */ #endif /* Constants_h */

View File

@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G3025" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1"> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G3025" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1">
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class"> <entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
<attribute name="articleCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/> <attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/> <attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/> <attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="unreadCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<relationship name="config" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="feed" inverseEntity="FeedConfig" syncable="YES"/> <relationship name="config" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="feed" inverseEntity="FeedConfig" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="FeedItem" inverseName="feed" inverseEntity="FeedItem" syncable="YES"/> <relationship name="items" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedItem" inverseName="feed" inverseEntity="FeedItem" syncable="YES"/>
</entity> </entity>
<entity name="FeedConfig" representedClassName="FeedConfig" syncable="YES" codeGenerationType="class"> <entity name="FeedConfig" representedClassName="FeedConfig" syncable="YES" codeGenerationType="class">
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/> <attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
@@ -15,7 +18,6 @@
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/> <attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/> <attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
<attribute name="unreadCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="url" optional="YES" attributeType="String" syncable="YES"/> <attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedConfig" inverseName="parent" inverseEntity="FeedConfig" syncable="YES"/> <relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedConfig" inverseName="parent" inverseEntity="FeedConfig" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="config" inverseEntity="Feed" syncable="YES"/> <relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="config" inverseEntity="Feed" syncable="YES"/>
@@ -29,6 +31,7 @@
<attribute name="guid" optional="YES" attributeType="String" syncable="YES"/> <attribute name="guid" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/> <attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray" syncable="YES"/> <attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray" syncable="YES"/>
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/> <attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/> <attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/> <relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/>
@@ -40,9 +43,9 @@
<relationship name="config" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="meta" inverseEntity="FeedConfig" syncable="YES"/> <relationship name="config" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="meta" inverseEntity="FeedConfig" syncable="YES"/>
</entity> </entity>
<elements> <elements>
<element name="Feed" positionX="-229.09375" positionY="-2.30859375" width="128" height="120"/> <element name="Feed" positionX="-229.09375" positionY="-2.30859375" width="128" height="165"/>
<element name="FeedConfig" positionX="-441.87109375" positionY="-47.390625" width="128" height="240"/> <element name="FeedConfig" positionX="-441.87109375" positionY="-47.390625" width="128" height="225"/>
<element name="FeedItem" positionX="-28.140625" positionY="-17.359375" width="128" height="180"/> <element name="FeedItem" positionX="-28.140625" positionY="-17.359375" width="128" height="195"/>
<element name="FeedMeta" positionX="-234" positionY="72" width="128" height="105"/> <element name="FeedMeta" positionX="-234" positionY="72" width="128" height="105"/>
</elements> </elements>
</model> </model>

View File

@@ -70,7 +70,7 @@ static BOOL _isReachable = NO;
+ (void)scheduleNextUpdate:(BOOL)forceUpdate { + (void)scheduleNextUpdate:(BOOL)forceUpdate {
static NSTimer *_updateTimer; static NSTimer *_updateTimer;
@synchronized (_updateTimer) { @synchronized (_updateTimer) { // TODO: dig into analyzer warning
if (_updateTimer) { if (_updateTimer) {
[_updateTimer invalidate]; [_updateTimer invalidate];
_updateTimer = nil; _updateTimer = nil;
@@ -112,7 +112,7 @@ static BOOL _isReachable = NO;
} }
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[StoreCoordinator saveContext:childContext andParent:YES]; [StoreCoordinator saveContext:childContext andParent:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:[list valueForKeyPath:@"objectID"]];
[childContext reset]; [childContext reset];
childContext = nil; childContext = nil;
[self scheduleNextUpdate:NO]; // after forced update, continue regular cycle [self scheduleNextUpdate:NO]; // after forced update, continue regular cycle

View File

@@ -111,6 +111,7 @@
if (self.shouldDeletePrevArticles) { if (self.shouldDeletePrevArticles) {
[item updateRSSFeed:self.feedResult]; [item updateRSSFeed:self.feedResult];
[item setEtag:self.httpEtag modified:self.httpDate]; [item setEtag:self.httpEtag modified:self.httpDate];
// TODO: add icon download
} }
if ([item.managedObjectContext hasChanges]) { if ([item.managedObjectContext hasChanges]) {
self.objectIsModified = YES; self.objectIsModified = YES;

View File

@@ -26,6 +26,7 @@
#import "ModalFeedEdit.h" #import "ModalFeedEdit.h"
#import "DrawImage.h" #import "DrawImage.h"
#import "StoreCoordinator.h" #import "StoreCoordinator.h"
#import "Constants.h"
@interface SettingsFeeds () <ModalEditDelegate> @interface SettingsFeeds () <ModalEditDelegate>
@property (weak) IBOutlet NSOutlineView *outlineView; @property (weak) IBOutlet NSOutlineView *outlineView;
@@ -77,11 +78,14 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (IBAction)remove:(id)sender { - (IBAction)remove:(id)sender {
[self.undoManager beginUndoGrouping]; [self.undoManager beginUndoGrouping];
for (NSIndexPath *path in self.dataStore.selectionIndexPaths) NSArray<NSTreeNode*> *parentNodes = [self.dataStore.selectedNodes valueForKeyPath:@"parentNode"];
[self incrementIndicesBy:-1 forSubsequentNodes:path];
[self.dataStore remove:sender]; [self.dataStore remove:sender];
for (NSTreeNode *parent in parentNodes) {
[self restoreOrderingAndIndexPathStr:parent];
}
[self.undoManager endUndoGrouping]; [self.undoManager endUndoGrouping];
[self saveChanges]; [self saveChanges];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
} }
- (IBAction)doubleClickOutlineView:(NSOutlineView*)sender { - (IBAction)doubleClickOutlineView:(NSOutlineView*)sender {
@@ -126,48 +130,58 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
}]; }];
} }
/// Called after an item was modified. May be called twice if download was still in progress.
- (void)modalDidUpdateFeedConfig:(FeedConfig*)config { - (void)modalDidUpdateFeedConfig:(FeedConfig*)config {
[self saveChanges]; // TODO: adjust total count [self saveChanges];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
} }
#pragma mark - Helper -
/// Insert @c FeedConfig item either after current selection or inside selected folder (if expanded)
- (FeedConfig*)insertSortedItemAtSelection { - (FeedConfig*)insertSortedItemAtSelection {
NSIndexPath *selectedIndex = [self.dataStore selectionIndexPath];
if (selectedIndex == NULL)
selectedIndex = [NSIndexPath new];
NSIndexPath *insertIndex = selectedIndex;
FeedConfig *selected = [[[self.dataStore arrangedObjects] descendantNodeAtIndexPath:selectedIndex] representedObject];
NSUInteger lastIndex = (selected ? selected.children.count : self.dataStore.arrangedObjects.childNodes.count);
BOOL groupSelected = (selected.typ == GROUP);
if (!groupSelected) {
lastIndex = (NSUInteger)selected.sortIndex + 1; // insert after selection
insertIndex = [insertIndex indexPathByRemovingLastIndex];
[self incrementIndicesBy:+1 forSubsequentNodes:selectedIndex];
--selected.sortIndex; // insert after selection
}
FeedConfig *newItem = [[FeedConfig alloc] initWithEntity:FeedConfig.entity insertIntoManagedObjectContext:self.dataStore.managedObjectContext]; FeedConfig *newItem = [[FeedConfig alloc] initWithEntity:FeedConfig.entity insertIntoManagedObjectContext:self.dataStore.managedObjectContext];
[self.dataStore insertObject:newItem atArrangedObjectIndexPath:[insertIndex indexPathByAddingIndex:lastIndex]]; NSTreeNode *selection = [[self.dataStore selectedNodes] firstObject];
// First insert, then parent, else troubles NSIndexPath *pth = nil;
newItem.sortIndex = (int32_t)lastIndex;
newItem.parent = (groupSelected ? selected : selected.parent); if (!selection) { // append to root
pth = [NSIndexPath indexPathWithIndex:[self.dataStore arrangedObjects].childNodes.count]; // or 0 to append at front
} else if ([self.outlineView isItemExpanded:selection]) { // append to group (if open)
pth = [selection.indexPath indexPathByAddingIndex:0]; // or 'selection.childNodes.count' to append at end
} else { // append before / after selected item
pth = selection.indexPath;
// remove the two lines below to insert infront of selection (instead of after selection)
NSUInteger lastIdx = [pth indexAtPosition:pth.length - 1];
pth = [[pth indexPathByRemovingLastIndex] indexPathByAddingIndex:lastIdx + 1];
}
[self.dataStore insertObject:newItem atArrangedObjectIndexPath:pth];
if (pth.length > 2) { // some subfolder; not root folder (has parent!)
NSTreeNode *parentNode = [[self.dataStore arrangedObjects] descendantNodeAtIndexPath:pth].parentNode;
newItem.parent = parentNode.representedObject;
[self restoreOrderingAndIndexPathStr:parentNode];
} else {
[self restoreOrderingAndIndexPathStr:[self.dataStore arrangedObjects]]; // .parent = nil
}
return newItem; return newItem;
} }
/// Loop over all descendants and update @c sortIndex @c (FeedConfig) as well as all @c indexPath @c (Feed)
#pragma mark - Import & Export of Data - (void)restoreOrderingAndIndexPathStr:(NSTreeNode*)parent {
NSArray<NSTreeNode*> *children = parent.childNodes;
for (NSUInteger i = 0; i < children.count; i++) {
- (void)incrementIndicesBy:(int)val forSubsequentNodes:(NSIndexPath*)path { NSTreeNode *n = [children objectAtIndex:i];
NSIndexPath *parentPath = [path indexPathByRemovingLastIndex]; FeedConfig *fc = n.representedObject;
NSTreeNode *root = [self.dataStore arrangedObjects]; // Re-calculate sort index for all affected parents
if (parentPath.length > 0) if (fc.sortIndex != (int32_t)i)
root = [root descendantNodeAtIndexPath:parentPath]; fc.sortIndex = (int32_t)i;
// Re-calculate index path for all contained feed items
for (NSUInteger i = [path indexAtPosition:path.length - 1]; i < root.childNodes.count; i++) { [fc iterateSorted:NO overDescendantFeeds:^(Feed *feed, BOOL *cancel) {
FeedConfig *conf = [root.childNodes[i] representedObject]; NSString *pthStr = [feed.config indexPathString];
conf.sortIndex += val; if (![feed.indexPath isEqualToString:pthStr])
feed.indexPath = pthStr;
}];
} }
} }
@@ -196,49 +210,20 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
} }
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index { - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index {
NSArray<NSTreeNode *> *dstChildren = [item childNodes]; NSTreeNode *destParent = (item != nil ? item : [self.dataStore arrangedObjects]);
if (!item || !dstChildren) NSUInteger idx = (NSUInteger)index;
dstChildren = [self.dataStore arrangedObjects].childNodes; if (index == -1) // drag items on folder or root drop
idx = destParent.childNodes.count;
NSIndexPath *dest = [destParent indexPath];
BOOL isFolderDrag = (index == -1); NSArray<NSTreeNode*> *previousParents = [self.currentlyDraggedNodes valueForKeyPath:@"parentNode"];
NSUInteger insertIndex = (isFolderDrag ? dstChildren.count : (NSUInteger)index); [self.dataStore moveNodes:self.currentlyDraggedNodes toIndexPath:[dest indexPathByAddingIndex:idx]];
// index where the items will be moved to, but not final since items above can vanish
NSIndexPath *dest = [item indexPath];
if (!dest) dest = [NSIndexPath indexPathWithIndex:insertIndex];
else dest = [dest indexPathByAddingIndex:insertIndex];
// decrement index for every item that is dragged from the same location (above the destination) for (NSTreeNode *node in previousParents) {
NSUInteger updateIndex = insertIndex; [self restoreOrderingAndIndexPathStr:node];
for (NSTreeNode *node in self.currentlyDraggedNodes) {
NSIndexPath *nodesPath = [node indexPath];
if ([[nodesPath indexPathByRemovingLastIndex] isEqualTo:[dest indexPathByRemovingLastIndex]] &&
insertIndex > [nodesPath indexAtPosition:nodesPath.length - 1])
{
--updateIndex;
}
}
for (NSUInteger i = self.currentlyDraggedNodes.count; i > 0; i--) { // sorted that way to handle children first
FeedConfig *fc = [self.currentlyDraggedNodes[i - 1] representedObject];
[fc.managedObjectContext refreshObject:fc mergeChanges:YES]; // make sure unreadCount is correct
[fc markUnread:-fc.unreadCount ancestorsOnly:YES];
} }
[self restoreOrderingAndIndexPathStr:destParent];
// decrement sort indices at source
for (NSTreeNode *node in self.currentlyDraggedNodes)
[self incrementIndicesBy:-1 forSubsequentNodes:[node indexPath]];
// increment sort indices at destination
if (!isFolderDrag)
[self incrementIndicesBy:(int)self.currentlyDraggedNodes.count forSubsequentNodes:dest];
// move items
[self.dataStore moveNodes:self.currentlyDraggedNodes toIndexPath:dest];
// set sort indices for dragged items
for (NSUInteger i = 0; i < self.currentlyDraggedNodes.count; i++) {
FeedConfig *fc = [self.currentlyDraggedNodes[i] representedObject];
fc.sortIndex = (int32_t)(updateIndex + i);
[fc markUnread:fc.unreadCount ancestorsOnly:YES];
}
return YES; return YES;
} }
@@ -317,15 +302,15 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)undo:(id)sender { - (void)undo:(id)sender {
[self.undoManager undo]; [self.undoManager undo];
[StoreCoordinator restoreUnreadCount];
[self saveChanges]; [self saveChanges];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
[self.dataStore rearrangeObjects]; // update ordering [self.dataStore rearrangeObjects]; // update ordering
} }
- (void)redo:(id)sender { - (void)redo:(id)sender {
[self.undoManager redo]; [self.undoManager redo];
[StoreCoordinator restoreUnreadCount];
[self saveChanges]; [self saveChanges];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
[self.dataStore rearrangeObjects]; // update ordering [self.dataStore rearrangeObjects]; // update ordering
} }

View File

@@ -60,7 +60,7 @@
- (IBAction)fixCache:(NSButton *)sender { - (IBAction)fixCache:(NSButton *)sender {
[StoreCoordinator deleteUnreferencedFeeds]; [StoreCoordinator deleteUnreferencedFeeds];
[StoreCoordinator restoreUnreadCount]; [StoreCoordinator restoreFeedCountsAndIndexPaths];
} }
- (IBAction)changeMenuBarIconSetting:(NSButton*)sender { - (IBAction)changeMenuBarIconSetting:(NSButton*)sender {

View File

@@ -24,4 +24,5 @@
@interface BarMenu : NSObject <NSMenuDelegate> @interface BarMenu : NSObject <NSMenuDelegate>
- (void)updateBarIcon; - (void)updateBarIcon;
- (void)reloadUnreadCountAndUpdateBarIcon;
@end @end

View File

@@ -27,7 +27,6 @@
#import "Preferences.h" #import "Preferences.h"
#import "UserPrefs.h" #import "UserPrefs.h"
#import "NSMenu+Ext.h" #import "NSMenu+Ext.h"
#import "NSMenuItem+Ext.h"
#import "Feed+Ext.h" #import "Feed+Ext.h"
#import "Constants.h" #import "Constants.h"
@@ -35,9 +34,9 @@
@interface BarMenu() @interface BarMenu()
@property (strong) NSStatusItem *barItem; @property (strong) NSStatusItem *barItem;
@property (strong) Preferences *prefWindow; @property (strong) Preferences *prefWindow;
@property (assign) int unreadCountTotal; @property (assign, atomic) NSInteger unreadCountTotal;
@property (strong) NSArray<FeedConfig*> *allFeeds; @property (weak) NSMenu *currentOpenMenu;
@property (strong) NSArray<NSManagedObjectID*> *currentOpenMenu; @property (strong) NSArray<NSManagedObjectID*> *objectIDsForMenu;
@property (strong) NSManagedObjectContext *readContext; @property (strong) NSManagedObjectContext *readContext;
@end @end
@@ -53,15 +52,13 @@
// Unread counter // Unread counter
self.unreadCountTotal = 0; self.unreadCountTotal = 0;
[self updateBarIcon]; [self updateBarIcon];
dispatch_async(dispatch_get_main_queue(), ^{ [self reloadUnreadCountAndUpdateBarIcon];
self.unreadCountTotal = [StoreCoordinator totalNumberOfUnreadFeeds];
[self updateBarIcon];
});
// Register for notifications // Register for notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadUnreadCountAndUpdateBarIcon) name:kNotificationTotalUnreadCountReset object:nil];
[FeedDownload registerNetworkChangeNotification]; [FeedDownload registerNetworkChangeNotification];
[FeedDownload performSelectorInBackground:@selector(scheduleNextUpdate:) withObject:[NSNumber numberWithBool:NO]]; [FeedDownload performSelectorInBackground:@selector(scheduleNextUpdate:) withObject:[NSNumber numberWithBool:NO]];
return self; return self;
@@ -72,15 +69,23 @@
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];
} }
/** #pragma mark - Update Menu Bar Icon -
Update menu bar icon and text according to unread count and user preferences.
*/ /// Regardless of current unread count, perform new core data fetch on total unread count and update icon.
- (void)reloadUnreadCountAndUpdateBarIcon {
dispatch_async(dispatch_get_main_queue(), ^{
self.unreadCountTotal = [StoreCoordinator unreadCountForIndexPathString:nil];
[self updateBarIcon];
});
}
/// Update menu bar icon and text according to unread count and user preferences.
- (void)updateBarIcon { - (void)updateBarIcon {
// TODO: Option: icon choice // TODO: Option: icon choice
// TODO: Show paused icon if no internet connection // TODO: Show paused icon if no internet connection
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) { if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) {
self.barItem.title = [NSString stringWithFormat:@"%d", self.unreadCountTotal]; self.barItem.title = [NSString stringWithFormat:@"%ld", self.unreadCountTotal];
} else { } else {
self.barItem.title = @""; self.barItem.title = @"";
} }
@@ -119,42 +124,62 @@
[self updateBarIcon]; [self updateBarIcon];
} }
/** /// Callback method fired when feeds have been updated in the background.
Callback method fired when feeds have been updated in the background.
*/
- (void)feedUpdated:(NSNotification*)notify { - (void)feedUpdated:(NSNotification*)notify {
if (self.barItem.menu.numberOfItems > 0) { if (self.barItem.menu.numberOfItems > 0) {
// update items only if menu is already open (e.g., during background update) // update items only if menu is already open (e.g., during background update)
[self.readContext refreshAllObjects]; // because self.allFeeds is the same context NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[self recursiveUpdateMenu:self.barItem.menu withFeed:nil]; for (NSManagedObjectID *oid in notify.object) {
FeedConfig *fc = [moc objectWithID:oid];
NSMenu *menu = [self fixUnreadCountForSubmenus:fc];
if (!menu || menu.numberOfItems > 0)
[self rebuiltFeedItems:fc.feed inMenu:menu]; // deepest menu level, feed items
}
[self.barItem.menu autoEnableMenuHeader:(self.unreadCountTotal > 0)]; // once per multi-feed update
[moc reset];
} }
} }
/** /**
Called recursively for all @c FeedConfig children. Go through all parent menus and reset the menu title and unread count
If the projected submenu in @c menu does not exist, all subsequent children are skipped in @c FeedConfig.
The title and unread count is updated for all menu items. @c FeedItem menus are completely re-generated.
@param config If @c nil the root object (@c self.allFeeds) is used. @param config Should contain a @c Feed object in @c config.feed.
@return @c NSMenu containing @c FeedItem. Will be @c nil if user hasn't open the menu yet.
*/ */
- (void)recursiveUpdateMenu:(NSMenu*)menu withFeed:(FeedConfig*)config { - (nullable NSMenu*)fixUnreadCountForSubmenus:(FeedConfig*)config {
if (config.feed.items.count > 0) { // deepest menu level, feed items NSMenu *menu = self.barItem.menu;
for (FeedConfig *conf in [config allParents]) {
NSInteger offset = [menu feedConfigOffset];
NSMenuItem *item = [menu itemAtIndex:offset + conf.sortIndex];
NSInteger unread = [item setTitleAndUnreadCount:conf];
menu = item.submenu;
if (!menu || menu.numberOfItems == 0)
return nil;
if (unread == 0) // if != 0 then 'setTitleAndUnreadCount' was successful (UserPrefs visible)
unread = [menu coreDataUnreadCount];
[menu autoEnableMenuHeader:(unread > 0)]; // of submenu (including: feed items menu)
}
return menu;
}
/**
Remove all @c NSMenuItem in menu and generate new ones. items from @c feed.items.
@param feed Corresponding @c Feed to @c NSMenu.
@param menu Deepest menu level which contains only feed items.
*/
- (void)rebuiltFeedItems:(Feed*)feed inMenu:(NSMenu*)menu {
if (self.currentOpenMenu != menu) {
// if the menu isn't open, re-create it dynamically instead
menu.itemArray.firstObject.parentItem.submenu = [menu cleanInstanceCopy];
} else {
[menu removeAllItems]; [menu removeAllItems];
[self insertDefaultHeaderForAllMenus:menu scope:ScopeFeed hasUnread:(config.unreadCount > 0)]; [self insertDefaultHeaderForAllMenus:menu hasUnread:(feed.unreadCount > 0)];
for (FeedItem *fi in config.feed.items) { for (FeedItem *fi in [feed sortedArticles]) {
NSMenuItem *mi = [menu addItemWithTitle:@"" action:@selector(openFeedURL:) keyEquivalent:@""]; NSMenuItem *mi = [menu addItemWithTitle:@"" action:@selector(openFeedURL:) keyEquivalent:@""];
mi.target = self; mi.target = self;
[mi setFeedItem:fi]; [mi setFeedItem:fi];
} }
} else {
BOOL hasUnread = (config ? config.unreadCount > 0 : self.unreadCountTotal > 0);
NSInteger offset = [menu getFeedConfigOffsetAndUpdateUnread:hasUnread];
for (FeedConfig *child in (config ? config.children : self.allFeeds)) {
NSMenuItem *item = [menu itemAtIndex:offset + child.sortIndex];
[item setTitleAndUnreadCount:child];
if (item.submenu.numberOfItems > 0)
[self recursiveUpdateMenu:[item submenu] withFeed:child];
}
} }
} }
@@ -162,102 +187,70 @@
#pragma mark - Menu Delegate & Menu Generation - #pragma mark - Menu Delegate & Menu Generation -
// Get rid of everything that is not needed when the system bar menu isnt open. /// @c currentOpenMenu is needed when a background update occurs. In case a feed items menu is open.
- (void)menuDidClose:(NSMenu*)menu { - (void)menuWillOpen:(NSMenu *)menu {
if ([menu isMainMenu]) { self.currentOpenMenu = menu;
self.allFeeds = nil;
[self.readContext reset];
self.readContext = nil;
self.barItem.menu = [NSMenu menuWithDelegate:self];
}
} }
// If main menu load inital set of items, then find item based on index path. /// Get rid of everything that is not needed when the system bar menu is closed.
- (NSInteger)numberOfItemsInMenu:(NSMenu*)menu { - (void)menuDidClose:(NSMenu*)menu {
if ([menu isMainMenu]) { self.currentOpenMenu = nil;
[self.readContext reset]; // will be ignored if nil if ([menu isMainMenu])
self.readContext = [StoreCoordinator createChildContext]; self.barItem.menu = [NSMenu menuWithDelegate:self];
self.allFeeds = [StoreCoordinator sortedFeedConfigItemsInContext:self.readContext];
self.currentOpenMenu = [self.allFeeds valueForKeyPath:@"objectID"];
} else {
FeedConfig *conf = [self configAtIndexPathStr:menu.title];
[self.readContext refreshObject:conf mergeChanges:YES];
self.currentOpenMenu = [(conf.typ == FEED ? conf.feed.items : [conf sortedChildren]) valueForKeyPath:@"objectID"];
}
return (NSInteger)[self.currentOpenMenu count];
} }
/** /**
Find @c FeedConfig item in array @c self.allFeeds that is already loaded. @note Delegate method not used. Here to prevent weird @c NSMenu behavior.
Otherwise, Cmd-Q (Quit) and Cmd-, (Preferences) will traverse all submenus.
@param indexString Path as string that is stored in @c NSMenu title Try yourself with @c NSLog() in @c numberOfItemsInMenu: and @c menuDidClose:
*/ */
- (FeedConfig*)configAtIndexPathStr:(NSString*)indexString { - (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id _Nullable __autoreleasing *)target action:(SEL _Nullable *)action {
NSArray<NSString*> *parts = [indexString componentsSeparatedByString:@"."]; return NO;
NSInteger firstIndex = [[parts objectAtIndex:1] integerValue];
FeedConfig *changing = [self.allFeeds objectAtIndex:(NSUInteger)firstIndex];
for (NSUInteger i = 2; i < parts.count; i++) {
NSInteger childIndex = [[parts objectAtIndex:i] integerValue];
BOOL err = YES;
for (FeedConfig *c in changing.children) {
if (c.sortIndex == childIndex) {
err = NO;
changing = c;
break; // Exit early. Should be faster than sorted children method.
}
}
NSAssert(!err, @"ERROR configAtIndex: Shouldn't happen. Something wrong with indexing.");
}
return changing;
} }
// Lazy populate the system bar menus when needed. /// Perform a core data fatch request, store sorted object ids array and return object count.
- (NSInteger)numberOfItemsInMenu:(NSMenu*)menu {
NSMenuItem *parent = [menu.supermenu itemAtIndex:[menu.supermenu indexOfItemWithSubmenu:menu]];
self.readContext = [StoreCoordinator createChildContext]; // will be deleted after menu:updateItem:
self.objectIDsForMenu = [StoreCoordinator sortedObjectIDsForParent:parent.representedObject isFeed:[menu isFeedMenu] inContext:self.readContext];
return (NSInteger)[self.objectIDsForMenu count];
}
/// Lazy populate system bar menus when needed.
- (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel { - (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel {
NSManagedObjectID *moid = [self.currentOpenMenu objectAtIndex:(NSUInteger)index]; id obj = [self.readContext objectWithID:[self.objectIDsForMenu objectAtIndex:(NSUInteger)index]];
id obj = [self.readContext objectWithID:moid];
[self.readContext refreshObject:obj mergeChanges:YES];
if ([obj isKindOfClass:[FeedConfig class]]) { if ([obj isKindOfClass:[FeedConfig class]]) {
[item setFeedConfig:obj]; [item setFeedConfig:obj];
if ([(FeedConfig*)obj typ] == FEED) { if ([(FeedConfig*)obj typ] == FEED)
item.target = self; [item setTarget:self action:@selector(openFeedURL:)];
item.action = @selector(openFeedURL:);
}
} else if ([obj isKindOfClass:[FeedItem class]]) { } else if ([obj isKindOfClass:[FeedItem class]]) {
[item setFeedItem:obj]; [item setFeedItem:obj];
item.target = self; [item setTarget:self action:@selector(openFeedURL:)];
item.action = @selector(openFeedURL:);
} }
if (menu.numberOfItems == index + 1) {
int unreadCount = self.unreadCountTotal; // if parent == nil if (index + 1 == menu.numberOfItems) { // last item of the menu
if ([obj isKindOfClass:[FeedItem class]]) { [self finalizeMenu:menu object:obj];
unreadCount = [[[(FeedItem*)obj feed] config] unreadCount]; self.objectIDsForMenu = nil;
} else if ([(FeedConfig*)obj parent]) { [self.readContext reset];
unreadCount = [[(FeedConfig*)obj parent] unreadCount]; self.readContext = nil;
}
[self finalizeMenu:menu hasUnread:(unreadCount > 0)];
self.currentOpenMenu = nil;
} }
return YES; return YES;
} }
/** /**
Add default menu items that are present in each menu as header. Add default menu items that are present in each menu as header and disable menu items if necessary
@param flag If @c NO, 'Open all unread' and 'Mark all read' will be disabled.
*/ */
- (void)finalizeMenu:(NSMenu*)menu hasUnread:(BOOL)flag { - (void)finalizeMenu:(NSMenu*)menu object:(id)obj {
BOOL isMainMenu = [menu isMainMenu]; NSInteger unreadCount = self.unreadCountTotal; // if parent == nil
MenuItemTag scope; if ([menu isFeedMenu]) {
if (isMainMenu) scope = ScopeGlobal; unreadCount = [(FeedItem*)obj feed].unreadCount;
else if ([menu isFeedMenu]) scope = ScopeFeed; } else if (![menu isMainMenu]) {
else scope = ScopeGroup; unreadCount = [menu coreDataUnreadCount];
[menu replaceSeparatorStringsWithActualSeparator];
[self insertDefaultHeaderForAllMenus:menu scope:scope hasUnread:flag];
if (isMainMenu) {
[self insertMainMenuHeader:menu];
} }
[menu replaceSeparatorStringsWithActualSeparator];
[self insertDefaultHeaderForAllMenus:menu hasUnread:(unreadCount > 0)];
if ([menu isMainMenu])
[self insertMainMenuHeader:menu];
} }
/** /**
@@ -265,11 +258,15 @@
@param flag If @c NO, 'Open all unread' and 'Mark all read' will be disabled. @param flag If @c NO, 'Open all unread' and 'Mark all read' will be disabled.
*/ */
- (void)insertDefaultHeaderForAllMenus:(NSMenu*)menu scope:(MenuItemTag)scope hasUnread:(BOOL)flag { - (void)insertDefaultHeaderForAllMenus:(NSMenu*)menu hasUnread:(BOOL)flag {
NSMenuItem *item1 = [self itemTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) tag:TagOpenAllUnread | scope]; MenuItemTag scope = [menu scope];
NSMenuItem *item1 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Open all unread", nil)
action:@selector(openAllUnread:) target:self tag:TagOpenAllUnread | scope];
NSMenuItem *item2 = [item1 alternateWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3]]; NSMenuItem *item2 = [item1 alternateWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3]];
NSMenuItem *item3 = [self itemTitle:NSLocalizedString(@"Mark all read", nil) selector:@selector(markAllReadOrUnread:) tag:TagMarkAllRead | scope]; NSMenuItem *item3 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Mark all read", nil)
NSMenuItem *item4 = [self itemTitle:NSLocalizedString(@"Mark all unread", nil) selector:@selector(markAllReadOrUnread:) tag:TagMarkAllUnread | scope]; action:@selector(markAllReadOrUnread:) target:self tag:TagMarkAllRead | scope];
NSMenuItem *item4 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Mark all unread", nil)
action:@selector(markAllReadOrUnread:) target:self tag:TagMarkAllUnread | scope];
item1.enabled = flag; item1.enabled = flag;
item2.enabled = flag; item2.enabled = flag;
item3.enabled = flag; item3.enabled = flag;
@@ -285,8 +282,10 @@
Insert default menu items for the main menu only. Like 'Pause Updates', 'Update all feeds', 'Preferences' and 'Quit'. Insert default menu items for the main menu only. Like 'Pause Updates', 'Update all feeds', 'Preferences' and 'Quit'.
*/ */
- (void)insertMainMenuHeader:(NSMenu*)menu { - (void)insertMainMenuHeader:(NSMenu*)menu {
NSMenuItem *item1 = [self itemTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) tag:TagPauseUpdates]; NSMenuItem *item1 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Pause Updates", nil)
NSMenuItem *item2 = [self itemTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) tag:TagUpdateFeed]; action:@selector(pauseUpdates:) target:self tag:TagPauseUpdates];
NSMenuItem *item2 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Update all feeds", nil)
action:@selector(updateAllFeeds:) target:self tag:TagUpdateFeed];
if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO) if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO)
item2.hidden = YES; item2.hidden = YES;
if (![FeedDownload isNetworkReachable]) if (![FeedDownload isNetworkReachable])
@@ -296,23 +295,13 @@
[menu insertItem:[NSMenuItem separatorItem] atIndex:2]; [menu insertItem:[NSMenuItem separatorItem] atIndex:2];
// < feed content > // < feed content >
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
NSMenuItem *prefs = [self itemTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) tag:TagPreferences]; NSMenuItem *prefs = [NSMenuItem itemWithTitle:NSLocalizedString(@"Preferences", nil)
action:@selector(openPreferences) target:self tag:TagPreferences];
prefs.keyEquivalent = @","; prefs.keyEquivalent = @",";
[menu addItem:prefs]; [menu addItem:prefs];
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"]; [menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
} }
/**
Helper method to generate a new @c NSMenuItem.
*/
- (NSMenuItem*)itemTitle:(NSString*)title selector:(SEL)selector tag:(MenuItemTag)tag {
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""];
item.target = self;
item.tag = tag;
[item applyUserSettingsDisplay];
return item;
}
#pragma mark - Menu Actions - #pragma mark - Menu Actions -
@@ -365,23 +354,19 @@
NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[sender iterateSorted:YES inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) { [sender iterateSorted:YES inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
int itemSum = 0; for (FeedItem *i in [feed sortedArticles]) { // TODO: open oldest articles first?
for (FeedItem *i in feed.items) { if (maxItemCount <= 0) break;
if (itemSum >= maxItemCount) {
break;
}
if (i.unread && i.link.length > 0) { if (i.unread && i.link.length > 0) {
[urls addObject:[NSURL URLWithString:i.link]]; [urls addObject:[NSURL URLWithString:i.link]];
i.unread = NO; i.unread = NO;
++itemSum; feed.unreadCount -= 1;
self.unreadCountTotal -= 1;
maxItemCount -= 1;
} }
} }
if (itemSum > 0) {
[feed.config markUnread:-itemSum ancestorsOnly:NO];
maxItemCount -= itemSum;
}
*cancel = (maxItemCount <= 0); *cancel = (maxItemCount <= 0);
}]; }];
[self updateBarIcon];
[self openURLsWithPreferredBrowser:urls]; [self openURLsWithPreferredBrowser:urls];
[StoreCoordinator saveContext:moc andParent:YES]; [StoreCoordinator saveContext:moc andParent:YES];
[moc reset]; [moc reset];
@@ -394,9 +379,9 @@
BOOL markRead = ((sender.tag & TagMaskType) == TagMarkAllRead); BOOL markRead = ((sender.tag & TagMaskType) == TagMarkAllRead);
NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[sender iterateSorted:NO inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) { [sender iterateSorted:NO inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
if (markRead) [feed markAllItemsRead]; self.unreadCountTotal += (markRead ? [feed markAllItemsRead] : [feed markAllItemsUnread]);
else [feed markAllItemsUnread];
}]; }];
[self updateBarIcon];
[StoreCoordinator saveContext:moc andParent:YES]; [StoreCoordinator saveContext:moc andParent:YES];
[moc reset]; [moc reset];
} }
@@ -416,11 +401,13 @@
if ([obj isKindOfClass:[FeedConfig class]]) { if ([obj isKindOfClass:[FeedConfig class]]) {
url = [[(FeedConfig*)obj feed] link]; url = [[(FeedConfig*)obj feed] link];
} else if ([obj isKindOfClass:[FeedItem class]]) { } else if ([obj isKindOfClass:[FeedItem class]]) {
FeedItem *feed = obj; FeedItem *item = obj;
url = [feed link]; url = [item link];
if (feed.unread) { if (item.unread) {
feed.unread = NO; item.unread = NO;
[feed.feed.config markUnread:-1 ancestorsOnly:NO]; item.feed.unreadCount -= 1;
self.unreadCountTotal -= 1;
[self updateBarIcon];
[StoreCoordinator saveContext:moc andParent:YES]; [StoreCoordinator saveContext:moc andParent:YES];
} }
} }

View File

@@ -21,12 +21,20 @@
// SOFTWARE. // SOFTWARE.
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import "NSMenuItem+Ext.h"
@interface NSMenu (Ext) @interface NSMenu (Ext)
// Generator
+ (instancetype)menuWithDelegate:(id<NSMenuDelegate>)target; + (instancetype)menuWithDelegate:(id<NSMenuDelegate>)target;
- (instancetype)submenuWithIndex:(int)index isFeed:(BOOL)flag; - (instancetype)submenuWithIndex:(int)index isFeed:(BOOL)flag;
- (void)replaceSeparatorStringsWithActualSeparator; - (instancetype)cleanInstanceCopy;
// Properties
- (BOOL)isMainMenu; - (BOOL)isMainMenu;
- (BOOL)isFeedMenu; - (BOOL)isFeedMenu;
- (NSInteger)getFeedConfigOffsetAndUpdateUnread:(BOOL)hasUnread; - (MenuItemTag)scope;
- (NSInteger)feedConfigOffset;
- (NSInteger)coreDataUnreadCount;
// Modify menu
- (void)replaceSeparatorStringsWithActualSeparator;
- (void)autoEnableMenuHeader:(BOOL)hasUnread;
@end @end

View File

@@ -21,10 +21,13 @@
// SOFTWARE. // SOFTWARE.
#import "NSMenu+Ext.h" #import "NSMenu+Ext.h"
#import "NSMenuItem+Ext.h" #import "StoreCoordinator.h"
@implementation NSMenu (Ext) @implementation NSMenu (Ext)
#pragma mark - Generator -
/// @return New main menu with target delegate.
+ (instancetype)menuWithDelegate:(id<NSMenuDelegate>)target { + (instancetype)menuWithDelegate:(id<NSMenuDelegate>)target {
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"M"]; NSMenu *menu = [[NSMenu alloc] initWithTitle:@"M"];
menu.autoenablesItems = NO; menu.autoenablesItems = NO;
@@ -32,18 +35,82 @@
return menu; return menu;
} }
/// @return New menu with old title and delegate. Index path in title is appended.
- (instancetype)submenuWithIndex:(int)index isFeed:(BOOL)flag { - (instancetype)submenuWithIndex:(int)index isFeed:(BOOL)flag {
NSMenu *menu = [NSMenu new]; NSMenu *menu = [NSMenu menuWithDelegate:self.delegate];
menu.title = [NSString stringWithFormat:@"%c%@.%d", (flag ? 'F' : 'G'), self.title, index]; menu.title = [NSString stringWithFormat:@"%c%@.%d", (flag ? 'F' : 'G'), self.title, index];
menu.autoenablesItems = NO;
menu.delegate = self.delegate;
return menu; return menu;
} }
/// @return New menu with old title and delegate.
- (instancetype)cleanInstanceCopy {
NSMenu *menu = [NSMenu menuWithDelegate:self.delegate];
menu.title = self.title;
return menu;
}
#pragma mark - Properties -
/// @return @c YES if menu is status bar menu.
- (BOOL)isMainMenu {
return [self.title isEqualToString:@"M"];
}
/// @return @c YES if menu contains feed articles only.
- (BOOL)isFeedMenu {
return [self.title characterAtIndex:0] == 'F';
}
/// @return Either @c ScopeGlobal, @c ScopeGroup or @c ScopeFeed.
- (MenuItemTag)scope {
if ([self isFeedMenu]) return ScopeFeed;
if ([self isMainMenu]) return ScopeGlobal;
return ScopeGroup;
}
/// @return Index offset of the first Core Data feed item (may be separator), skipping default header and main menu header.
- (NSInteger)feedConfigOffset {
for (NSInteger i = 0; i < self.numberOfItems; i++) {
if ([[[self itemAtIndex:i] representedObject] isKindOfClass:[NSManagedObjectID class]])
return i;
}
return 0;
}
/// Perform Core Data fetch request and return unread count for all descendent items.
- (NSInteger)coreDataUnreadCount {
NSUInteger loc = [self.title rangeOfString:@"."].location;
NSString *path = nil;
if (loc != NSNotFound)
path = [self.title substringFromIndex:loc + 1];
return [StoreCoordinator unreadCountForIndexPathString:path];
}
#pragma mark - Modify Menu -
/// Loop over default header and enable 'OpenAllUnread' and 'TagMarkAllRead' based on unread count.
- (void)autoEnableMenuHeader:(BOOL)hasUnread {
for (NSMenuItem *item in self.itemArray) {
if (item.representedObject)
return; // default menu has no represented object
switch (item.tag & TagMaskType) {
case TagOpenAllUnread: case TagMarkAllRead:
item.enabled = hasUnread;
default: break;
}
//[item applyUserSettingsDisplay]; // should not change while menu is open
}
}
/// Loop over menu and replace all separator items (text) with actual separator.
- (void)replaceSeparatorStringsWithActualSeparator { - (void)replaceSeparatorStringsWithActualSeparator {
for (NSInteger i = 0; i < self.numberOfItems; i++) { for (NSInteger i = 0; i < self.numberOfItems; i++) {
NSMenuItem *oldItem = [self itemAtIndex:i]; NSMenuItem *oldItem = [self itemAtIndex:i];
if ([oldItem.title isEqualToString:@"---SEPARATOR---"]) { if ([oldItem.title isEqualToString:kSeparatorItemTitle]) {
NSMenuItem *newItem = [NSMenuItem separatorItem]; NSMenuItem *newItem = [NSMenuItem separatorItem];
newItem.representedObject = oldItem.representedObject; newItem.representedObject = oldItem.representedObject;
[self removeItemAtIndex:i]; [self removeItemAtIndex:i];
@@ -52,50 +119,4 @@
} }
} }
- (BOOL)isMainMenu {
return [self.title isEqualToString:@"M"];
}
- (BOOL)isFeedMenu {
return [self.title characterAtIndex:0] == 'F';
}
//- (void)iterateMenuItems:(void(^)(NSMenuItem*,BOOL))block atIndexPath:(NSIndexPath*)path {
// NSMenu *m = self;
// for (NSUInteger u = 0; u < path.length; u++) {
// NSUInteger i = [path indexAtPosition:u];
// for (NSMenuItem *item in m.itemArray) {
// if (![item.representedObject isKindOfClass:[NSManagedObjectID class]]) {
// continue; // not a core data item
// }
// if (i == 0) {
// BOOL isFinalItem = (u == path.length - 1);
// block(item, isFinalItem);
// if (isFinalItem) return; // item found!
// m = item.submenu;
// break; // cancel evaluation of remaining items
// }
// i -= 1;
// }
// }
// return; // whenever a menu inbetween is nil (e.g., wasn't set yet)
//}
- (NSInteger)getFeedConfigOffsetAndUpdateUnread:(BOOL)hasUnread {
for (NSInteger i = 0; i < self.numberOfItems; i++) {
NSMenuItem *item = [self itemAtIndex:i];
if ([item.representedObject isKindOfClass:[NSManagedObjectID class]]) {
return i;
} else {
//[item applyUserSettingsDisplay]; // should not change while menu is open
switch (item.tag & TagMaskType) {
case TagOpenAllUnread: case TagMarkAllRead:
item.enabled = hasUnread;
default: break;
}
}
}
return 0;
}
@end @end

View File

@@ -22,6 +22,8 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
static NSString *kSeparatorItemTitle = @"---SEPARATOR---";
/// @c NSMenuItem options that are assigned to the @c tag attribute. /// @c NSMenuItem options that are assigned to the @c tag attribute.
typedef NS_OPTIONS(NSInteger, MenuItemTag) { typedef NS_OPTIONS(NSInteger, MenuItemTag) {
/// Item visible at the very first menu level /// Item visible at the very first menu level
@@ -45,11 +47,13 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
@class FeedConfig, Feed, FeedItem; @class FeedConfig, Feed, FeedItem;
@interface NSMenuItem (Feed) @interface NSMenuItem (Feed)
+ (NSMenuItem*)itemWithTitle:(NSString*)title action:(SEL)selector target:(id)target tag:(MenuItemTag)tag;
- (NSMenuItem*)alternateWithTitle:(NSString*)title; - (NSMenuItem*)alternateWithTitle:(NSString*)title;
- (void)setTarget:(id)target action:(SEL)selector;
- (void)setFeedConfig:(FeedConfig*)config; - (void)setFeedConfig:(FeedConfig*)config;
- (void)setFeedItem:(FeedItem*)item; - (void)setFeedItem:(FeedItem*)item;
- (void)setTitleAndUnreadCount:(FeedConfig*)config; - (NSInteger)setTitleAndUnreadCount:(FeedConfig*)config;
- (void)iterateSorted:(BOOL)ordered inContext:(NSManagedObjectContext*)moc overDescendentFeeds:(void(^)(Feed *feed, BOOL *cancel))block; - (void)iterateSorted:(BOOL)ordered inContext:(NSManagedObjectContext*)moc overDescendentFeeds:(void(^)(Feed *feed, BOOL *cancel))block;
- (void)applyUserSettingsDisplay;
@end @end

View File

@@ -39,6 +39,19 @@ typedef NS_ENUM(char, DisplaySetting) {
@implementation NSMenuItem (Feed) @implementation NSMenuItem (Feed)
#pragma mark - General helper methods -
/**
Helper method to generate a new @c NSMenuItem.
*/
+ (NSMenuItem*)itemWithTitle:(NSString*)title action:(SEL)selector target:(id)target tag:(MenuItemTag)tag {
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""];
item.target = target;
item.tag = tag;
[item applyUserSettingsDisplay];
return item;
}
/** /**
Create a copy of an existing menu item and set it's option key modifier. Create a copy of an existing menu item and set it's option key modifier.
*/ */
@@ -54,17 +67,31 @@ typedef NS_ENUM(char, DisplaySetting) {
} }
/** /**
Set title based on preferences either with or without unread count in parenthesis. Convenient method to set @c target and @c action simultaneously.
*/ */
- (void)setTitleAndUnreadCount:(FeedConfig*)config { - (void)setTarget:(id)target action:(SEL)selector {
if (config.unreadCount > 0 && self.target = target;
((config.typ == FEED && [UserPrefs defaultYES:@"feedUnreadCount"]) || self.action = selector;
(config.typ == GROUP && [UserPrefs defaultYES:@"groupUnreadCount"]))) }
{
self.title = [NSString stringWithFormat:@"%@ (%d)", config.name, config.unreadCount];
} else { #pragma mark - Set properties based on Core Data object -
self.title = config.name;
/**
Set title based on preferences either with or without unread count in parenthesis.
@return Number of unread items. (@b warning: May return @c 0 if visibility is disabled in @c UserPrefs)
*/
- (NSInteger)setTitleAndUnreadCount:(FeedConfig*)config {
NSInteger uCount = 0;
if (config.typ == FEED && [UserPrefs defaultYES:@"feedUnreadCount"]) {
uCount = config.feed.unreadCount;
} else if (config.typ == GROUP && [UserPrefs defaultYES:@"groupUnreadCount"]) {
uCount = [self.submenu coreDataUnreadCount];
} }
self.title = (uCount > 0 ? [NSString stringWithFormat:@"%@ (%ld)", config.name, uCount] : config.name);
return uCount;
} }
/** /**
@@ -73,10 +100,10 @@ typedef NS_ENUM(char, DisplaySetting) {
- (void)setFeedConfig:(FeedConfig*)config { - (void)setFeedConfig:(FeedConfig*)config {
self.representedObject = config.objectID; self.representedObject = config.objectID;
if (config.typ == SEPARATOR) { if (config.typ == SEPARATOR) {
self.title = @"---SEPARATOR---"; self.title = kSeparatorItemTitle;
} else { } else {
[self setTitleAndUnreadCount:config];
self.submenu = [self.menu submenuWithIndex:config.sortIndex isFeed:(config.typ == FEED)]; self.submenu = [self.menu submenuWithIndex:config.sortIndex isFeed:(config.typ == FEED)];
[self setTitleAndUnreadCount:config]; // after submenu is set
if (config.typ == FEED) { if (config.typ == FEED) {
[self configureAsFeed:config]; [self configureAsFeed:config];
} else { } else {
@@ -140,7 +167,7 @@ typedef NS_ENUM(char, DisplaySetting) {
/** /**
@return @c FeedConfig object if @c representedObject contains a valid @c NSManagedObjectID. @return @c FeedConfig object if @c representedObject contains a valid @c NSManagedObjectID.
*/ */
- (FeedConfig*)feedConfig:(NSManagedObjectContext*)moc { - (FeedConfig*)requestConfig:(NSManagedObjectContext*)moc {
if (!self.representedObject || ![self.representedObject isKindOfClass:[NSManagedObjectID class]]) if (!self.representedObject || ![self.representedObject isKindOfClass:[NSManagedObjectID class]])
return nil; return nil;
FeedConfig *config = [moc objectWithID:self.representedObject]; FeedConfig *config = [moc objectWithID:self.representedObject];
@@ -157,10 +184,10 @@ typedef NS_ENUM(char, DisplaySetting) {
*/ */
- (void)iterateSorted:(BOOL)ordered inContext:(NSManagedObjectContext*)moc overDescendentFeeds:(void(^)(Feed*,BOOL*))block { - (void)iterateSorted:(BOOL)ordered inContext:(NSManagedObjectContext*)moc overDescendentFeeds:(void(^)(Feed*,BOOL*))block {
if (self.parentItem) { if (self.parentItem) {
[[self.parentItem feedConfig:moc] iterateSorted:ordered overDescendantFeeds:block]; [[self.parentItem requestConfig:moc] iterateSorted:ordered overDescendantFeeds:block];
} else { } else {
for (NSMenuItem *item in self.menu.itemArray) { for (NSMenuItem *item in self.menu.itemArray) {
FeedConfig *fc = [item feedConfig:moc]; FeedConfig *fc = [item requestConfig:moc];
if (fc != nil) { // All groups and feeds; Ignore default header if (fc != nil) { // All groups and feeds; Ignore default header
if (![fc iterateSorted:ordered overDescendantFeeds:block]) if (![fc iterateSorted:ordered overDescendantFeeds:block])
return; return;

View File

@@ -27,14 +27,16 @@
@class RSParsedFeed; @class RSParsedFeed;
@interface StoreCoordinator : NSObject @interface StoreCoordinator : NSObject
+ (NSManagedObjectContext*)getMainContext; // Managing contexts
+ (NSManagedObjectContext*)createChildContext; + (NSManagedObjectContext*)createChildContext;
+ (void)saveContext:(NSManagedObjectContext*)context andParent:(BOOL)flag; + (void)saveContext:(NSManagedObjectContext*)context andParent:(BOOL)flag;
+ (NSArray<FeedConfig*>*)sortedFeedConfigItemsInContext:(nonnull NSManagedObjectContext*)context; // Feed update
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc; + (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc;
+ (NSDate*)nextScheduledUpdate; + (NSDate*)nextScheduledUpdate;
+ (int)totalNumberOfUnreadFeeds; // Feed display
+ (NSInteger)unreadCountForIndexPathString:(NSString*)str;
+ (NSArray*)sortedObjectIDsForParent:(id)parent isFeed:(BOOL)flag inContext:(NSManagedObjectContext*)moc;
// Restore sound state // Restore sound state
+ (void)deleteUnreferencedFeeds; + (void)deleteUnreferencedFeeds;
+ (void)restoreUnreadCount; + (void)restoreFeedCountsAndIndexPaths;
@end @end

View File

@@ -26,6 +26,8 @@
@implementation StoreCoordinator @implementation StoreCoordinator
#pragma mark - Managing contexts -
+ (NSManagedObjectContext*)getMainContext { + (NSManagedObjectContext*)getMainContext {
return [(AppHook*)NSApp persistentContainer].viewContext; return [(AppHook*)NSApp persistentContainer].viewContext;
} }
@@ -53,20 +55,14 @@
} }
} }
+ (NSArray<FeedConfig*>*)sortedFeedConfigItemsInContext:(NSManagedObjectContext*)context { #pragma mark - Feed Update -
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name];
fr.predicate = [NSPredicate predicateWithFormat:@"parent = NULL"]; // %@", parent
fr.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]];
NSError *err;
NSArray *result = [context executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return result;
}
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc { + (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc {
// TODO: Get Feed instead of FeedConfig
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name]; NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name];
if (!forceAll) { if (!forceAll) {
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d AND scheduled <= %@", FEED, [NSDate date]]; // when fetching also get those feeds that would need update soon (now + 30s)
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d AND scheduled <= %@", FEED, [NSDate dateWithTimeIntervalSinceNow:+30]];
} else { } else {
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d", FEED]; fr.predicate = [NSPredicate predicateWithFormat:@"type = %d", FEED];
} }
@@ -97,7 +93,9 @@
return fetchResults.firstObject[@"earliestDate"]; // can be nil return fetchResults.firstObject[@"earliestDate"]; // can be nil
} }
+ (int)totalNumberOfUnreadFeeds { #pragma mark - Feed Display -
+ (NSInteger)unreadCountForIndexPathString:(NSString*)str {
// Always get context first, or 'FeedConfig.entity.name' may not be available on app start // Always get context first, or 'FeedConfig.entity.name' may not be available on app start
NSManagedObjectContext *moc = [self getMainContext]; NSManagedObjectContext *moc = [self getMainContext];
NSExpression *exp = [NSExpression expressionForFunction:@"sum:" NSExpression *exp = [NSExpression expressionForFunction:@"sum:"
@@ -107,15 +105,29 @@
[expDesc setExpression:exp]; [expDesc setExpression:exp];
[expDesc setExpressionResultType:NSInteger32AttributeType]; [expDesc setExpressionResultType:NSInteger32AttributeType];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name]; NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: Feed.entity.name];
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d", FEED]; if (str && str.length > 0)
fr.predicate = [NSPredicate predicateWithFormat:@"indexPath BEGINSWITH %@", str];
[fr setResultType:NSDictionaryResultType]; [fr setResultType:NSDictionaryResultType];
[fr setPropertiesToFetch:@[expDesc]]; [fr setPropertiesToFetch:@[expDesc]];
NSError *err; NSError *err;
NSArray *fetchResults = [moc executeFetchRequest:fr error:&err]; NSArray *fetchResults = [moc executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err); if (err) NSLog(@"%@", err);
return [fetchResults.firstObject[@"totalUnread"] intValue]; return [fetchResults.firstObject[@"totalUnread"] integerValue];
}
+ (NSArray*)sortedObjectIDsForParent:(id)parent isFeed:(BOOL)flag inContext:(NSManagedObjectContext*)moc {
// NSManagedObjectContext *moc = [self getMainContext];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: (flag ? FeedItem.entity : FeedConfig.entity).name];
fr.predicate = [NSPredicate predicateWithFormat:(flag ? @"feed.config = %@" : @"parent = %@"), parent];
fr.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:!flag]];
[fr setResultType:NSManagedObjectIDResultType];
NSError *err;
NSArray *fetchResults = [moc executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return fetchResults;
} }
//+ (void)addToSortIndex:(int)num start:(int)index parent:(FeedConfig*)config inContext:(NSManagedObjectContext*)moc { //+ (void)addToSortIndex:(int)num start:(int)index parent:(FeedConfig*)config inContext:(NSManagedObjectContext*)moc {
@@ -142,27 +154,23 @@
if (err) NSLog(@"%@", err); if (err) NSLog(@"%@", err);
} }
+ (void)restoreUnreadCount { + (void)restoreFeedCountsAndIndexPaths {
NSManagedObjectContext *moc = [self getMainContext]; NSManagedObjectContext *moc = [self getMainContext];
NSError *err; NSError *err;
NSArray *confs = [moc executeFetchRequest:[NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name] error:&err]; NSArray *result = [moc executeFetchRequest:[NSFetchRequest fetchRequestWithEntityName: Feed.entity.name] error:&err];
if (err) NSLog(@"%@", err);
NSArray *feeds = [moc executeFetchRequest:[NSFetchRequest fetchRequestWithEntityName: Feed.entity.name] error:&err];
if (err) NSLog(@"%@", err); if (err) NSLog(@"%@", err);
[moc performBlock:^{ [moc performBlock:^{
for (FeedConfig *conf in confs) { for (Feed *feed in result) {
conf.unreadCount = 0; int16_t totalCount = (int16_t)feed.items.count;
} int16_t unreadCount = (int16_t)[[feed.items valueForKeyPath:@"@sum.unread"] integerValue];
for (Feed *feed in feeds) { if (feed.articleCount != totalCount)
int count = 0; feed.articleCount = totalCount;
for (FeedItem *item in feed.items) { if (feed.unreadCount != unreadCount)
if (item.unread) ++count; feed.unreadCount = unreadCount; // remember to update global total unread count
}
FeedConfig *parent = feed.config; NSString *pathStr = [feed.config indexPathString];
while (parent) { if (![feed.indexPath isEqualToString:pathStr])
parent.unreadCount += count; feed.indexPath = pathStr;
parent = parent.parent;
}
} }
}]; }];
} }