Refactoring Part 1: Dynamic menus (stable)

This commit is contained in:
relikd
2018-11-24 14:18:06 +01:00
parent 080991ebc4
commit 6223d1a169
22 changed files with 1026 additions and 973 deletions

View File

@@ -7,9 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
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 */; };
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
543695D5214EFD9800DA979D /* NSMenuItem+Info.m in Sources */ = {isa = PBXBuildFile; fileRef = 543695D4214EFD9800DA979D /* NSMenuItem+Info.m */; };
543695D8214F1F2700DA979D /* NSMenuItem+Generate.m in Sources */ = {isa = PBXBuildFile; fileRef = 543695D7214F1F2700DA979D /* NSMenuItem+Generate.m */; };
543695D8214F1F2700DA979D /* NSMenuItem+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 543695D7214F1F2700DA979D /* NSMenuItem+Ext.m */; };
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
544DCCB9212A2B4D002DBC46 /* RSXML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 544DCCB8212A2B4D002DBC46 /* RSXML.framework */; };
@@ -71,12 +72,15 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
54195881218A061100581B79 /* Feed+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Feed+Ext.h"; sourceTree = "<group>"; };
54195882218A061100581B79 /* Feed+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Feed+Ext.m"; sourceTree = "<group>"; };
54195884218E1BDB00581B79 /* NSMenu+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenu+Ext.h"; sourceTree = "<group>"; };
54195885218E1BDB00581B79 /* NSMenu+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenu+Ext.m"; sourceTree = "<group>"; };
541958872190FF1200581B79 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = "<group>"; };
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
543695D3214EFD9800DA979D /* NSMenuItem+Info.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Info.h"; sourceTree = "<group>"; };
543695D4214EFD9800DA979D /* NSMenuItem+Info.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Info.m"; sourceTree = "<group>"; };
543695D6214F1F2700DA979D /* NSMenuItem+Generate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Generate.h"; sourceTree = "<group>"; };
543695D7214F1F2700DA979D /* NSMenuItem+Generate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Generate.m"; sourceTree = "<group>"; };
543695D6214F1F2700DA979D /* NSMenuItem+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Ext.h"; sourceTree = "<group>"; };
543695D7214F1F2700DA979D /* NSMenuItem+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Ext.m"; sourceTree = "<group>"; };
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
@@ -136,13 +140,24 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
54195880218A05E700581B79 /* Categories */ = {
isa = PBXGroup;
children = (
5477D34C21233C62002BA27F /* FeedConfig+Ext.h */,
5477D34D21233C62002BA27F /* FeedConfig+Ext.m */,
54195881218A061100581B79 /* Feed+Ext.h */,
54195882218A061100581B79 /* Feed+Ext.m */,
);
path = Categories;
sourceTree = "<group>";
};
541A90EF21257D4F002680A6 /* Status Bar Menu */ = {
isa = PBXGroup;
children = (
543695D3214EFD9800DA979D /* NSMenuItem+Info.h */,
543695D4214EFD9800DA979D /* NSMenuItem+Info.m */,
543695D6214F1F2700DA979D /* NSMenuItem+Generate.h */,
543695D7214F1F2700DA979D /* NSMenuItem+Generate.m */,
543695D6214F1F2700DA979D /* NSMenuItem+Ext.h */,
543695D7214F1F2700DA979D /* NSMenuItem+Ext.m */,
54195884218E1BDB00581B79 /* NSMenu+Ext.h */,
54195885218E1BDB00581B79 /* NSMenu+Ext.m */,
54FE73D1212316CD003EAC65 /* BarMenu.h */,
54FE73D2212316CD003EAC65 /* BarMenu.m */,
);
@@ -174,8 +189,6 @@
546FC44D2118B357007CC3A3 /* Preferences */ = {
isa = PBXGroup;
children = (
5477D34C21233C62002BA27F /* FeedConfig+Ext.h */,
5477D34D21233C62002BA27F /* FeedConfig+Ext.m */,
54ACC29621061FBA0020715F /* Preferences.h */,
54ACC29721061FBA0020715F /* Preferences.m */,
546FC4462118A8E6007CC3A3 /* Preferences.xib */,
@@ -211,6 +224,7 @@
children = (
544B011B2114EE9100386E5C /* AppHook.h */,
544B011C2114EE9100386E5C /* AppHook.m */,
541958872190FF1200581B79 /* Constants.h */,
541A90EF21257D4F002680A6 /* Status Bar Menu */,
54FE73CE21220DEC003EAC65 /* StoreCoordinator.h */,
54FE73CF21220DEC003EAC65 /* StoreCoordinator.m */,
@@ -218,6 +232,7 @@
54ACC29421061E270020715F /* FeedDownload.m */,
54209E922117325100F3B5EF /* DrawImage.h */,
54209E932117325100F3B5EF /* DrawImage.m */,
54195880218A05E700581B79 /* Categories */,
546FC44D2118B357007CC3A3 /* Preferences */,
54ACC28521061B3C0020715F /* Assets.xcassets */,
54ACC28A21061B3C0020715F /* Info.plist */,
@@ -366,7 +381,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
543695D8214F1F2700DA979D /* NSMenuItem+Generate.m in Sources */,
543695D8214F1F2700DA979D /* NSMenuItem+Ext.m in Sources */,
54F39C2E210BE1F700AEE730 /* DBv1.xcdatamodeld in Sources */,
544B011D2114EE9100386E5C /* AppHook.m in Sources */,
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */,
@@ -375,11 +390,12 @@
54ACC28C21061B3C0020715F /* main.m in Sources */,
54FE73D3212316CD003EAC65 /* BarMenu.m in Sources */,
544B011A2114B41200386E5C /* ModalSheet.m in Sources */,
543695D5214EFD9800DA979D /* NSMenuItem+Info.m in Sources */,
54ACC29821061FBA0020715F /* Preferences.m in Sources */,
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */,
5496B511214D6275003ED4ED /* UserPrefs.m in Sources */,
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
54195883218A061100581B79 /* Feed+Ext.m in Sources */,
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
);

View File

@@ -0,0 +1,32 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "Feed+CoreDataClass.h"
@class RSParsedFeed;
@interface Feed (Ext)
+ (Feed*)feedFromRSS:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls unread:(int*)unreadCount;
- (NSArray<NSString*>*)alreadyReadURLs;
- (void)markAllItemsRead;
- (void)markAllItemsUnread;
@end

View File

@@ -0,0 +1,89 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "Feed+Ext.h"
#import "FeedConfig+Ext.h"
#import "FeedItem+CoreDataClass.h"
#import <RSXML/RSXML.h>
@implementation Feed (Ext)
+ (FeedItem*)createFeedItemFrom:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)context {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context];
b.guid = entry.guid;
b.title = entry.title;
b.abstract = entry.abstract;
b.body = entry.body;
b.author = entry.author;
b.link = entry.link;
b.published = entry.datePublished;
return b;
}
+ (Feed*)feedFromRSS:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls unread:(int*)unreadCount {
Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context];
a.title = obj.title;
a.subtitle = obj.subtitle;
a.link = obj.link;
for (RSParsedArticle *article in obj.articles) {
FeedItem *b = [self createFeedItemFrom:article inContext:context];
if ([urls containsObject:b.link]) {
b.unread = NO;
} else {
*unreadCount += 1;
}
[a addItemsObject:b];
}
return a;
}
- (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];
}
- (void)markAllItemsUnread {
[self markAllArticlesRead:NO];
}
- (void)markAllArticlesRead:(BOOL)readFlag {
int count = 0;
for (FeedItem *i in self.items) {
if (i.unread == readFlag) {
i.unread = !readFlag;
++count;
}
}
[self.config markUnread:(readFlag ? -count : +count) ancestorsOnly:NO];
}
@end

View File

@@ -22,7 +22,7 @@
#import "FeedConfig+CoreDataClass.h"
@class FeedItem;
@class FeedItem, RSParsedFeed;
@interface FeedConfig (Ext)
/// Enum type to distinguish different @c FeedConfig types
@@ -31,22 +31,18 @@ typedef enum int16_t {
FEED = 1,
SEPARATOR = 2
} FeedConfigType;
/**
Iteration block for descendants of @c FeedItem.
@param parent The parent @c FeedConfig where this @c FeedItem belongs to.
@param item Currently processed @c FeedItem.
@return Return @c YES to continue processing. Return @c NO to stop processing and exit early.
*/
typedef BOOL (^FeedConfigRecursiveItemsBlock) (FeedConfig *parent, FeedItem *item);
@property (getter=typ, setter=setTyp:) FeedConfigType typ;
@property (readonly) NSArray<FeedConfig*> *sortedChildren;
@property (readonly) NSIndexPath *indexPath;
- (BOOL)descendantFeedItems:(FeedConfigRecursiveItemsBlock)block;
- (NSArray<FeedConfig*>*)sortedChildren;
- (NSIndexPath*)indexPath;
- (void)markUnread:(int)count ancestorsOnly:(BOOL)flag;
- (void)calculateAndSetScheduled;
- (void)mergeChangesAndSave;
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed*,BOOL*))block;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (void)updateRSSFeed:(RSParsedFeed*)obj;
- (NSString*)readableRefreshString;
- (NSString*)readableDescription;
@end

View File

@@ -21,7 +21,9 @@
// SOFTWARE.
#import "FeedConfig+Ext.h"
#import "Feed+CoreDataClass.h"
#import "Feed+Ext.h"
#import "FeedMeta+CoreDataClass.h"
#import "Constants.h"
@implementation FeedConfig (Ext)
/// Enum tpye getter see @c FeedConfigType
@@ -40,31 +42,28 @@
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
}
/// IndexPath for sorted children starting with root index.
- (NSIndexPath*)indexPath {
if (self.parent == nil)
return [NSIndexPath indexPathWithIndex:(NSUInteger)self.sortIndex];
return [self.parent.indexPath indexPathByAddingIndex:(NSUInteger)self.sortIndex];
return [[self.parent indexPath] indexPathByAddingIndex:(NSUInteger)self.sortIndex];
}
/**
Iterate over all descendant @c FeedItems in sub groups
Change unread counter for all parents recursively. Result will never be negative.
@param block Will yield the current parent config and feed item. Return @c NO to cancel iteration.
@return Returns @c NO if the iteration was canceled early. Otherwise @c YES.
@param count If negative, mark items read.
*/
- (BOOL)descendantFeedItems:(FeedConfigRecursiveItemsBlock)block {
if (self.children.count > 0) {
for (FeedConfig *config in self.sortedChildren) {
if ([config descendantFeedItems:block] == NO)
return NO;
- (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;
}
} else if (self.feed.items.count > 0) {
for (FeedItem* item in self.feed.items) {
if (block(self, item) == NO)
return NO;
}
}
return YES;
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged
object:[NSNumber numberWithInt:count]];
}
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
@@ -78,13 +77,45 @@
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
}
/// Update item with @c mergeChanges:YES and save the context
- (void)mergeChangesAndSave {
[self.managedObjectContext performBlockAndWait:^{
[self.managedObjectContext refreshObject:self mergeChanges:YES];
[self.managedObjectContext save:nil];
}];
/// 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 {
if (self.feed) {
BOOL stopEarly = NO;
block(self.feed, &stopEarly);
if (stopEarly) return NO;
} else {
for (FeedConfig *fc in (ordered ? [self sortedChildren] : self.children)) {
if (![fc iterateSorted:ordered overDescendantFeeds:block])
return NO;
}
}
return YES;
}
#pragma mark - Printing -
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (NSString*)readableRefreshString {

30
baRSS/Constants.h Normal file
View File

@@ -0,0 +1,30 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#ifndef Constants_h
#define Constants_h
static NSString *kNotificationFeedUpdated = @"baRSS-notification-feed-updated";
static NSString *kNotificationNetworkStatusChanged = @"baRSS-notification-network-status-changed";
static NSString *kNotificationTotalUnreadCountChanged = @"baRSS-notification-total-unread-count-changed";
#endif /* Constants_h */

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G65" 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">
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
@@ -15,6 +15,7 @@
<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="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"/>
<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"/>
@@ -40,7 +41,7 @@
</entity>
<elements>
<element name="Feed" positionX="-229.09375" positionY="-2.30859375" width="128" height="120"/>
<element name="FeedConfig" positionX="-441.87109375" positionY="-47.390625" width="128" height="225"/>
<element name="FeedConfig" positionX="-441.87109375" positionY="-47.390625" width="128" height="240"/>
<element name="FeedItem" positionX="-28.140625" positionY="-17.359375" width="128" height="180"/>
<element name="FeedMeta" positionX="-234" positionY="72" width="128" height="105"/>
</elements>

View File

@@ -21,6 +21,7 @@
// SOFTWARE.
#import "FeedDownload.h"
#import "Constants.h"
#import "StoreCoordinator.h"
#import <SystemConfiguration/SystemConfiguration.h>
@@ -95,21 +96,25 @@ static BOOL _isReachable = NO;
BOOL forceAll = [timer.userInfo boolValue];
// TODO: check internet connection
// TODO: disable menu item 'update all' during update
NSArray<FeedConfig*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:forceAll];
__block NSManagedObjectContext *childContext = [StoreCoordinator createChildContext];
NSArray<FeedConfig*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:forceAll inContext:childContext];
if (list.count == 0) {
NSLog(@"ERROR: Something went wrong, timer fired too early.");
[childContext reset];
childContext = nil;
// thechnically should never happen, anyway we need to reset the timer
[self scheduleNextUpdate:NO]; // NO, since forceAll will get ALL items and shouldn't be 0
return; // nothing to do here
}
NSUndoManager *um = list.firstObject.managedObjectContext.undoManager;
[um beginUndoGrouping];
dispatch_group_t group = dispatch_group_create();
for (FeedConfig *c in list) {
[self downloadFeedForConfig:c group:group];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[um endUndoGrouping];
[StoreCoordinator saveContext:childContext andParent:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:nil];
[childContext reset];
childContext = nil;
[self scheduleNextUpdate:NO]; // after forced update, continue regular cycle
});
}
@@ -118,7 +123,8 @@ static BOOL _isReachable = NO;
if (!_isReachable) return;
dispatch_group_enter(group);
[[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequest:config] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[config.managedObjectContext.undoManager beginUndoGrouping];
[config.managedObjectContext performBlock:^{
// core data block inside of url session block; otherwise config access will EXC_BAD_INSTRUCTION
if (error) {
int16_t n = config.errorCount + 1;
config.errorCount = (n < 1 ? 1 : (n > 19 ? 19 : n)); // between: 2 sec and 6 days
@@ -130,8 +136,8 @@ static BOOL _isReachable = NO;
config.errorCount = 0; // reset counter
[self downloadSuccessful:data forFeed:config response:(NSHTTPURLResponse*)response];
}
[config.managedObjectContext.undoManager endUndoGrouping];
dispatch_group_leave(group);
}];
}] resume];
}
@@ -143,15 +149,16 @@ static BOOL _isReachable = NO;
if (parsed) {
// TODO: add support for media player?
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
[StoreCoordinator overwriteConfig:config withFeed:parsed];
[config updateRSSFeed:parsed];
}
}
config.meta.httpModified = [http allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
config.meta.httpEtag = [http allHeaderFields][@"Etag"];
[config setEtag:[http allHeaderFields][@"Etag"] modified:[http allHeaderFields][@"Date"]]; // @"Expires", @"Last-Modified"
// Don't update redirected url since it happened in the background; User may not recognize url
[config calculateAndSetScheduled];
[config mergeChangesAndSave];
[[NSNotificationCenter defaultCenter] postNotificationName:@"baRSS-notification-feed-updated" object:config];
// [config mergeChangesAndSave];
// [config.managedObjectContext performBlock:^{
// [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:config.objectID];
// }];
}
@@ -190,7 +197,7 @@ static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetwo
if (_reachability == NULL)
return;
_isReachable = [FeedDownload hasConnectivity:flags];
[[NSNotificationCenter defaultCenter] postNotificationName:@"baRSS-notification-network-status-change"
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationNetworkStatusChanged
object:[NSNumber numberWithBool:_isReachable]];
if (_isReachable) {
NSLog(@"reachable");

View File

@@ -109,22 +109,13 @@
item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem;
if (self.shouldDeletePrevArticles) {
[StoreCoordinator overwriteConfig:item withFeed:self.feedResult];
[item.managedObjectContext performBlockAndWait:^{
// TODO: move to separate function and add icon download
if (!item.meta) {
item.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:item.managedObjectContext];
}
item.meta.httpEtag = self.httpEtag;
item.meta.httpModified = self.httpDate;
}];
[item updateRSSFeed:self.feedResult];
[item setEtag:self.httpEtag modified:self.httpDate];
}
if ([item.managedObjectContext hasChanges]) {
self.objectIsModified = YES;
[item calculateAndSetScheduled];
[item.managedObjectContext performBlockAndWait:^{
[item.managedObjectContext refreshObject:item mergeChanges:YES];
}];
}
}
@@ -222,9 +213,7 @@
NSString *name = ((NSTextField*)self.view).stringValue;
if (![item.name isEqualToString: name]) {
item.name = name;
[item.managedObjectContext performBlockAndWait:^{
[item.managedObjectContext refreshObject:item mergeChanges:YES];
}];
[self.delegate modalDidUpdateFeedConfig:item];
}
}

View File

@@ -21,7 +21,6 @@
// SOFTWARE.
#import "SettingsFeeds.h"
#import "AppHook.h"
#import "BarMenu.h"
#import "ModalSheet.h"
#import "ModalFeedEdit.h"
@@ -47,27 +46,16 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self.outlineView registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]];
[self.dataStore setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[childContext setParentContext:[(AppHook*)NSApp persistentContainer].viewContext];
// childContext.automaticallyMergesChangesFromParent = YES;
NSUndoManager *um = [[NSUndoManager alloc] init];
um.groupsByEvent = NO;
um.levelsOfUndo = 30;
childContext.undoManager = um;
self.undoManager = [[NSUndoManager alloc] init];
self.undoManager.groupsByEvent = NO;
self.undoManager.levelsOfUndo = 30;
self.dataStore.managedObjectContext = childContext;
self.undoManager = self.dataStore.managedObjectContext.undoManager;
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
self.dataStore.managedObjectContext.undoManager = self.undoManager;
}
- (void)saveAndRebuildMenu {
[self.dataStore.managedObjectContext performBlock:^{
[StoreCoordinator saveContext:self.dataStore.managedObjectContext];
[[(AppHook*)NSApp barMenu] rebuildMenu]; // updating individual items was way to complicated ...
[self.dataStore.managedObjectContext.parentContext performBlock:^{
[StoreCoordinator saveContext:self.dataStore.managedObjectContext.parentContext];
}];
}];
- (void)saveChanges {
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
}
- (IBAction)addFeed:(id)sender {
@@ -84,7 +72,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
sp.name = @"---";
sp.typ = SEPARATOR;
[self.undoManager endUndoGrouping];
[self saveAndRebuildMenu];
[self saveChanges];
}
- (IBAction)remove:(id)sender {
@@ -93,7 +81,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self incrementIndicesBy:-1 forSubsequentNodes:path];
[self.dataStore remove:sender];
[self.undoManager endUndoGrouping];
[self saveAndRebuildMenu];
[self saveChanges];
}
- (IBAction)doubleClickOutlineView:(NSOutlineView*)sender {
@@ -139,7 +127,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
}
- (void)modalDidUpdateFeedConfig:(FeedConfig*)config {
[self saveAndRebuildMenu];
[self saveChanges]; // TODO: adjust total count
}
- (FeedConfig*)insertSortedItemAtSelection {
@@ -178,7 +166,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
root = [root descendantNodeAtIndexPath:parentPath];
for (NSUInteger i = [path indexAtPosition:path.length - 1]; i < root.childNodes.count; i++) {
((FeedConfig*)[root.childNodes[i] representedObject]).sortIndex += val;
FeedConfig *conf = [root.childNodes[i] representedObject];
conf.sortIndex += val;
}
}
@@ -197,7 +186,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)outlineView:(NSOutlineView *)outlineView draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation {
[self.undoManager endUndoGrouping];
if (self.dataStore.managedObjectContext.hasChanges) {
[self saveAndRebuildMenu];
[self saveChanges];
} else {
[self.undoManager disableUndoRegistration];
[self.undoManager undoNestedGroup];
@@ -228,6 +217,11 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
--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];
}
// decrement sort indices at source
for (NSTreeNode *node in self.currentlyDraggedNodes)
@@ -243,6 +237,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
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;
}
@@ -322,14 +317,16 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)undo:(id)sender {
[self.undoManager undo];
[StoreCoordinator restoreUnreadCount];
[self saveChanges];
[self.dataStore rearrangeObjects]; // update ordering
[self saveAndRebuildMenu];
}
- (void)redo:(id)sender {
[self.undoManager redo];
[StoreCoordinator restoreUnreadCount];
[self saveChanges];
[self.dataStore rearrangeObjects]; // update ordering
[self saveAndRebuildMenu];
}
- (void)enterPressed:(id)sender {

View File

@@ -24,6 +24,7 @@
#import "AppHook.h"
#import "BarMenu.h"
#import "UserPrefs.h"
#import "StoreCoordinator.h"
#import <ServiceManagement/ServiceManagement.h>
@@ -57,6 +58,15 @@
CFRelease(helperIdentifier);
}
- (IBAction)fixCache:(NSButton *)sender {
[StoreCoordinator deleteUnreferencedFeeds];
[StoreCoordinator restoreUnreadCount];
}
- (IBAction)changeMenuBarIconSetting:(NSButton*)sender {
[[(AppHook*)NSApp barMenu] updateBarIcon];
}
- (IBAction)changeHttpApplication:(NSPopUpButton *)sender {
[UserPrefs setHttpApplication:sender.selectedItem.representedObject];
}
@@ -68,31 +78,6 @@
}
}
// TODO: add self to login items
- (IBAction)checkmarkClicked:(NSButton*)sender {
// TODO: Could be optimized by updating only the relevant parts
[[(AppHook*)NSApp barMenu] rebuildMenu];
}
- (IBAction)changeMenuBarIconSetting:(NSButton*)sender {
[[(AppHook*)NSApp barMenu] updateBarIcon];
}
- (IBAction)changeMenuHeaderSetting:(NSButton*)sender {
BOOL recursive = YES;
NSString *bindingKey = [[sender infoForBinding:@"value"] valueForKey:NSObservedKeyPathKey];
if ([bindingKey containsString:@"values.global"]) {
recursive = NO; // item is in menu bar menu, no need to go recursive
}
[[(AppHook*)NSApp barMenu] updateMenuHeaders:recursive];
}
- (IBAction)changeMenuItemUpdateAll:(NSButton*)sender {
BOOL checked = (sender.state == NSControlStateValueOn);
[[(AppHook*)NSApp barMenu] setItemUpdateAllHidden:!checked];
}
#pragma mark - Helper methods
/**

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -45,7 +45,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuItemUpdateAll:" target="-2" id="Zb8-Oi-JVr"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalUpdateAll" id="FrQ-u0-lFo">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -62,7 +61,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="Tte-Vw-oMq"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalOpenUnread" id="c20-0p-cPb">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -79,7 +77,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="zRA-Ht-Qj1"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupOpenUnread" id="mCn-aE-DwT">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -96,7 +93,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="4sR-3H-A6H"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedOpenUnread" id="Qyh-BN-P74">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -113,7 +109,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="gcu-x5-gUa"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalMarkRead" id="uiO-3M-xfT">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -130,7 +125,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="rTt-3J-rkn"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupMarkRead" id="YLZ-t8-Jbk">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -147,7 +141,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="2cM-mG-Lnw"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedMarkRead" id="mYj-26-0OV">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -164,7 +157,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="anc-id-9sf"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalMarkUnread" id="drp-87-kfY">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -181,7 +173,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="98j-A6-A2m"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupMarkUnread" id="bJP-0I-l7t">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -198,7 +189,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuHeaderSetting:" target="-2" id="Muv-3Y-LU0"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedMarkUnread" id="mRu-7M-3bu">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -232,7 +222,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="checkmarkClicked:" target="-2" id="PUq-gk-16h"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupUnreadCount" id="Mg5-xJ-L3n">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -249,7 +238,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="checkmarkClicked:" target="-2" id="dfY-Sm-GHz"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedUnreadCount" id="hnm-Q2-kbs">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -266,7 +254,6 @@
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="checkmarkClicked:" target="-2" id="hzW-x5-kBO"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedTickMark" id="xKL-Lh-tBL">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
@@ -446,6 +433,17 @@
<action selector="changeDefaultRSSReader:" target="-2" id="ul1-1K-oJb"/>
</connections>
</popUpButton>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QwE-M7-q2R">
<rect key="frame" x="206" y="279" width="100" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Fix Cache" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ady-2s-Ggm">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="fixCache:" target="-2" id="gbM-hA-UVF"/>
</connections>
</button>
</subviews>
<point key="canvasLocation" x="140" y="-155.5"/>
</customView>

View File

@@ -23,8 +23,5 @@
#import <Cocoa/Cocoa.h>
@interface BarMenu : NSObject <NSMenuDelegate>
- (void)rebuildMenu;
- (void)updateBarIcon;
- (void)updateMenuHeaders:(BOOL)recursive;
- (void)setItemUpdateAllHidden:(BOOL)hidden;
@end

View File

@@ -25,16 +25,20 @@
#import "FeedDownload.h"
#import "DrawImage.h"
#import "Preferences.h"
#import "NSMenuItem+Info.h"
#import "NSMenuItem+Generate.h"
#import "UserPrefs.h"
#import "NSMenu+Ext.h"
#import "NSMenuItem+Ext.h"
#import "Feed+Ext.h"
#import "Constants.h"
@interface BarMenu()
@property (strong) NSStatusItem *barItem;
@property (strong) Preferences *prefWindow;
@property (weak) NSMenu *mm;
@property (assign) int unreadCountTotal;
@property (strong) NSArray<FeedConfig*> *allFeeds;
@property (strong) NSArray<NSManagedObjectID*> *currentOpenMenu;
@property (strong) NSManagedObjectContext *readContext;
@end
@@ -44,9 +48,20 @@
self = [super init];
self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
self.barItem.highlightMode = YES;
[self rebuildMenu];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChange:) name:@"baRSS-notification-network-status-change" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:@"baRSS-notification-feed-updated" object:nil];
self.barItem.menu = [NSMenu menuWithDelegate:self];
// Unread counter
self.unreadCountTotal = 0;
[self updateBarIcon];
dispatch_async(dispatch_get_main_queue(), ^{
self.unreadCountTotal = [StoreCoordinator totalNumberOfUnreadFeeds];
[self updateBarIcon];
});
// Register for notifications
[[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(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil];
[FeedDownload registerNetworkChangeNotification];
[FeedDownload performSelectorInBackground:@selector(scheduleNextUpdate:) withObject:[NSNumber numberWithBool:NO]];
return self;
@@ -57,46 +72,6 @@
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)networkChange:(NSNotification*)notify {
BOOL available = [[notify object] boolValue];
[self.barItem.menu itemWithTag:TagUpdateFeed].enabled = available;
[self updateBarIcon];
// TODO: Disable 'update all' menu item?
}
- (void)feedUpdated:(NSNotification*)notify {
FeedConfig *config = notify.object;
NSLog(@"%@", config.indexPath);
[self rebuildMenu];
}
- (void)rebuildMenu {
self.barItem.menu = [self generateMainMenu];
[self updateBarIcon];
}
- (void)donothing {
dispatch_async(dispatch_get_main_queue(), ^{
[self.mm itemAtIndex:4].title = [NSString stringWithFormat:@"%@", [NSDate date]];
});
sleep(1);
[self performSelectorInBackground:@selector(donothing) withObject:nil];
}
// TODO: remove debugging stuff
- (void)printUnreadRecurisve:(NSMenu*)menu str:(NSString*)prefix {
for (NSMenuItem *item in menu.itemArray) {
if (![item hasReaderInfo]) continue;
id obj = [item requestCoreDataObject];
if ([obj isKindOfClass:[FeedItem class]] && ([obj unread] > 0 || item.unreadCount > 0))
NSLog(@"%@ %@ (%d == %d)", prefix, item.title, item.unreadCount, [obj unread]);
else if ([item hasUnread])
NSLog(@"%@ %@ (%d)", prefix, item.title, item.unreadCount);
if (item.hasSubmenu) {
[self printUnreadRecurisve:item.submenu str:[NSString stringWithFormat:@" %@", prefix]];
}
}
}
/**
Update menu bar icon and text according to unread count and user preferences.
*/
@@ -117,170 +92,229 @@
self.barItem.image.template = YES;
}
});
// NSLog(@"==> %d", self.unreadCountTotal);
// [self printUnreadRecurisve:self.barItem.menu str:@""];
}
#pragma mark - Menu Generator
#pragma mark - Notification callback methods -
/**
Builds main menu with items on the very first menu level. Including Preferences, Quit, etc.
Callback method fired when network conditions change.
@param notify Notification object contains a @c BOOL value indicating the current status.
*/
- (NSMenu*)generateMainMenu {
NSMenu *menu = [NSMenu new];
menu.autoenablesItems = NO;
[self addTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) toMenu:menu tag:TagPauseUpdates];
NSMenuItem *updateAll = [self addTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) toMenu:menu tag:TagUpdateFeed];
- (void)networkChanged:(NSNotification*)notify {
BOOL available = [[notify object] boolValue];
[self.barItem.menu itemWithTag:TagUpdateFeed].enabled = available;
[self updateBarIcon];
}
/**
Callback method fired when feeds have been updated and the total unread count needs update.
@param notify Notification object contains the unread count difference to the current count. May be negative.
*/
- (void)unreadCountChanged:(NSNotification*)notify {
self.unreadCountTotal += [[notify object] intValue];
[self updateBarIcon];
}
/**
Callback method fired when feeds have been updated in the background.
*/
- (void)feedUpdated:(NSNotification*)notify {
if (self.barItem.menu.numberOfItems > 0) {
// update items only if menu is already open (e.g., during background update)
[self.readContext refreshAllObjects]; // because self.allFeeds is the same context
[self recursiveUpdateMenu:self.barItem.menu withFeed:nil];
}
}
/**
Called recursively for all @c FeedConfig children.
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.
*/
- (void)recursiveUpdateMenu:(NSMenu*)menu withFeed:(FeedConfig*)config {
if (config.feed.items.count > 0) { // deepest menu level, feed items
[menu removeAllItems];
[self insertDefaultHeaderForAllMenus:menu scope:ScopeFeed hasUnread:(config.unreadCount > 0)];
for (FeedItem *fi in config.feed.items) {
NSMenuItem *mi = [menu addItemWithTitle:@"" action:@selector(openFeedURL:) keyEquivalent:@""];
mi.target = self;
[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];
}
}
}
#pragma mark - Menu Delegate & Menu Generation -
// Get rid of everything that is not needed when the system bar menu isnt open.
- (void)menuDidClose:(NSMenu*)menu {
if ([menu isMainMenu]) {
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.
- (NSInteger)numberOfItemsInMenu:(NSMenu*)menu {
if ([menu isMainMenu]) {
[self.readContext reset]; // will be ignored if nil
self.readContext = [StoreCoordinator createChildContext];
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.
@param indexString Path as string that is stored in @c NSMenu title
*/
- (FeedConfig*)configAtIndexPathStr:(NSString*)indexString {
NSArray<NSString*> *parts = [indexString componentsSeparatedByString:@"."];
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.
- (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:moid];
[self.readContext refreshObject:obj mergeChanges:YES];
if ([obj isKindOfClass:[FeedConfig class]]) {
[item setFeedConfig:obj];
if ([(FeedConfig*)obj typ] == FEED) {
item.target = self;
item.action = @selector(openFeedURL:);
}
} else if ([obj isKindOfClass:[FeedItem class]]) {
[item setFeedItem:obj];
item.target = self;
item.action = @selector(openFeedURL:);
}
if (menu.numberOfItems == index + 1) {
int unreadCount = self.unreadCountTotal; // if parent == nil
if ([obj isKindOfClass:[FeedItem class]]) {
unreadCount = [[[(FeedItem*)obj feed] config] unreadCount];
} else if ([(FeedConfig*)obj parent]) {
unreadCount = [[(FeedConfig*)obj parent] unreadCount];
}
[self finalizeMenu:menu hasUnread:(unreadCount > 0)];
self.currentOpenMenu = nil;
}
return YES;
}
/**
Add default menu items that are present in each menu as header.
@param flag If @c NO, 'Open all unread' and 'Mark all read' will be disabled.
*/
- (void)finalizeMenu:(NSMenu*)menu hasUnread:(BOOL)flag {
BOOL isMainMenu = [menu isMainMenu];
MenuItemTag scope;
if (isMainMenu) scope = ScopeGlobal;
else if ([menu isFeedMenu]) scope = ScopeFeed;
else scope = ScopeGroup;
[menu replaceSeparatorStringsWithActualSeparator];
[self insertDefaultHeaderForAllMenus:menu scope:scope hasUnread:flag];
if (isMainMenu) {
[self insertMainMenuHeader:menu];
}
}
/**
Insert items 'Open all unread', 'Mark all read' and 'Mark all unread' at index 0.
@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 {
NSMenuItem *item1 = [self itemTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) tag:TagOpenAllUnread | scope];
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 *item4 = [self itemTitle:NSLocalizedString(@"Mark all unread", nil) selector:@selector(markAllReadOrUnread:) tag:TagMarkAllUnread | scope];
item1.enabled = flag;
item2.enabled = flag;
item3.enabled = flag;
// TODO: disable item3 if all items are unread?
[menu insertItem:item1 atIndex:0];
[menu insertItem:item2 atIndex:1];
[menu insertItem:item3 atIndex:2];
[menu insertItem:item4 atIndex:3];
[menu insertItem:[NSMenuItem separatorItem] atIndex:4];
}
/**
Insert default menu items for the main menu only. Like 'Pause Updates', 'Update all feeds', 'Preferences' and 'Quit'.
*/
- (void)insertMainMenuHeader:(NSMenu*)menu {
NSMenuItem *item1 = [self itemTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) tag:TagPauseUpdates];
NSMenuItem *item2 = [self itemTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) tag:TagUpdateFeed];
if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO)
updateAll.hidden = YES;
item2.hidden = YES;
if (![FeedDownload isNetworkReachable])
item2.enabled = NO;
[menu insertItem:item1 atIndex:0];
[menu insertItem:item2 atIndex:1];
[menu insertItem:[NSMenuItem separatorItem] atIndex:2];
// < feed content >
[menu addItem:[NSMenuItem separatorItem]];
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
self.unreadCountTotal = 0;
@autoreleasepool {
for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) {
[menu addItem:[self generateMenuItem:fc unread:&_unreadCountTotal]];
}
}
[self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)];
[menu addItem:[NSMenuItem separatorItem]];
NSMenuItem *prefs = [self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) toMenu:menu tag:TagPreferences];
NSMenuItem *prefs = [self itemTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) tag:TagPreferences];
prefs.keyEquivalent = @",";
[menu addItem:prefs];
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
return menu;
}
/**
Generate menu item with all its sub-menus. @c FeedConfig type is evaluated automatically.
@param unread Pointer to an unread count. Will be incremented while traversing through sub-menus.
Helper method to generate a new @c NSMenuItem.
*/
- (NSMenuItem*)generateMenuItem:(FeedConfig*)config unread:(int*)unread {
NSMenuItem *item = [NSMenuItem feedConfig:config];
int count = 0;
if (item.tag == ScopeFeed) {
count += [self setSubmenuForFeedScope:item config:config];
} else if (item.tag == ScopeGroup) {
[self setSubmenuForGroupScope:item config:config unread:&count];
} else { // Separator item
return item;
}
*unread += count;
[item markReadAndUpdateTitle:-count];
[self updateMenuHeaderEnabled:item.submenu hasUnread:(count > 0)];
return item;
}
/**
Set subitems for a @c FeedConfig group item. Namely various @c FeedConfig and @c FeedItem items.
@param item The item where the menu will be appended.
@param config A @c FeedConfig group item.
@param unread Pointer to an unread count. Will be incremented while traversing through sub-menus.
*/
- (void)setSubmenuForGroupScope:(NSMenuItem*)item config:(FeedConfig*)config unread:(int*)unread {
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeGroup];
for (FeedConfig *obj in config.sortedChildren) {
[item.submenu addItem: [self generateMenuItem:obj unread:unread]];
}
}
/**
Set subitems for a @c FeedConfig feed item. Namely its @c FeedItem items.
@param item The item where the menu will be appended.
@param config For which item the menu should be generated. Attribute @c feed should be populated.
@return Unread count for feed.
*/
- (int)setSubmenuForFeedScope:(NSMenuItem*)item config:(FeedConfig*)config {
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeFeed];
int count = 0;
for (FeedItem *obj in config.feed.items) {
if (obj.unread) ++count;
[item.submenu addItem:[[NSMenuItem feedItem:obj] setAction:@selector(openFeedURL:) target:self]];
}
[item setAction:@selector(openFeedURL:) target:self];
return count;
}
/**
Helper function to insert a menu item with @c target @c = @c self
*/
- (NSMenuItem*)addTitle:(NSString*)title selector:(SEL)selector toMenu:(NSMenu*)menu tag:(MenuItemTag)tag {
- (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];
[menu addItem:item];
return item;
}
#pragma mark - Default Menu Header Items
/**
Append header items to menu accoring to user preferences.
@note If @c menu is @c nil a new menu is created and returned.
@param menu The menu where the items should be appended.
@param scope Tag will be concatenated with that scope (Global, Group or Local).
@return Will return the menu item provided or create a new one if menu was @c nil.
*/
- (NSMenu*)defaultHeaderForMenu:(NSMenu*)menu scope:(MenuItemTag)scope {
if (!menu) {
menu = [NSMenu new];
menu.autoenablesItems = NO;
}
NSMenuItem *item = [self addTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) toMenu:menu tag:TagOpenAllUnread | scope];
[menu addItem:[item alternateWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3]]];
[self addTitle:NSLocalizedString(@"Mark all read", nil) selector:@selector(markAllRead:) toMenu:menu tag:TagMarkAllRead | scope];
[self addTitle:NSLocalizedString(@"Mark all unread", nil) selector:@selector(markAllUnread:) toMenu:menu tag:TagMarkAllUnread | scope];
[menu addItem:[NSMenuItem separatorItem]];
return menu;
}
- (void)setItemUpdateAllHidden:(BOOL)hidden {
[self.barItem.menu itemWithTag:TagUpdateFeed].hidden = hidden;
}
- (void)updateMenuHeaders:(BOOL)recursive {
[self updateMenuHeaderHidden:self.barItem.menu recursive:recursive];
}
- (void)updateMenuHeaderHidden:(NSMenu*)menu recursive:(BOOL)flag {
for (NSMenuItem *item in menu.itemArray) {
[item applyUserSettingsDisplay];
if (flag && item.hasSubmenu) {
[self updateMenuHeaderHidden:item.submenu recursive:YES];
}
}
}
- (void)updateMenuHeaderEnabled:(NSMenu*)menu hasUnread:(BOOL)flag {
int stopAfter = 4; // 3 (+1 alternate)
for (NSMenuItem *item in menu.itemArray) {
switch (item.tag & TagMaskType) {
case TagMarkAllRead: item.enabled = flag; break;
case TagMarkAllUnread: item.enabled = !flag; break;
case TagOpenAllUnread: item.enabled = flag; break;
default: continue; // wrong tag, ignore
}
--stopAfter;
if (stopAfter < 0)
break; // break early after all header items have been processed
}
}
#pragma mark - Menu Actions
#pragma mark - Menu Actions -
/**
@@ -297,117 +331,100 @@
[self.prefWindow showWindow:nil];
}
/**
Callback method after user closes the preferences window.
*/
- (void)preferencesClosed:(id)sender {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:self.prefWindow.window];
self.prefWindow = nil;
}
/**
Called when user clicks on 'Pause Updates' in the main menu (only).
*/
- (void)pauseUpdates:(NSMenuItem*)sender {
NSLog(@"1pause");
}
/**
Called when user clicks on 'Update all feeds' in the main menu (only).
*/
- (void)updateAllFeeds:(NSMenuItem*)sender {
// TODO: Disable 'update all' menu item during update?
[FeedDownload scheduleNextUpdate:YES];
}
/**
Combined selector for menu action.
@note @c sender.tag includes @c ScopeLocal, @c ScopeGroup @b or @c ScopeGlobal.
@param sender @c NSMenuItem that was clicked during the action (e.g., "open all unread")
Called when user clicks on 'Open all unread' or 'Open a few unread ...' on any scope level.
*/
- (void)openAllUnread:(NSMenuItem*)sender {
int maxItemCount = INT_MAX;
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
__block int maxItemCount = INT_MAX;
if (sender.isAlternate)
maxItemCount = 3; // TODO: read from preferences
__block int stopAfter = maxItemCount;
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
if (stopAfter <= 0)
return NO; // stop further processing
if (item.unread && item.link.length > 0) {
[urls addObject:[NSURL URLWithString:item.link]];
item.unread = NO;
--stopAfter;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[sender iterateSorted:YES inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
int itemSum = 0;
for (FeedItem *i in feed.items) {
if (itemSum >= maxItemCount) {
break;
}
return YES;
if (i.unread && i.link.length > 0) {
[urls addObject:[NSURL URLWithString:i.link]];
i.unread = NO;
++itemSum;
}
}
if (itemSum > 0) {
[feed.config markUnread:-itemSum ancestorsOnly:NO];
maxItemCount -= itemSum;
}
*cancel = (maxItemCount <= 0);
}];
stopAfter = maxItemCount;
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
if (item.tag & ScopeFeed) {
if (stopAfter <= 0) return -1;
--stopAfter;
}
[item markReadAndUpdateTitle:count];
return count;
} unreadEntriesOnly:YES];
[self updateAcestors:sender markRead:total];
[self openURLsWithPreferredBrowser:urls];
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset];
}
/**
Combined selector for menu action.
@note @c sender.tag includes @c ScopeLocal, @c ScopeGroup @b or @c ScopeGlobal.
@param sender @c NSMenuItem that was clicked during the action (e.g., "mark all read")
Called when user clicks on 'Mark all read' @b or 'Mark all unread' on any scope level.
*/
- (void)markAllRead:(NSMenuItem*)sender {
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
if (item.unread)
item.unread = NO;
return YES;
- (void)markAllReadOrUnread:(NSMenuItem*)sender {
BOOL markRead = ((sender.tag & TagMaskType) == TagMarkAllRead);
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[sender iterateSorted:NO inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
if (markRead) [feed markAllItemsRead];
else [feed markAllItemsUnread];
}];
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
[item markReadAndUpdateTitle:count];
return count;
} unreadEntriesOnly:YES];
[self updateAcestors:sender markRead:total];
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset];
}
/**
Combined selector for menu action.
Called when user clicks on a single feed item or the feed group.
@note @c sender.tag includes @c ScopeLocal, @c ScopeGroup @b or @c ScopeGlobal.
@param sender @c NSMenuItem that was clicked during the action (e.g., "mark all unread")
*/
- (void)markAllUnread:(NSMenuItem*)sender {
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
if (item.unread == NO)
item.unread = YES;
return YES;
}];
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
if (count > item.unreadCount)
[item markReadAndUpdateTitle:(item.unreadCount - count)];
return count;
} unreadEntriesOnly:NO];
[self updateAcestors:sender markRead:([self getAncestorUnreadCount:sender] - total)];
}
/**
Called when user clicks on a single feed item or the superior feed.
@param sender A menu item containing either a @c FeedItem or a @c FeedConfig.
@param sender A menu item containing either a @c FeedItem or a @c FeedConfig objectID.
*/
- (void)openFeedURL:(NSMenuItem*)sender {
if (!sender.hasReaderInfo)
NSManagedObjectID *oid = sender.representedObject;
if (!oid)
return;
NSString *url = nil;
id obj = [sender requestCoreDataObject];
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
id obj = [moc objectWithID:oid];
if ([obj isKindOfClass:[FeedConfig class]]) {
url = [[(FeedConfig*)obj feed] link];
} else if ([obj isKindOfClass:[FeedItem class]]) {
FeedItem *feed = obj;
url = [feed link];
if ([sender hasUnread]) {
if (feed.unread) {
feed.unread = NO;
[sender markReadAndUpdateTitle:1];
[self updateAcestors:sender markRead:1];
[feed.feed.config markUnread:-1 ancestorsOnly:NO];
[StoreCoordinator saveContext:moc andParent:YES];
}
}
[moc reset];
if (!url || url.length == 0) return;
[self openURLsWithPreferredBrowser:@[[NSURL URLWithString:url]]];
}
@@ -422,58 +439,4 @@
[[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:[UserPrefs getHttpApplication] options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
}
#pragma mark - Iterating over items and propagating unread count
/**
Iterate over all feed items from siblings and contained children.
@param sender @c NSMenuItem that was clicked during the action (e.g., "open all unread")
@param block Iterate over all FeedItems on the deepest layer.
*/
- (void)siblingsDescendantFeedConfigs:(NSMenuItem*)sender block:(FeedConfigRecursiveItemsBlock)block {
if (sender.parentItem) {
FeedConfig *obj = [sender.parentItem requestCoreDataObject];
if ([obj isKindOfClass:[FeedConfig class]]) // important: this could be a FeedItem
[obj descendantFeedItems:block];
} else {
// Sadly we can't just fetch the list of FeedItems since it is not ordered (in case open 10 at a time)
@autoreleasepool {
for (FeedConfig *config in [StoreCoordinator sortedFeedConfigItems]) {
if ([config descendantFeedItems:block] == NO)
break;
}
}
}
}
/**
Recursively update all parent's unread count and total unread count.
@param sender Current menu item, parent will be called recursively on this element.
@param count The amount by which the unread count is adjusted. If negative, items will be marked as unread.
*/
- (void)updateAcestors:(NSMenuItem*)sender markRead:(int)count {
[sender markAncestorsRead:count];
self.unreadCountTotal -= count;
if (self.unreadCountTotal < 0) {
NSLog(@"Should never happen. Global unread count < 0");
self.unreadCountTotal = 0;
}
[self updateBarIcon];
}
/**
Get unread count from the parent menu item. If there is none, get the total unread count
@param sender Current menu item, parent will be called on this element.
@return Unread count for parent element (total count if parent is @c nil)
*/
- (int)getAncestorUnreadCount:(NSMenuItem*)sender {
if ([sender.parentItem hasReaderInfo])
return [sender.parentItem unreadCount];
return self.unreadCountTotal;
}
@end

View File

@@ -22,12 +22,11 @@
#import <Cocoa/Cocoa.h>
@class FeedConfig, FeedItem;
@interface NSMenuItem (Generate)
+ (NSMenuItem*)feedConfig:(FeedConfig*)config;
+ (NSMenuItem*)feedItem:(FeedItem*)item;
- (NSMenuItem*)alternateWithTitle:(NSString*)title;
- (NSMenuItem*)setAction:(nullable SEL)action target:(nullable id)target;
@interface NSMenu (Ext)
+ (instancetype)menuWithDelegate:(id<NSMenuDelegate>)target;
- (instancetype)submenuWithIndex:(int)index isFeed:(BOOL)flag;
- (void)replaceSeparatorStringsWithActualSeparator;
- (BOOL)isMainMenu;
- (BOOL)isFeedMenu;
- (NSInteger)getFeedConfigOffsetAndUpdateUnread:(BOOL)hasUnread;
@end

View File

@@ -0,0 +1,101 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "NSMenu+Ext.h"
#import "NSMenuItem+Ext.h"
@implementation NSMenu (Ext)
+ (instancetype)menuWithDelegate:(id<NSMenuDelegate>)target {
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"M"];
menu.autoenablesItems = NO;
menu.delegate = target;
return menu;
}
- (instancetype)submenuWithIndex:(int)index isFeed:(BOOL)flag {
NSMenu *menu = [NSMenu new];
menu.title = [NSString stringWithFormat:@"%c%@.%d", (flag ? 'F' : 'G'), self.title, index];
menu.autoenablesItems = NO;
menu.delegate = self.delegate;
return menu;
}
- (void)replaceSeparatorStringsWithActualSeparator {
for (NSInteger i = 0; i < self.numberOfItems; i++) {
NSMenuItem *oldItem = [self itemAtIndex:i];
if ([oldItem.title isEqualToString:@"---SEPARATOR---"]) {
NSMenuItem *newItem = [NSMenuItem separatorItem];
newItem.representedObject = oldItem.representedObject;
[self removeItemAtIndex:i];
[self insertItem:newItem atIndex:i];
}
}
}
- (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

View File

@@ -42,26 +42,14 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
TagMaskType = 0xFFF0,
};
@class FeedConfig, Feed, FeedItem;
@interface NSMenuItem (Info)
/**
Iteration block for descendants of @c NSMenuItem.
@interface NSMenuItem (Feed)
- (NSMenuItem*)alternateWithTitle:(NSString*)title;
@param count The number of sub-elements contained in that @c NSMenuItem. 1 for @c FeedItems at the deepest layer.
Otherwise the number of (updated) descendants.
@return Return how many elements are updated in this block execution. If none were changed return @c 0.
If execution should be stopped early, return @c -1.
*/
typedef int (^ReaderInfoRecursiveBlock) (NSMenuItem *item, int count);
- (BOOL)hasUnread;
- (int)unreadCount;
- (BOOL)hasReaderInfo;
- (void)setReaderInfo:(NSManagedObjectID*)oid unread:(int)count;
- (id)requestCoreDataObject;
- (void)setFeedConfig:(FeedConfig*)config;
- (void)setFeedItem:(FeedItem*)item;
- (void)setTitleAndUnreadCount:(FeedConfig*)config;
- (void)iterateSorted:(BOOL)ordered inContext:(NSManagedObjectContext*)moc overDescendentFeeds:(void(^)(Feed *feed, BOOL *cancel))block;
- (void)applyUserSettingsDisplay;
- (void)markReadAndUpdateTitle:(int)count;
- (void)countInTitle:(BOOL)show;
- (void)markAncestorsRead:(int)count;
- (int)siblingsDescendantItemInfo:(ReaderInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag;
@end

View File

@@ -0,0 +1,217 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "NSMenuItem+Ext.h"
#import "NSMenu+Ext.h"
#import "StoreCoordinator.h"
#import "DrawImage.h"
#import "UserPrefs.h"
/// User preferences for displaying menu items
typedef NS_ENUM(char, DisplaySetting) {
/// User preference not available. @c NSMenuItem is not configurable (not a header item)
INVALID,
/// User preference to display this item
ALLOW,
/// User preference to hide this item
PROHIBIT
};
@implementation NSMenuItem (Feed)
/**
Create a copy of an existing menu item and set it's option key modifier.
*/
- (NSMenuItem*)alternateWithTitle:(NSString*)title {
NSMenuItem *alt = [self copy];
alt.title = title;
alt.keyEquivalentModifierMask = NSEventModifierFlagOption;
if (!alt.hidden) { // hidden will be ignored if alternate is YES
alt.hidden = YES; // force hidden to hide if menu is already open (background update)
alt.alternate = YES;
}
return alt;
}
/**
Set title based on preferences either with or without unread count in parenthesis.
*/
- (void)setTitleAndUnreadCount:(FeedConfig*)config {
if (config.unreadCount > 0 &&
((config.typ == FEED && [UserPrefs defaultYES:@"feedUnreadCount"]) ||
(config.typ == GROUP && [UserPrefs defaultYES:@"groupUnreadCount"])))
{
self.title = [NSString stringWithFormat:@"%@ (%d)", config.name, config.unreadCount];
} else {
self.title = config.name;
}
}
/**
Fully configures a Separator item OR group item OR feed item. (but not @c FeedItem item)
*/
- (void)setFeedConfig:(FeedConfig*)config {
self.representedObject = config.objectID;
if (config.typ == SEPARATOR) {
self.title = @"---SEPARATOR---";
} else {
[self setTitleAndUnreadCount:config];
self.submenu = [self.menu submenuWithIndex:config.sortIndex isFeed:(config.typ == FEED)];
if (config.typ == FEED) {
[self configureAsFeed:config];
} else {
[self configureAsGroup:config];
}
}
}
/**
Configure menu item to be used as a container for @c FeedItem entries (incl. feed icon).
*/
- (void)configureAsFeed:(FeedConfig*)config {
self.tag = ScopeFeed;
self.toolTip = config.feed.subtitle;
self.enabled = (config.feed.items.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:(FeedConfig*)config {
self.tag = ScopeGroup;
self.enabled = (config.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 FeedItem.
*/
- (void)setFeedItem:(FeedItem*)item {
self.title = item.title;
self.tag = ScopeFeed;
self.enabled = (item.link.length > 0);
self.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff);
self.representedObject = item.objectID;
//mi.toolTip = item.abstract;
// TODO: Do regex during save, not during display. Its here for testing purposes ...
if (item.abstract.length > 0) {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"<[^>]*>" options:kNilOptions error:nil];
self.toolTip = [regex stringByReplacingMatchesInString:item.abstract options:kNilOptions range:NSMakeRange(0, item.abstract.length) withTemplate:@""];
}
}
#pragma mark - Helper -
/**
@return @c FeedConfig object if @c representedObject contains a valid @c NSManagedObjectID.
*/
- (FeedConfig*)feedConfig:(NSManagedObjectContext*)moc {
if (!self.representedObject || ![self.representedObject isKindOfClass:[NSManagedObjectID class]])
return nil;
FeedConfig *config = [moc objectWithID:self.representedObject];
if (![config isKindOfClass:[FeedConfig class]])
return nil;
return config;
}
/**
Perform @c block on every @c FeedConfig in the items menu or any of its submenues.
@param ordered Whether order matters or not. If all items are processed anyway, pass @c NO for a speedup.
@param block Set cancel to @c YES to stop enumeration early.
*/
- (void)iterateSorted:(BOOL)ordered inContext:(NSManagedObjectContext*)moc overDescendentFeeds:(void(^)(Feed*,BOOL*))block {
if (self.parentItem) {
[[self.parentItem feedConfig:moc] iterateSorted:ordered overDescendantFeeds:block];
} else {
for (NSMenuItem *item in self.menu.itemArray) {
FeedConfig *fc = [item feedConfig:moc];
if (fc != nil) { // All groups and feeds; Ignore default header
if (![fc iterateSorted:ordered overDescendantFeeds:block])
return;
}
}
}
}
/**
Check user preferences for preferred display style.
@return As per user settings return @c ALLOW or @c PROHIBIT. Will return @c INVALID for items that aren't configurable.
*/
- (DisplaySetting)allowsDisplay {
NSString *prefix;
switch (self.tag & TagMaskScope) {
case ScopeFeed: prefix = @"feed"; break;
case ScopeGroup: prefix = @"group"; break;
case ScopeGlobal: prefix = @"global"; break;
default: return INVALID; // no scope, not recognized menu item
}
NSString *postfix;
switch (self.tag & TagMaskType) {
case TagOpenAllUnread: postfix = @"OpenUnread"; break;
case TagMarkAllRead: postfix = @"MarkRead"; break;
case TagMarkAllUnread: postfix = @"MarkUnread"; break;
default: return INVALID; // wrong tag, ignore
}
if ([UserPrefs defaultYES:[prefix stringByAppendingString:postfix]])
return ALLOW;
return PROHIBIT;
}
/**
Set item @c hidden based on user preferences. Does nothing for items that aren't configurable in settings.
*/
- (void)applyUserSettingsDisplay {
switch ([self allowsDisplay]) {
case ALLOW:
self.hidden = NO;
if (self.keyEquivalentModifierMask == NSEventModifierFlagOption)
self.alternate = YES; // restore alternate flag
break;
case PROHIBIT:
if (self.isAlternate)
self.alternate = NO; // to allow hidden = YES, alternate flag needs to be NO
self.hidden = YES;
break;
case INVALID: break;
}
}
@end

View File

@@ -1,127 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "NSMenuItem+Generate.h"
#import "NSMenuItem+Info.h"
#import "StoreCoordinator.h"
#import "DrawImage.h"
@implementation NSMenuItem (Feed)
/**
Generate a new @c NSMenuItem based on the type stored in @c FeedConfig.
@param config @c FeedConfig object that represents a superior feed element.
@return Return a fully configured Separator item OR group item OR feed item. (but not @c FeedItem item)
*/
+ (NSMenuItem*)feedConfig:(FeedConfig*)config {
NSMenuItem *item;
switch (config.typ) {
case SEPARATOR: item = [NSMenuItem separatorItem]; break;
case GROUP: item = [self feedConfigItemGroup:config]; break;
case FEED: item = [self feedConfigItemFeed:config]; break;
}
[item setReaderInfo:config.objectID unread:0];
return item;
}
/**
Generate a new @c NSMenuItem from a @c FeedConfig feed item.
@param config @c FeedConfig object that represents a superior feed element.
*/
+ (NSMenuItem*)feedConfigItemFeed:(FeedConfig*)config {
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""];
item.toolTip = config.feed.subtitle;
item.enabled = (config.feed.items.count > 0);
item.tag = ScopeFeed;
// set icon
dispatch_async(dispatch_get_main_queue(), ^{
static NSImage *defaultRSSIcon;
if (!defaultRSSIcon)
defaultRSSIcon = [RSSIcon iconWithSize:16];
item.image = defaultRSSIcon;
});
return item;
}
/**
Generate a new @c NSMenuItem from a @c FeedConfig group item
@param config @c FeedConfig object that represents a group item.
*/
+ (NSMenuItem*)feedConfigItemGroup:(FeedConfig*)config {
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""];
item.tag = ScopeGroup;
// set icon
dispatch_async(dispatch_get_main_queue(), ^{
static NSImage *groupIcon;
if (!groupIcon) {
groupIcon = [NSImage imageNamed:NSImageNameFolder];
groupIcon.size = NSMakeSize(16, 16);
}
item.image = groupIcon;
});
return item;
}
/**
Generate new @c NSMenuItem based on the attributes of a @c FeedItem.
*/
+ (NSMenuItem*)feedItem:(FeedItem*)item {
NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:nil keyEquivalent:@""];
[mi setReaderInfo:item.objectID unread:(item.unread ? 1 : 0)];
//mi.toolTip = item.abstract;
// TODO: Do regex during save, not during display. Its here for testing purposes ...
if (item.abstract.length > 0) {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"<[^>]*>" options:kNilOptions error:nil];
mi.toolTip = [regex stringByReplacingMatchesInString:item.abstract options:kNilOptions range:NSMakeRange(0, item.abstract.length) withTemplate:@""];
}
mi.enabled = (item.link.length > 0);
mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff);
mi.tag = ScopeFeed;
return mi;
}
/**
Create a copy of an existing menu item and set it's option key modifier.
*/
- (NSMenuItem*)alternateWithTitle:(NSString*)title {
NSMenuItem *alt = [self copy];
alt.title = title;
alt.keyEquivalentModifierMask = NSEventModifierFlagOption;
if (!alt.hidden) // hidden will be ignored if alternate is YES
alt.alternate = YES;
return alt;
}
/**
Set @c action and @c target attributes.
@return Return @c self instance. Intended for method chains.
*/
- (NSMenuItem*)setAction:(SEL)action target:(id)target {
self.action = action;
self.target = target;
return self;
}
@end

View File

@@ -1,275 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "NSMenuItem+Info.h"
#import "UserPrefs.h"
#import "StoreCoordinator.h"
/// User preferences for displaying menu items
typedef NS_ENUM(char, DisplaySetting) {
/// User preference not available. @c NSMenuItem is not configurable (not a header item)
INVALID,
/// User preference to display this item
ALLOW,
/// User preference to hide this item
PROHIBIT
};
@interface ReaderInfo : NSObject
@property (strong) NSManagedObjectID *objID;
/// internal counter used to sum the unread count of all sub items
@property (assign) int unreadCount;
/// internal flag whether unread count is displayed in parenthesis
@property (assign) BOOL countInTitle;
@end
@implementation ReaderInfo
/// set: unreadCount -= count
- (void)markRead:(int)count {
if (count > self.unreadCount) {
NSLog(@"should never happen, trying to set an unread count below zero");
self.unreadCount = 0;
} else {
self.unreadCount -= count;
}
}
@end
// ################################################################
// #
// # NSMenuItem ReaderInfo Extension
// #
// ################################################################
@implementation NSMenuItem (Info)
/** Call represented object and check whether unread count > 0. */
- (BOOL)hasUnread {
return [(ReaderInfo*)self.representedObject unreadCount] > 0;
}
/** Call represented object and retrieve the unread count from info. */
- (int)unreadCount {
return [(ReaderInfo*)self.representedObject unreadCount];
}
/** Return @c YES if @c ReaderInfo is stored in @c representedObject. */
- (BOOL)hasReaderInfo {
return [self.representedObject isKindOfClass:[ReaderInfo class]];
}
/**
Save represented core data object in @c ReaderInfo.
@param oid Represented core data object id.
@param count Unread count for item.
*/
- (void)setReaderInfo:(NSManagedObjectID*)oid unread:(int)count {
ReaderInfo *info = [ReaderInfo new];
info.objID = oid;
info.unreadCount = count;
self.representedObject = info;
}
/**
Return represented core data object. Return @c nil if @c ReaderInfo is missing.
*/
- (id)requestCoreDataObject {
if (![self hasReaderInfo])
return nil;
return [StoreCoordinator objectWithID: [(ReaderInfo*)self.representedObject objID]];
}
/**
Check user preferences for preferred display style.
@return As per user settings return @c ALLOW or @c PROHIBIT. Will return @c INVALID for items that aren't configurable.
*/
- (DisplaySetting)allowsDisplay {
NSString *prefix;
switch (self.tag & TagMaskScope) {
case ScopeFeed: prefix = @"feed"; break;
case ScopeGroup: prefix = @"group"; break;
case ScopeGlobal: prefix = @"global"; break;
default: return INVALID; // no scope, not recognized menu item
}
NSString *postfix;
switch (self.tag & TagMaskType) {
case TagOpenAllUnread: postfix = @"OpenUnread"; break;
case TagMarkAllRead: postfix = @"MarkRead"; break;
case TagMarkAllUnread: postfix = @"MarkUnread"; break;
default: return INVALID; // wrong tag, ignore
}
if ([UserPrefs defaultYES:[prefix stringByAppendingString:postfix]])
return ALLOW;
return PROHIBIT;
}
/**
Set item @c hidden based on user preferences. Does nothing for items that aren't configurable in settings.
*/
- (void)applyUserSettingsDisplay {
switch ([self allowsDisplay]) {
case ALLOW:
self.hidden = NO;
if (self.keyEquivalentModifierMask == NSEventModifierFlagOption)
self.alternate = YES; // restore alternate flag
break;
case PROHIBIT:
if (self.isAlternate)
self.alternate = NO; // to allow hidden = YES, alternate flag needs to be NO
self.hidden = YES;
break;
case INVALID: break;
}
}
/**
Update internal unread counter and append unread count to title.
@note Count may be negative to mark items as unread.
@warning Does not check if @c representedObject is set accordingly
@param count The amount by which the counter is adjusted.
If negative the items will be marked as unread.
*/
- (void)markReadAndUpdateTitle:(int)count {
if (count == 0) return; // 0 won't change anything
ReaderInfo *info = self.representedObject;
if (!self.hasSubmenu) {
[info markRead:count];
self.state = ([self hasUnread] ? NSControlStateValueOn : NSControlStateValueOff);
} else {
int countBefore = info.unreadCount;
[info markRead:count];
if (info.countInTitle) {
[self removeUnreadCountFromTitle:countBefore];
info.countInTitle = NO;
}
[self addUnreadCountToTitle];
}
}
/**
Update title without changing internal unread count. Save to call multiple times.
@param show Whether to show or hide count
*/
- (void)countInTitle:(BOOL)show {
ReaderInfo *info = self.representedObject;
NSLog(@"%@", info);
return;
if (!show && info.countInTitle) {
[self removeUnreadCountFromTitle: info.unreadCount];
info.countInTitle = NO;
} else if (show && !info.countInTitle) {
[self addUnreadCountToTitle];
}
}
/**
Update title after unread count has changed
@param countBefore The count before the update
*/
- (void)removeUnreadCountFromTitle:(int)countBefore {
int digitsBefore = (int)log10f(countBefore) + 1;
NSInteger index = (NSInteger)self.title.length - digitsBefore - 3; // " (%d)"
if (index < 0) index = 0;
self.title = [self.title substringToIndex:(NSUInteger)index]; // remove old count
}
/**
Append count in parenthesis if thats allowed for the current scope (user settings)
*/
- (void)addUnreadCountToTitle {
ReaderInfo *info = self.representedObject;
if (info.unreadCount > 0 &&
(((self.tag & ScopeGroup) && [UserPrefs defaultYES:@"groupUnreadCount"]) ||
((self.tag & ScopeFeed) && [UserPrefs defaultYES:@"feedUnreadCount"])))
{
self.title = [self.title stringByAppendingFormat:@" (%d)", info.unreadCount];
info.countInTitle = YES;
}
}
/**
Recursively propagate unread count to ancestor menu items.
@note Does not update the current item, only the ancestors.
@param count The amount by which the counter is adjusted.
If negative the items will be marked as unread.
*/
- (void)markAncestorsRead:(int)count {
NSMenuItem *parent = self.parentItem;
while (parent.representedObject) {
[parent markReadAndUpdateTitle:count];
parent = parent.parentItem;
}
}
/**
Recursively iterate over submenues and children. Count aggregated element edits.
@warning Block will be called for parent items, too. Consider this when using counters.
@param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c ReaderInfo.
Return -1 to stop processing early.
@param flag If set to @c YES, recursive calls will be skipped for submenus that contain soleily read elements.
@return The number of changed elements in total.
*/
- (int)descendantItemInfo:(ReaderInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag {
if (self.isSeparatorItem) return 0;
if (![self hasReaderInfo]) return 0;
if (flag && ![self hasUnread]) return 0;
int countItems = 1; // deepest entry, FeedItem
if (self.hasSubmenu) {
countItems = 0;
for (NSMenuItem *child in self.submenu.itemArray) {
int c = [child descendantItemInfo:block unreadEntriesOnly:flag];
if (c < 0) break;
countItems += c;
}
}
return block(self, countItems);
}
/**
Recursively iterate over siblings and all contained children. Count aggregated element edits.
@warning Block will be called for parent items, too. Consider this when using counters.
@param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c ReaderInfo.
Return -1 to stop processing early.
@param flag If set to @c YES, recursive calls will be skipped for submenus that contain soleily read elements.
@return The number of changed elements in total.
*/
- (int)siblingsDescendantItemInfo:(ReaderInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag {
int markedTotal = 0;
for (NSMenuItem *sibling in self.menu.itemArray) {
int marked = [sibling descendantItemInfo:block unreadEntriesOnly:flag];
if (marked < 0) break;
markedTotal += marked;
}
return markedTotal;
}
@end

View File

@@ -27,11 +27,14 @@
@class RSParsedFeed;
@interface StoreCoordinator : NSObject
+ (void)saveContext:(NSManagedObjectContext*)context;
+ (void)deleteUnreferencedFeeds;
+ (NSArray<FeedConfig*>*)sortedFeedConfigItems;
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll;
+ (NSManagedObjectContext*)getMainContext;
+ (NSManagedObjectContext*)createChildContext;
+ (void)saveContext:(NSManagedObjectContext*)context andParent:(BOOL)flag;
+ (NSArray<FeedConfig*>*)sortedFeedConfigItemsInContext:(nonnull NSManagedObjectContext*)context;
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc;
+ (NSDate*)nextScheduledUpdate;
+ (id)objectWithID:(NSManagedObjectID*)objID;
+ (void)overwriteConfig:(FeedConfig*)config withFeed:(RSParsedFeed*)obj;
+ (int)totalNumberOfUnreadFeeds;
// Restore sound state
+ (void)deleteUnreferencedFeeds;
+ (void)restoreUnreadCount;
@end

View File

@@ -26,11 +26,19 @@
@implementation StoreCoordinator
+ (NSManagedObjectContext*)getContext {
+ (NSManagedObjectContext*)getMainContext {
return [(AppHook*)NSApp persistentContainer].viewContext;
}
+ (void)saveContext:(NSManagedObjectContext*)context {
+ (NSManagedObjectContext*)createChildContext {
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setParentContext:[self getMainContext]];
context.undoManager = nil;
//context.automaticallyMergesChangesFromParent = YES;
return context;
}
+ (void)saveContext:(NSManagedObjectContext*)context andParent:(BOOL)flag {
// Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user.
if (![context commitEditing]) {
NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd));
@@ -40,31 +48,22 @@
// Customize this code block to include application-specific recovery steps.
[[NSApplication sharedApplication] presentError:error];
}
if (flag && context.parentContext) {
[self saveContext:context.parentContext andParent:flag];
}
}
+ (void)deleteUnreferencedFeeds {
NSManagedObjectContext *moc = [self getContext];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:Feed.entity.name];
fr.predicate = [NSPredicate predicateWithFormat:@"config = NULL"];
NSBatchDeleteRequest *bdr = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fr];
NSError *err;
[moc executeRequest:bdr error:&err];
if (err) NSLog(@"%@", err);
}
+ (NSArray<FeedConfig*>*)sortedFeedConfigItems {
NSManagedObjectContext *moc = [self getContext];
+ (NSArray<FeedConfig*>*)sortedFeedConfigItemsInContext:(NSManagedObjectContext*)context {
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 = [moc executeFetchRequest:fr error:&err];
NSArray *result = [context executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return result;
}
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll {
NSManagedObjectContext *moc = [self getContext];
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc {
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name];
if (!forceAll) {
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d AND scheduled <= %@", FEED, [NSDate date]];
@@ -78,7 +77,10 @@
}
+ (NSDate*)nextScheduledUpdate {
NSExpression *exp = [NSExpression expressionForFunction:@"min:" arguments:@[[NSExpression expressionForKeyPath:@"scheduled"]]];
// Always get context first, or 'FeedConfig.entity.name' may not be available on app start
NSManagedObjectContext *moc = [self getMainContext];
NSExpression *exp = [NSExpression expressionForFunction:@"min:"
arguments:@[[NSExpression expressionForKeyPath:@"scheduled"]]];
NSExpressionDescription *expDesc = [[NSExpressionDescription alloc] init];
[expDesc setName:@"earliestDate"];
[expDesc setExpression:exp];
@@ -90,65 +92,79 @@
[fr setPropertiesToFetch:@[expDesc]];
NSError *err;
NSArray *fetchResults = [[self getContext] executeFetchRequest:fr error:&err];
NSArray *fetchResults = [moc executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return [fetchResults firstObject][@"earliestDate"]; // can be nil
return fetchResults.firstObject[@"earliestDate"]; // can be nil
}
+ (id)objectWithID:(NSManagedObjectID*)objID {
return [[self getContext] objectWithID:objID];
+ (int)totalNumberOfUnreadFeeds {
// Always get context first, or 'FeedConfig.entity.name' may not be available on app start
NSManagedObjectContext *moc = [self getMainContext];
NSExpression *exp = [NSExpression expressionForFunction:@"sum:"
arguments:@[[NSExpression expressionForKeyPath:@"unreadCount"]]];
NSExpressionDescription *expDesc = [[NSExpressionDescription alloc] init];
[expDesc setName:@"totalUnread"];
[expDesc setExpression:exp];
[expDesc setExpressionResultType:NSInteger32AttributeType];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name];
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d", FEED];
[fr setResultType:NSDictionaryResultType];
[fr setPropertiesToFetch:@[expDesc]];
NSError *err;
NSArray *fetchResults = [moc executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return [fetchResults.firstObject[@"totalUnread"] intValue];
}
//+ (void)addToSortIndex:(int)num start:(int)index parent:(FeedConfig*)config inContext:(NSManagedObjectContext*)moc {
// NSBatchUpdateRequest *ur = [[NSBatchUpdateRequest alloc] initWithEntityName: FeedConfig.entity.name];
// ur.predicate = [NSPredicate predicateWithFormat:@"parent = %@ AND sortIndex >= %d", config, index];
// ur.propertiesToUpdate = @{@"sortIndex": [NSExpression expressionWithFormat: @"sortIndex + %d", num]};
// ur.resultType = NSUpdatedObjectsCountResultType;//NSUpdatedObjectIDsResultType;//NSStatusOnlyResultType;
// NSError *err;
// NSBatchUpdateResult *result = [moc executeRequest:ur error:&err];
// if (err) NSLog(@"%@", err);
// NSLog(@"Result: %@", result.result);
// //[NSManagedObjectContext mergeChangesFromRemoteContextSave:@{NSUpdatedObjectsKey : result.result} intoContexts:@[moc]];
//}
+ (void)overwriteConfig:(FeedConfig*)config withFeed:(RSParsedFeed*)obj {
NSArray<NSString*> *readURLs = [self alreadyReadURLsInFeed:config.feed];
[config.managedObjectContext performBlockAndWait:^{
if (config.feed)
[config.managedObjectContext deleteObject:(NSManagedObject*)config.feed];
if (obj) {
config.feed = [StoreCoordinator createFeedFrom:obj inContext:config.managedObjectContext alreadyRead:readURLs];
#pragma mark - Restore Sound State -
+ (void)deleteUnreferencedFeeds {
NSManagedObjectContext *moc = [self getMainContext];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:Feed.entity.name];
fr.predicate = [NSPredicate predicateWithFormat:@"config = NULL"];
NSBatchDeleteRequest *bdr = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fr];
NSError *err;
[moc executeRequest:bdr error:&err];
if (err) NSLog(@"%@", err);
}
+ (void)restoreUnreadCount {
NSManagedObjectContext *moc = [self getMainContext];
NSError *err;
NSArray *confs = [moc executeFetchRequest:[NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name] error:&err];
if (err) NSLog(@"%@", err);
NSArray *feeds = [moc executeFetchRequest:[NSFetchRequest fetchRequestWithEntityName: Feed.entity.name] error:&err];
if (err) NSLog(@"%@", err);
[moc performBlock:^{
for (FeedConfig *conf in confs) {
conf.unreadCount = 0;
}
for (Feed *feed in feeds) {
int count = 0;
for (FeedItem *item in feed.items) {
if (item.unread) ++count;
}
FeedConfig *parent = feed.config;
while (parent) {
parent.unreadCount += count;
parent = parent.parent;
}
}
}];
}
#pragma mark - Helper methods -
+ (FeedItem*)createFeedItemFrom:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)context {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context];
b.guid = entry.guid;
b.title = entry.title;
b.abstract = entry.abstract;
b.body = entry.body;
b.author = entry.author;
b.link = entry.link;
b.published = entry.datePublished;
return b;
}
+ (Feed*)createFeedFrom:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls {
Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context];
a.title = obj.title;
a.subtitle = obj.subtitle;
a.link = obj.link;
for (RSParsedArticle *article in obj.articles) {
FeedItem *b = [self createFeedItemFrom:article inContext:context];
if ([urls containsObject:b.link]) {
b.unread = NO;
}
[a addItemsObject:b];
}
return a;
}
+ (NSArray<NSString*>*)alreadyReadURLsInFeed:(Feed*)local {
if (!local || !local.items) return nil;
NSMutableArray<NSString*> *mArr = [NSMutableArray arrayWithCapacity:local.items.count];
for (FeedItem *f in local.items) {
if (!f.unread) {
[mArr addObject:f.link];
}
}
return mArr;
}
@end