Refactoring Part 1: Dynamic menus (stable)
This commit is contained in:
@@ -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 */,
|
||||
);
|
||||
|
||||
32
baRSS/Categories/Feed+Ext.h
Normal file
32
baRSS/Categories/Feed+Ext.h
Normal 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
|
||||
89
baRSS/Categories/Feed+Ext.m
Normal file
89
baRSS/Categories/Feed+Ext.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -34,37 +36,34 @@
|
||||
|
||||
@return Sorted array of @c FeedConfig items.
|
||||
*/
|
||||
- (NSArray<FeedConfig *> *)sortedChildren {
|
||||
- (NSArray<FeedConfig*>*)sortedChildren {
|
||||
if (self.children.count == 0)
|
||||
return nil;
|
||||
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
|
||||
}
|
||||
|
||||
- (NSIndexPath *)indexPath {
|
||||
/// 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;
|
||||
}
|
||||
} else if (self.feed.items.count > 0) {
|
||||
for (FeedItem* item in self.feed.items) {
|
||||
if (block(self, item) == 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;
|
||||
}
|
||||
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,14 +77,46 @@
|
||||
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 {
|
||||
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
|
||||
30
baRSS/Constants.h
Normal file
30
baRSS/Constants.h
Normal 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 */
|
||||
@@ -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>
|
||||
|
||||
@@ -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,20 +123,21 @@ 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];
|
||||
if (error) {
|
||||
int16_t n = config.errorCount + 1;
|
||||
config.errorCount = (n < 1 ? 1 : (n > 19 ? 19 : n)); // between: 2 sec and 6 days
|
||||
NSTimeInterval retryWaitTime = pow(2, config.errorCount); // 2^n seconds
|
||||
config.scheduled = [NSDate dateWithTimeIntervalSinceNow:retryWaitTime];
|
||||
// TODO: remove logging
|
||||
NSLog(@"Error loading: %@ (%d)", response.URL, config.errorCount);
|
||||
} else {
|
||||
config.errorCount = 0; // reset counter
|
||||
[self downloadSuccessful:data forFeed:config response:(NSHTTPURLResponse*)response];
|
||||
}
|
||||
[config.managedObjectContext.undoManager endUndoGrouping];
|
||||
dispatch_group_leave(group);
|
||||
[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
|
||||
NSTimeInterval retryWaitTime = pow(2, config.errorCount); // 2^n seconds
|
||||
config.scheduled = [NSDate dateWithTimeIntervalSinceNow:retryWaitTime];
|
||||
// TODO: remove logging
|
||||
NSLog(@"Error loading: %@ (%d)", response.URL, config.errorCount);
|
||||
} else {
|
||||
config.errorCount = 0; // reset counter
|
||||
[self downloadSuccessful:data forFeed:config response:(NSHTTPURLResponse*)response];
|
||||
}
|
||||
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");
|
||||
|
||||
@@ -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];
|
||||
}];
|
||||
[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];
|
||||
}];
|
||||
[item.managedObjectContext refreshObject:item mergeChanges:YES];
|
||||
[self.delegate modalDidUpdateFeedConfig:item];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO)
|
||||
updateAll.hidden = YES;
|
||||
- (void)networkChanged:(NSNotification*)notify {
|
||||
BOOL available = [[notify object] boolValue];
|
||||
[self.barItem.menu itemWithTag:TagUpdateFeed].enabled = available;
|
||||
[self updateBarIcon];
|
||||
}
|
||||
|
||||
[menu addItem:[NSMenuItem separatorItem]];
|
||||
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
|
||||
/**
|
||||
Callback method fired when feeds have been updated and the total unread count needs update.
|
||||
|
||||
self.unreadCountTotal = 0;
|
||||
@autoreleasepool {
|
||||
for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) {
|
||||
[menu addItem:[self generateMenuItem:fc unread:&_unreadCountTotal]];
|
||||
@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];
|
||||
}
|
||||
}
|
||||
[self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)];
|
||||
}
|
||||
|
||||
|
||||
#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)
|
||||
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]];
|
||||
|
||||
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;
|
||||
}
|
||||
if (i.unread && i.link.length > 0) {
|
||||
[urls addObject:[NSURL URLWithString:i.link]];
|
||||
i.unread = NO;
|
||||
++itemSum;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
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
|
||||
|
||||
@@ -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
|
||||
101
baRSS/Status Bar Menu/NSMenu+Ext.m
Normal file
101
baRSS/Status Bar Menu/NSMenu+Ext.m
Normal 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
|
||||
@@ -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
|
||||
217
baRSS/Status Bar Menu/NSMenuItem+Ext.m
Normal file
217
baRSS/Status Bar Menu/NSMenuItem+Ext.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user