From 3d6865657bd66ead907b3d7a1458962a6e63f73e Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 15 Jan 2019 01:51:11 +0100 Subject: [PATCH] Bugfix: initial empty list + errorCount + manual update --- README.md | 2 +- baRSS/Categories/FeedGroup+Ext.h | 3 +- baRSS/Categories/FeedMeta+Ext.h | 12 ++--- baRSS/Categories/FeedMeta+Ext.m | 50 ++++++++++++++------- baRSS/FeedDownload.m | 20 ++++----- baRSS/Preferences/Feeds Tab/OpmlExport.m | 2 +- baRSS/Preferences/Feeds Tab/SettingsFeeds.m | 9 ++-- baRSS/Status Bar Menu/BarMenu.m | 43 +++++++++++++----- baRSS/Status Bar Menu/NSMenuItem+Ext.m | 7 +-- 9 files changed, 90 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 863e4d1..708ecb0 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ ToDo - [x] Code Documentation (mostly methods) - [ ] Add Sandboxing - [ ] Disable Startup checkbox (or other workaround) - - [ ] Fix nasty bug: empty feed list (initial state) + - [x] Fix nasty bug: empty feed list (initial state) - [ ] Additional features diff --git a/baRSS/Categories/FeedGroup+Ext.h b/baRSS/Categories/FeedGroup+Ext.h index 0a50ab4..bd27e9d 100644 --- a/baRSS/Categories/FeedGroup+Ext.h +++ b/baRSS/Categories/FeedGroup+Ext.h @@ -22,13 +22,14 @@ #import "FeedGroup+CoreDataClass.h" -@interface FeedGroup (Ext) /// Enum type to distinguish different @c FeedGroup types: @c GROUP, @c FEED, @c SEPARATOR typedef NS_ENUM(int16_t, FeedGroupType) { /// Other types: @c GROUP, @c FEED, @c SEPARATOR GROUP = 0, FEED = 1, SEPARATOR = 2 }; + +@interface FeedGroup (Ext) /// Overwrites @c type attribute with enum. Use one of: @c GROUP, @c FEED, @c SEPARATOR. @property (nonatomic) FeedGroupType type; diff --git a/baRSS/Categories/FeedMeta+Ext.h b/baRSS/Categories/FeedMeta+Ext.h index c3692f5..f8e9dcf 100644 --- a/baRSS/Categories/FeedMeta+Ext.h +++ b/baRSS/Categories/FeedMeta+Ext.h @@ -22,20 +22,22 @@ #import "FeedMeta+CoreDataClass.h" -@interface FeedMeta (Ext) /// Easy memorable @c int16_t enum for refresh unit index typedef NS_ENUM(int16_t, RefreshUnitType) { RefreshUnitSeconds = 0, RefreshUnitMinutes = 1, RefreshUnitHours = 2, RefreshUnitDays = 3, RefreshUnitWeeks = 4 }; + +@interface FeedMeta (Ext) +@property (readonly) BOOL refreshIntervalDisabled; // self.refreshNum <= 0 +@property (readonly) int32_t refreshInterval; // self.refreshNum * RefreshUnitValue + +// HTTP response - (void)setErrorAndPostponeSchedule; - (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response; - +// Setter - (void)setUrlIfChanged:(NSString*)url; - (void)setEtag:(NSString*)etag modified:(NSString*)modified; - (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit; - (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval; - -- (int32_t)refreshInterval; -- (NSString*)readableRefreshString; @end diff --git a/baRSS/Categories/FeedMeta+Ext.m b/baRSS/Categories/FeedMeta+Ext.m index e7a8ea1..beab2a9 100644 --- a/baRSS/Categories/FeedMeta+Ext.m +++ b/baRSS/Categories/FeedMeta+Ext.m @@ -29,6 +29,27 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd @implementation FeedMeta (Ext) +#pragma mark - Getter + +/// Check whether update interval is disabled by user (refresh interval is 0). +- (BOOL)refreshIntervalDisabled { + return (self.refreshNum <= 0); +} + +/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m' +- (int32_t)refreshInterval { + return self.refreshNum * RefreshUnitValues[self.refreshUnit % 5]; +} + +/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h ) +- (NSString*)readableRefreshString { + if (self.refreshIntervalDisabled) + return @"∞"; // ∞ ƒ Ø + return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]]; +} + +#pragma mark - HTTP response + /// Increment @c errorCount and set new @c scheduled date (2^N minutes, max. 5.7 days). - (void)setErrorAndPostponeSchedule { if (self.errorCount < 0) @@ -36,7 +57,7 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd int16_t n = self.errorCount + 1; // always increment errorCount (can be used to indicate bad feeds) NSTimeInterval retryWaitTime = pow(2, (n > 13 ? 13 : n)) * 60; // 2^N (between: 2 minutes and 5.7 days) self.errorCount = n; - self.scheduled = [NSDate dateWithTimeIntervalSinceNow:retryWaitTime]; + [self scheduleNow:retryWaitTime]; // TODO: remove logging NSLog(@"ERROR: Feed download failed: %@ (errorCount: %d)", self.url, n); } @@ -45,14 +66,10 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd self.errorCount = 0; // reset counter NSDictionary *header = [response allHeaderFields]; [self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified" - [self calculateAndSetScheduled]; + [self scheduleNow:[self refreshInterval]]; } -/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update. -- (void)calculateAndSetScheduled { - NSTimeInterval interval = [self refreshInterval]; // 0 if refresh = 0 (update deactivated) - self.scheduled = (interval <= 0 ? nil : [[NSDate date] dateByAddingTimeInterval:interval]); -} +#pragma mark - Setter /// Set @c url attribute but only if value differs. - (void)setUrlIfChanged:(NSString*)url { @@ -65,7 +82,6 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd if (![self.modified isEqualToString:modified]) self.modified = modified; } - /** Set @c refresh and @c unit from popup button selection. Only values that differ will be updated. Also, calculate and set new @c scheduled date and update FeedGroup @c refreshStr (if changed). @@ -78,7 +94,7 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd if (self.refreshUnit != unit) self.refreshUnit = unit; if (intervalChanged) { - [self calculateAndSetScheduled]; + [self scheduleNow:[self refreshInterval]]; NSString *str = [self readableRefreshString]; if (![self.feed.group.refreshStr isEqualToString:str]) self.feed.group.refreshStr = str; @@ -102,14 +118,14 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd return NO; // since loop didn't return, no value was changed } -/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m' -- (int32_t)refreshInterval { - return self.refreshNum * RefreshUnitValues[self.refreshUnit % 5]; -} - -/// @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]]; +/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update. +- (void)scheduleNow:(NSTimeInterval)future { + if (self.refreshIntervalDisabled) { // update deactivated; manually update with force update all + if (self.scheduled != nil) // already nil? Avoid unnecessary core data edits + self.scheduled = nil; + } else { + self.scheduled = [NSDate dateWithTimeIntervalSinceNow:future]; + } } @end diff --git a/baRSS/FeedDownload.m b/baRSS/FeedDownload.m index 2f6b1bf..3e30495 100644 --- a/baRSS/FeedDownload.m +++ b/baRSS/FeedDownload.m @@ -128,10 +128,10 @@ static BOOL _nextUpdateIsForced = NO; NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSArray *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc]; - NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); + //NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); [FeedDownload batchUpdateFeeds:list showErrorAlert:NO finally:^(NSArray *successful, NSArray *failed) { - [self postChanges:successful andSaveContext:moc]; + [self saveContext:moc andPostChanges:successful]; [moc reset]; [self resumeUpdates]; // always reset the timer }]; @@ -139,19 +139,13 @@ static BOOL _nextUpdateIsForced = NO; /** Perform save on context and all parents. Then post @c FeedUpdated notification. - Use return value to download additional data. - - @return @c YES if @c (list.count @c > @c 0). - Return @c NO if context wasn't saved, and no notification was sent. */ -+ (BOOL)postChanges:(NSArray*)changedFeeds andSaveContext:(NSManagedObjectContext*)moc { ++ (void)saveContext:(NSManagedObjectContext*)moc andPostChanges:(NSArray*)changedFeeds { + [StoreCoordinator saveContext:moc andParent:YES]; if (changedFeeds && changedFeeds.count > 0) { - [StoreCoordinator saveContext:moc andParent:YES]; NSArray *list = [changedFeeds valueForKeyPath:@"objectID"]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:list]; - return YES; } - return NO; } @@ -279,7 +273,11 @@ static BOOL _nextUpdateIsForced = NO; Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc]; f.meta.url = url; [self batchDownloadRSSAndFavicons:@[f] showErrorAlert:YES rssFinished:^(NSArray *successful, BOOL *cancelFavicons) { - *cancelFavicons = ![self postChanges:successful andSaveContext:moc]; + if (successful.count == 0) { + *cancelFavicons = YES; + } else { + [self saveContext:moc andPostChanges:successful]; + } } finally:^(BOOL successful) { if (successful) { [StoreCoordinator saveContext:moc andParent:YES]; diff --git a/baRSS/Preferences/Feeds Tab/OpmlExport.m b/baRSS/Preferences/Feeds Tab/OpmlExport.m index e297984..f59fcac 100644 --- a/baRSS/Preferences/Feeds Tab/OpmlExport.m +++ b/baRSS/Preferences/Feeds Tab/OpmlExport.m @@ -233,7 +233,7 @@ [child setAttribute:@"rss" forKey:OPMLTypeKey]; [child setAttribute:item.feed.link forKey:OPMLHMTLURLKey]; [child setAttribute:item.feed.meta.url forKey:OPMLXMLURLKey]; - NSNumber *refreshNum = [NSNumber numberWithInteger:[item.feed.meta refreshInterval]]; + NSNumber *refreshNum = [NSNumber numberWithInteger:item.feed.meta.refreshInterval]; [child setAttribute:refreshNum forKey:@"refreshInterval"]; // baRSS specific } else { for (FeedGroup *subItem in [item sortedChildren]) { diff --git a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m index defd64e..f51ceb0 100644 --- a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m @@ -275,25 +275,22 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; /// Populate @c NSOutlineView data cells with core data object values. - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item { FeedGroup *fg = [(NSTreeNode*)item representedObject]; - BOOL isFeed = (fg.type == FEED); BOOL isSeperator = (fg.type == SEPARATOR); BOOL isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"]; - BOOL refreshDisabled = (!isFeed || fg.refreshStr.length == 0 || [fg.refreshStr characterAtIndex:0] == '0'); NSString *cellIdent = (isRefreshColumn ? @"cellRefresh" : (isSeperator ? @"cellSeparator" : @"cellFeed")); // owner is nil to prohibit repeated awakeFromNib calls NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil]; if (isRefreshColumn) { - cellView.textField.stringValue = (refreshDisabled ? (isFeed ? @"--" : @"") : fg.refreshStr); + cellView.textField.objectValue = fg.refreshStr; + cellView.textField.textColor = (fg.refreshStr.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]); } else if (isSeperator) { - return cellView; // the refresh cell is already skipped with the above if condition + return cellView; // refresh cell already skipped with the above if condition } else { cellView.textField.objectValue = fg.name; cellView.imageView.image = (fg.type == GROUP ? [NSImage imageNamed:NSImageNameFolder] : [fg.feed iconImage16]); } - // also for refresh column - cellView.textField.textColor = (isFeed && refreshDisabled ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]); return cellView; } diff --git a/baRSS/Status Bar Menu/BarMenu.m b/baRSS/Status Bar Menu/BarMenu.m index c8124fb..b6e9a44 100644 --- a/baRSS/Status Bar Menu/BarMenu.m +++ b/baRSS/Status Bar Menu/BarMenu.m @@ -36,6 +36,7 @@ @property (strong) NSStatusItem *barItem; @property (strong) Preferences *prefWindow; @property (assign, atomic) NSInteger unreadCountTotal; +@property (assign) BOOL coreDataEmpty; @property (weak) NSMenu *currentOpenMenu; @property (strong) NSArray *objectIDsForMenu; @property (strong) NSManagedObjectContext *readContext; @@ -67,7 +68,7 @@ [[NSNotificationCenter defaultCenter] removeObserver:self]; } -#pragma mark - Update Menu Bar Icon - +#pragma mark - Update Menu Bar Icon /// Regardless of current unread count, perform new core data fetch on total unread count and update icon. - (void)asyncReloadUnreadCountAndUpdateBarIcon { @@ -96,7 +97,7 @@ } -#pragma mark - Notification callback methods - +#pragma mark - Notification callback methods /** @@ -180,7 +181,7 @@ } -#pragma mark - Menu Delegate & Menu Generation - +#pragma mark - Menu Delegate & Menu Generation /// @c currentOpenMenu is needed when a background update occurs. In case a feed items menu is open. @@ -206,14 +207,19 @@ /// Perform a core data fatch request, store sorted object ids array and return object count. - (NSInteger)numberOfItemsInMenu:(NSMenu*)menu { - NSMenuItem *parent = [menu.supermenu itemAtIndex:[menu.supermenu indexOfItemWithSubmenu:menu]]; - self.readContext = [StoreCoordinator createChildContext]; // will be deleted after menu:updateItem: - self.objectIDsForMenu = [StoreCoordinator sortedObjectIDsForParent:parent.representedObject isFeed:[menu isFeedMenu] inContext:self.readContext]; - return (NSInteger)[self.objectIDsForMenu count]; + [self prepareContextAndTemporaryObjectIDs:menu]; + if (_coreDataEmpty) return 1; // only if main menu empty + return (NSInteger)self.objectIDsForMenu.count; } /// Lazy populate system bar menus when needed. - (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel { + if (_coreDataEmpty) { + item.title = NSLocalizedString(@"~~~ list empty ~~~", nil); + item.enabled = NO; + [self finalizeMenu:menu object:nil]; + return YES; + } id obj = [self.readContext objectWithID:[self.objectIDsForMenu objectAtIndex:(NSUInteger)index]]; if ([obj isKindOfClass:[FeedGroup class]]) { [item setFeedGroup:obj]; @@ -226,13 +232,28 @@ if (index + 1 == menu.numberOfItems) { // last item of the menu [self finalizeMenu:menu object:obj]; - self.objectIDsForMenu = nil; - [self.readContext reset]; - self.readContext = nil; + [self resetContextAndTemporaryObjectIDs]; } return YES; } + +#pragma mark - Helper + + +- (void)prepareContextAndTemporaryObjectIDs:(NSMenu*)menu { + NSMenuItem *parent = [menu.supermenu itemAtIndex:[menu.supermenu indexOfItemWithSubmenu:menu]]; + self.readContext = [StoreCoordinator createChildContext]; // will be deleted after menu:updateItem: + self.objectIDsForMenu = [StoreCoordinator sortedObjectIDsForParent:parent.representedObject isFeed:[menu isFeedMenu] inContext:self.readContext]; + _coreDataEmpty = ([menu isMainMenu] && self.objectIDsForMenu.count == 0); // initial state or no feeds in date store +} + +- (void)resetContextAndTemporaryObjectIDs { + self.objectIDsForMenu = nil; + [self.readContext reset]; + self.readContext = nil; +} + /** Add default menu items that are present in each menu as header and disable menu items if necessary */ @@ -301,7 +322,7 @@ } -#pragma mark - Menu Actions - +#pragma mark - Menu Actions /** diff --git a/baRSS/Status Bar Menu/NSMenuItem+Ext.m b/baRSS/Status Bar Menu/NSMenuItem+Ext.m index 1e6ac0c..2695ec4 100644 --- a/baRSS/Status Bar Menu/NSMenuItem+Ext.m +++ b/baRSS/Status Bar Menu/NSMenuItem+Ext.m @@ -92,11 +92,8 @@ typedef NS_ENUM(char, DisplaySetting) { } else if (fg.type == GROUP && [UserPrefs defaultYES:@"groupUnreadCount"]) { uCount = [self.submenu coreDataUnreadCount]; } - if (uCount > 0) { - self.title = [NSString stringWithFormat:@"%@ (%ld)", fg.name, uCount]; - } else { - self.title = (fg.name ? fg.name : @"(error)"); - } + NSString *name = (fg.name ? fg.name : NSLocalizedString(@"(error)", nil)); + self.title = (uCount == 0 ? name : [NSString stringWithFormat:@"%@ (%ld)", name, uCount]); return uCount; }