Bugfix: initial empty list + errorCount + manual update

This commit is contained in:
relikd
2019-01-15 01:51:11 +01:00
parent c391bc0b39
commit 3d6865657b
9 changed files with 90 additions and 58 deletions

View File

@@ -86,7 +86,7 @@ ToDo
- [x] Code Documentation (mostly methods) - [x] Code Documentation (mostly methods)
- [ ] Add Sandboxing - [ ] Add Sandboxing
- [ ] Disable Startup checkbox (or other workaround) - [ ] 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 - [ ] Additional features

View File

@@ -22,13 +22,14 @@
#import "FeedGroup+CoreDataClass.h" #import "FeedGroup+CoreDataClass.h"
@interface FeedGroup (Ext)
/// Enum type to distinguish different @c FeedGroup types: @c GROUP, @c FEED, @c SEPARATOR /// Enum type to distinguish different @c FeedGroup types: @c GROUP, @c FEED, @c SEPARATOR
typedef NS_ENUM(int16_t, FeedGroupType) { typedef NS_ENUM(int16_t, FeedGroupType) {
/// Other types: @c GROUP, @c FEED, @c SEPARATOR /// Other types: @c GROUP, @c FEED, @c SEPARATOR
GROUP = 0, FEED = 1, SEPARATOR = 2 GROUP = 0, FEED = 1, SEPARATOR = 2
}; };
@interface FeedGroup (Ext)
/// Overwrites @c type attribute with enum. Use one of: @c GROUP, @c FEED, @c SEPARATOR. /// Overwrites @c type attribute with enum. Use one of: @c GROUP, @c FEED, @c SEPARATOR.
@property (nonatomic) FeedGroupType type; @property (nonatomic) FeedGroupType type;

View File

@@ -22,20 +22,22 @@
#import "FeedMeta+CoreDataClass.h" #import "FeedMeta+CoreDataClass.h"
@interface FeedMeta (Ext)
/// Easy memorable @c int16_t enum for refresh unit index /// Easy memorable @c int16_t enum for refresh unit index
typedef NS_ENUM(int16_t, RefreshUnitType) { typedef NS_ENUM(int16_t, RefreshUnitType) {
RefreshUnitSeconds = 0, RefreshUnitMinutes = 1, RefreshUnitHours = 2, RefreshUnitDays = 3, RefreshUnitWeeks = 4 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)setErrorAndPostponeSchedule;
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response; - (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response;
// Setter
- (void)setUrlIfChanged:(NSString*)url; - (void)setUrlIfChanged:(NSString*)url;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified; - (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit; - (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit;
- (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval; - (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval;
- (int32_t)refreshInterval;
- (NSString*)readableRefreshString;
@end @end

View File

@@ -29,6 +29,27 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd
@implementation FeedMeta (Ext) @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). /// Increment @c errorCount and set new @c scheduled date (2^N minutes, max. 5.7 days).
- (void)setErrorAndPostponeSchedule { - (void)setErrorAndPostponeSchedule {
if (self.errorCount < 0) 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) 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) NSTimeInterval retryWaitTime = pow(2, (n > 13 ? 13 : n)) * 60; // 2^N (between: 2 minutes and 5.7 days)
self.errorCount = n; self.errorCount = n;
self.scheduled = [NSDate dateWithTimeIntervalSinceNow:retryWaitTime]; [self scheduleNow:retryWaitTime];
// TODO: remove logging // TODO: remove logging
NSLog(@"ERROR: Feed download failed: %@ (errorCount: %d)", self.url, n); 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 self.errorCount = 0; // reset counter
NSDictionary *header = [response allHeaderFields]; NSDictionary *header = [response allHeaderFields];
[self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified" [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. #pragma mark - Setter
- (void)calculateAndSetScheduled {
NSTimeInterval interval = [self refreshInterval]; // 0 if refresh = 0 (update deactivated)
self.scheduled = (interval <= 0 ? nil : [[NSDate date] dateByAddingTimeInterval:interval]);
}
/// Set @c url attribute but only if value differs. /// Set @c url attribute but only if value differs.
- (void)setUrlIfChanged:(NSString*)url { - (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; 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. 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). 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 (self.refreshUnit != unit) self.refreshUnit = unit;
if (intervalChanged) { if (intervalChanged) {
[self calculateAndSetScheduled]; [self scheduleNow:[self refreshInterval]];
NSString *str = [self readableRefreshString]; NSString *str = [self readableRefreshString];
if (![self.feed.group.refreshStr isEqualToString:str]) if (![self.feed.group.refreshStr isEqualToString:str])
self.feed.group.refreshStr = 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 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' /// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
- (int32_t)refreshInterval { - (void)scheduleNow:(NSTimeInterval)future {
return self.refreshNum * RefreshUnitValues[self.refreshUnit % 5]; 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;
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h ) } else {
- (NSString*)readableRefreshString { self.scheduled = [NSDate dateWithTimeIntervalSinceNow:future];
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]]; }
} }
@end @end

View File

@@ -128,10 +128,10 @@ static BOOL _nextUpdateIsForced = NO;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
NSArray<Feed*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc]; NSArray<Feed*> *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<Feed*> *successful, NSArray<Feed*> *failed) { [FeedDownload batchUpdateFeeds:list showErrorAlert:NO finally:^(NSArray<Feed*> *successful, NSArray<Feed*> *failed) {
[self postChanges:successful andSaveContext:moc]; [self saveContext:moc andPostChanges:successful];
[moc reset]; [moc reset];
[self resumeUpdates]; // always reset the timer [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. 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<Feed*>*)changedFeeds andSaveContext:(NSManagedObjectContext*)moc { + (void)saveContext:(NSManagedObjectContext*)moc andPostChanges:(NSArray<Feed*>*)changedFeeds {
[StoreCoordinator saveContext:moc andParent:YES];
if (changedFeeds && changedFeeds.count > 0) { if (changedFeeds && changedFeeds.count > 0) {
[StoreCoordinator saveContext:moc andParent:YES];
NSArray<NSManagedObjectID*> *list = [changedFeeds valueForKeyPath:@"objectID"]; NSArray<NSManagedObjectID*> *list = [changedFeeds valueForKeyPath:@"objectID"];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:list]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:list];
return YES;
} }
return NO;
} }
@@ -279,7 +273,11 @@ static BOOL _nextUpdateIsForced = NO;
Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc]; Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc];
f.meta.url = url; f.meta.url = url;
[self batchDownloadRSSAndFavicons:@[f] showErrorAlert:YES rssFinished:^(NSArray<Feed *> *successful, BOOL *cancelFavicons) { [self batchDownloadRSSAndFavicons:@[f] showErrorAlert:YES rssFinished:^(NSArray<Feed *> *successful, BOOL *cancelFavicons) {
*cancelFavicons = ![self postChanges:successful andSaveContext:moc]; if (successful.count == 0) {
*cancelFavicons = YES;
} else {
[self saveContext:moc andPostChanges:successful];
}
} finally:^(BOOL successful) { } finally:^(BOOL successful) {
if (successful) { if (successful) {
[StoreCoordinator saveContext:moc andParent:YES]; [StoreCoordinator saveContext:moc andParent:YES];

View File

@@ -233,7 +233,7 @@
[child setAttribute:@"rss" forKey:OPMLTypeKey]; [child setAttribute:@"rss" forKey:OPMLTypeKey];
[child setAttribute:item.feed.link forKey:OPMLHMTLURLKey]; [child setAttribute:item.feed.link forKey:OPMLHMTLURLKey];
[child setAttribute:item.feed.meta.url forKey:OPMLXMLURLKey]; [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 [child setAttribute:refreshNum forKey:@"refreshInterval"]; // baRSS specific
} else { } else {
for (FeedGroup *subItem in [item sortedChildren]) { for (FeedGroup *subItem in [item sortedChildren]) {

View File

@@ -275,25 +275,22 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
/// Populate @c NSOutlineView data cells with core data object values. /// Populate @c NSOutlineView data cells with core data object values.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item { - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
FeedGroup *fg = [(NSTreeNode*)item representedObject]; FeedGroup *fg = [(NSTreeNode*)item representedObject];
BOOL isFeed = (fg.type == FEED);
BOOL isSeperator = (fg.type == SEPARATOR); BOOL isSeperator = (fg.type == SEPARATOR);
BOOL isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"]; 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")); NSString *cellIdent = (isRefreshColumn ? @"cellRefresh" : (isSeperator ? @"cellSeparator" : @"cellFeed"));
// owner is nil to prohibit repeated awakeFromNib calls // owner is nil to prohibit repeated awakeFromNib calls
NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil]; NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil];
if (isRefreshColumn) { 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) { } 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 { } else {
cellView.textField.objectValue = fg.name; cellView.textField.objectValue = fg.name;
cellView.imageView.image = (fg.type == GROUP ? [NSImage imageNamed:NSImageNameFolder] : [fg.feed iconImage16]); 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; return cellView;
} }

View File

@@ -36,6 +36,7 @@
@property (strong) NSStatusItem *barItem; @property (strong) NSStatusItem *barItem;
@property (strong) Preferences *prefWindow; @property (strong) Preferences *prefWindow;
@property (assign, atomic) NSInteger unreadCountTotal; @property (assign, atomic) NSInteger unreadCountTotal;
@property (assign) BOOL coreDataEmpty;
@property (weak) NSMenu *currentOpenMenu; @property (weak) NSMenu *currentOpenMenu;
@property (strong) NSArray<NSManagedObjectID*> *objectIDsForMenu; @property (strong) NSArray<NSManagedObjectID*> *objectIDsForMenu;
@property (strong) NSManagedObjectContext *readContext; @property (strong) NSManagedObjectContext *readContext;
@@ -67,7 +68,7 @@
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[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. /// Regardless of current unread count, perform new core data fetch on total unread count and update icon.
- (void)asyncReloadUnreadCountAndUpdateBarIcon { - (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. /// @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. /// Perform a core data fatch request, store sorted object ids array and return object count.
- (NSInteger)numberOfItemsInMenu:(NSMenu*)menu { - (NSInteger)numberOfItemsInMenu:(NSMenu*)menu {
NSMenuItem *parent = [menu.supermenu itemAtIndex:[menu.supermenu indexOfItemWithSubmenu:menu]]; [self prepareContextAndTemporaryObjectIDs:menu];
self.readContext = [StoreCoordinator createChildContext]; // will be deleted after menu:updateItem: if (_coreDataEmpty) return 1; // only if main menu empty
self.objectIDsForMenu = [StoreCoordinator sortedObjectIDsForParent:parent.representedObject isFeed:[menu isFeedMenu] inContext:self.readContext]; return (NSInteger)self.objectIDsForMenu.count;
return (NSInteger)[self.objectIDsForMenu count];
} }
/// Lazy populate system bar menus when needed. /// Lazy populate system bar menus when needed.
- (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel { - (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel {
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]]; id obj = [self.readContext objectWithID:[self.objectIDsForMenu objectAtIndex:(NSUInteger)index]];
if ([obj isKindOfClass:[FeedGroup class]]) { if ([obj isKindOfClass:[FeedGroup class]]) {
[item setFeedGroup:obj]; [item setFeedGroup:obj];
@@ -226,13 +232,28 @@
if (index + 1 == menu.numberOfItems) { // last item of the menu if (index + 1 == menu.numberOfItems) { // last item of the menu
[self finalizeMenu:menu object:obj]; [self finalizeMenu:menu object:obj];
self.objectIDsForMenu = nil; [self resetContextAndTemporaryObjectIDs];
[self.readContext reset];
self.readContext = nil;
} }
return YES; 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 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
/** /**

View File

@@ -92,11 +92,8 @@ typedef NS_ENUM(char, DisplaySetting) {
} else if (fg.type == GROUP && [UserPrefs defaultYES:@"groupUnreadCount"]) { } else if (fg.type == GROUP && [UserPrefs defaultYES:@"groupUnreadCount"]) {
uCount = [self.submenu coreDataUnreadCount]; uCount = [self.submenu coreDataUnreadCount];
} }
if (uCount > 0) { NSString *name = (fg.name ? fg.name : NSLocalizedString(@"(error)", nil));
self.title = [NSString stringWithFormat:@"%@ (%ld)", fg.name, uCount]; self.title = (uCount == 0 ? name : [NSString stringWithFormat:@"%@ (%ld)", name, uCount]);
} else {
self.title = (fg.name ? fg.name : @"(error)");
}
return uCount; return uCount;
} }