From c30d6b82edb8ea5cc7afdda63132a34a6959bb75 Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 11 Jan 2026 21:35:30 +0100 Subject: [PATCH] feat: "Update feeds" for all menu levels --- baRSS/Core Data/StoreCoordinator.h | 3 +- baRSS/Core Data/StoreCoordinator.m | 20 ++++++-- baRSS/Feed Import/UpdateScheduler.h | 2 +- baRSS/Feed Import/UpdateScheduler.m | 49 +++++++++---------- baRSS/Helper/UserPrefs.h | 2 + baRSS/Helper/UserPrefs.m | 2 +- .../Appearance Tab/SettingsAppearanceView.m | 10 ++-- baRSS/Status Bar Menu/BarStatusItem.m | 17 +------ baRSS/Status Bar Menu/NSMenu+Ext.h | 1 + baRSS/Status Bar Menu/NSMenu+Ext.m | 21 ++++++++ 10 files changed, 74 insertions(+), 53 deletions(-) diff --git a/baRSS/Core Data/StoreCoordinator.h b/baRSS/Core Data/StoreCoordinator.h index 0a7a8ce..308a272 100644 --- a/baRSS/Core Data/StoreCoordinator.h +++ b/baRSS/Core Data/StoreCoordinator.h @@ -15,7 +15,8 @@ NS_ASSUME_NONNULL_BEGIN // Feed update + (NSDate*)nextScheduledUpdate; -+ (NSArray*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(nullable NSManagedObjectContext*)moc; ++ (NSArray*)feedsThatNeedUpdate:(nullable NSManagedObjectContext*)moc; ++ (NSArray*)feedsWithIndexPath:(nullable NSString*)path inContext:(nullable NSManagedObjectContext*)moc; // Count elements + (BOOL)isEmpty; diff --git a/baRSS/Core Data/StoreCoordinator.m b/baRSS/Core Data/StoreCoordinator.m index 298d1fb..3860c6d 100644 --- a/baRSS/Core Data/StoreCoordinator.m +++ b/baRSS/Core Data/StoreCoordinator.m @@ -79,14 +79,24 @@ /** List of @c Feed items that need to be updated. Scheduled time is now (or in past). - @param forceAll If @c YES get a list of all @c Feed regardless of schedules time. @param moc If @c nil perform requests on main context (ok for reading). */ -+ (NSArray*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(nullable NSManagedObjectContext*)moc { ++ (NSArray*)feedsThatNeedUpdate:(nullable NSManagedObjectContext*)moc { NSFetchRequest *fr = [Feed fetchRequest]; - if (!forceAll) { - // when fetching also get those feeds that would need update soon (now + 2s) - [fr where:@"meta.scheduled <= %@", [NSDate dateWithTimeIntervalSinceNow:+2]]; + // when fetching also get those feeds that would need update soon (now + 2s) + [fr where:@"meta.scheduled <= %@", [NSDate dateWithTimeIntervalSinceNow:+2]]; + return [fr fetchAllRows:moc ? moc : [self getMainContext]]; +} + +/** List of @c Feed items that match @c Feed.indexPath either by direct match or some child thereof. + + @param path If @c nil return all @c Feed items. May match either full string OR startswith string + "." + @param moc If @c nil perform requests on main context (ok for reading). + */ ++ (NSArray*)feedsWithIndexPath:(nullable NSString*)path inContext:(nullable NSManagedObjectContext*)moc { + NSFetchRequest *fr = [Feed fetchRequest]; + if (path && path.length > 0) { + [fr where:@"indexPath = %@ OR indexPath BEGINSWITH %@", path, [path stringByAppendingString:@"."]]; } return [fr fetchAllRows:moc ? moc : [self getMainContext]]; } diff --git a/baRSS/Feed Import/UpdateScheduler.h b/baRSS/Feed Import/UpdateScheduler.h index 38b869c..e87755a 100644 --- a/baRSS/Feed Import/UpdateScheduler.h +++ b/baRSS/Feed Import/UpdateScheduler.h @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSString*)updatingXFeeds; // Scheduling + (void)scheduleNextFeed; -+ (void)forceUpdateAllFeeds; ++ (void)forceUpdate:(NSString*)indexPath; + (void)downloadList:(NSArray*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block; + (void)updateAllFavicons; // Auto Download & Parse Feed URL diff --git a/baRSS/Feed Import/UpdateScheduler.m b/baRSS/Feed Import/UpdateScheduler.m index 775f88b..28af29f 100644 --- a/baRSS/Feed Import/UpdateScheduler.m +++ b/baRSS/Feed Import/UpdateScheduler.m @@ -18,7 +18,6 @@ static NSTimer *_timer; static SCNetworkReachabilityRef _reachability = NULL; static BOOL _isReachable = YES; static BOOL _updatePaused = NO; -static BOOL _nextUpdateIsForced = NO; static _Atomic(NSUInteger) _queueSize = 0; @implementation UpdateScheduler @@ -90,14 +89,9 @@ static _Atomic(NSUInteger) _queueSize = 0; nextTime = [NSDate dateWithTimeIntervalSinceNow:1]; } [self scheduleTimer:nextTime]; -} - -/// Start download of all feeds (immediatelly) regardless of @c .scheduled property. -+ (void)forceUpdateAllFeeds { - if (![self allowNetworkConnection]) // timer will restart once connection exists - return; - _nextUpdateIsForced = YES; - [self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.05]]; +#ifdef DEBUG + NSLog(@"schedule next update: %@", nextTime); +#endif } /** @@ -116,13 +110,9 @@ static _Atomic(NSUInteger) _queueSize = 0; if (!nextTime) nextTime = [NSDate distantFuture]; int tolerance = (int)([nextTime timeIntervalSinceNow] * 0.15); - tolerance = (tolerance < 1 ? 1 : tolerance > 600 ? 600 : tolerance); // at least 1 sec, upto 10 min - _timer.tolerance = tolerance; + _timer.tolerance = (tolerance < 1 ? 1 : tolerance > 600 ? 600 : tolerance); // at least 1 sec, upto 10 min _timer.fireDate = nextTime; PostNotification(kNotificationScheduleTimerChanged, nil); -#ifdef DEBUG - NSLog(@"schedule timer: %@ (+/- %d sec)", nextTime, tolerance); -#endif } + (void)didWakeAfterSleep { @@ -134,17 +124,26 @@ static _Atomic(NSUInteger) _queueSize = 0; /// Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user. + (void)updateTimerCallback { -#ifdef DEBUG - NSLog(@"fired"); -#endif - BOOL updateAll = _nextUpdateIsForced; - _nextUpdateIsForced = NO; - NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; - NSArray *list = [StoreCoordinator listOfFeedsThatNeedUpdate:updateAll inContext:moc]; - //NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); - - [self downloadList:list userInitiated:updateAll notifications:YES finally:^{ + NSArray *list = [StoreCoordinator feedsThatNeedUpdate:moc]; + [self update:list userInitiated:NO context:moc]; +} + +/// Start download of feeds immediatelly, regardless of @c .scheduled property. ++ (void)forceUpdate:(NSString*)indexPath { + if (![self allowNetworkConnection]) // menu item should be disabled anyway + return; + NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; + NSArray *list = [StoreCoordinator feedsWithIndexPath:indexPath inContext:moc]; + [self update:list userInitiated:YES context:moc]; +} + +/// Helper method for actual download ++ (void)update:(NSArray*)list userInitiated:(BOOL)flag context:(NSManagedObjectContext*)moc { +#ifdef DEBUG + NSLog(@"updating feeds: %ld (%@)", list.count, flag ? @"forced" : @"scheduled"); +#endif + [self downloadList:list userInitiated:flag notifications:YES finally:^{ [StoreCoordinator saveContext:moc andParent:YES]; // save parents too ... [moc reset]; [self scheduleNextFeed]; // always reset the timer @@ -157,7 +156,7 @@ static _Atomic(NSUInteger) _queueSize = 0; /// Perform @c FaviconDownload on all core data @c Feed entries. + (void)updateAllFavicons { - for (Feed *f in [StoreCoordinator listOfFeedsThatNeedUpdate:YES inContext:nil]) + for (Feed *f in [StoreCoordinator feedsWithIndexPath:nil inContext:nil]) [FaviconDownload updateFeed:f finally:nil]; } diff --git a/baRSS/Helper/UserPrefs.h b/baRSS/Helper/UserPrefs.h index 8153a7c..b0c42b3 100644 --- a/baRSS/Helper/UserPrefs.h +++ b/baRSS/Helper/UserPrefs.h @@ -18,6 +18,8 @@ // menu buttons /** default: @c NO */ static NSString* const Pref_globalToggleHidden = @"globalToggleHidden"; /** default: @c YES */ static NSString* const Pref_globalUpdateAll = @"globalUpdateAll"; +/** default: @c YES */ static NSString* const Pref_groupUpdateAll = @"groupUpdateAll"; +/** default: @c YES */ static NSString* const Pref_feedUpdateAll = @"feedUpdateAll"; /** default: @c YES */ static NSString* const Pref_globalOpenUnread = @"globalOpenUnread"; /** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread"; /** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread"; diff --git a/baRSS/Helper/UserPrefs.m b/baRSS/Helper/UserPrefs.m index 399a4f8..207f776 100644 --- a/baRSS/Helper/UserPrefs.m +++ b/baRSS/Helper/UserPrefs.m @@ -12,7 +12,7 @@ void UserPrefsInit(void) { NSMutableDictionary *defs = [NSMutableDictionary dictionary]; defaultsAppend(defs, @YES, @[ Pref_globalTintMenuIcon, - Pref_globalUpdateAll, + Pref_globalUpdateAll, Pref_groupUpdateAll, Pref_feedUpdateAll, Pref_globalOpenUnread, Pref_groupOpenUnread, Pref_feedOpenUnread, Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead, Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread, diff --git a/baRSS/Preferences/Appearance Tab/SettingsAppearanceView.m b/baRSS/Preferences/Appearance Tab/SettingsAppearanceView.m index d24e2b5..328f524 100644 --- a/baRSS/Preferences/Appearance Tab/SettingsAppearanceView.m +++ b/baRSS/Preferences/Appearance Tab/SettingsAppearanceView.m @@ -54,11 +54,6 @@ tip:NSLocalizedString(@"You can hold down option-key before opening the main menu to temporarily show all hidden entries.", nil) c1:Pref_globalToggleHidden c2:nil c3:nil c4:nil]; - [self entry:NSLocalizedString(@"“Update all feeds”", nil) - help:NSLocalizedString(@"Show button to reload all feeds. This will force fetch new online content regardless of next-update timer.", nil) - tip:nil - c1:Pref_globalUpdateAll c2:nil c3:nil c4:nil]; - [self entry:NSLocalizedString(@"“Open all unread”", nil) help:NSLocalizedString(@"Show button to open unread articles.", nil) tip:nil @@ -74,6 +69,11 @@ tip:NSLocalizedString(@"Alternatively, you can hold down option-key and click on an article to toggle that item (un-)read.", nil) c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread c4:nil]; + [self entry:NSLocalizedString(@"“Update feeds”", nil) + help:NSLocalizedString(@"Show button to reload all feeds. This will force fetch new online content regardless of next-update timer.", nil) + tip:nil + c1:Pref_globalUpdateAll c2:Pref_groupUpdateAll c3:Pref_feedUpdateAll c4:nil]; + // self.y += PAD_M; [self intInput:Pref_openFewLinksLimit unit:NSLocalizedString(@"%ld unread", nil) diff --git a/baRSS/Status Bar Menu/BarStatusItem.m b/baRSS/Status Bar Menu/BarStatusItem.m index 583f144..f93da81 100644 --- a/baRSS/Status Bar Menu/BarStatusItem.m +++ b/baRSS/Status Bar Menu/BarStatusItem.m @@ -8,12 +8,12 @@ #import "NotifyEndpoint.h" #import "NSView+Ext.h" #import "NSColor+Ext.h" +#import "NSMenu+Ext.h" @interface BarStatusItem() @property (strong) BarMenu *barMenu; @property (strong) NSStatusItem *statusItem; @property (assign) NSInteger unreadCountTotal; -@property (weak) NSMenuItem *updateAllItem; /// Set to `true` if user toggled the `"Show hidden feeds"` menu option. @property (assign) BOOL optShowHidden; /// Set to `true` if menu bar was opened while holding down option-key. @@ -49,8 +49,8 @@ /// Fired when network conditions change. - (void)networkChanged:(NSNotification*)notify { BOOL available = [[notify object] boolValue]; - self.updateAllItem.enabled = available; [self updateBarIcon]; + [self.statusItem.menu recursiveSetNetworkAvailable:available]; } /// Fired when a single feed has been updated. Object contains relative unread count change. @@ -197,13 +197,6 @@ } } - // 'Update all feeds' item - if (UserPrefsBool(Pref_globalUpdateAll)) { - NSMenuItem *updateAll = [menu addItemWithTitle:NSLocalizedString(@"Update all feeds", nil) action:@selector(updateAllFeeds) keyEquivalent:@""]; - updateAll.target = self; - updateAll.enabled = [UpdateScheduler allowNetworkConnection]; - self.updateAllItem = updateAll; - } // Separator between main header and default header [menu addItem:[NSMenuItem separatorItem]]; } @@ -220,10 +213,4 @@ self.barMenu.showHidden = self.optShowHidden; } -/// Called when user clicks on `Update all feeds` (main menu only). -- (void)updateAllFeeds { -// [self asyncReloadUnreadCount]; // should not be necessary - [UpdateScheduler forceUpdateAllFeeds]; -} - @end diff --git a/baRSS/Status Bar Menu/NSMenu+Ext.h b/baRSS/Status Bar Menu/NSMenu+Ext.h index d5778f8..1aee87f 100644 --- a/baRSS/Status Bar Menu/NSMenu+Ext.h +++ b/baRSS/Status Bar Menu/NSMenu+Ext.h @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)insertDefaultHeader; // Update menu - (void)setHeaderHasUnread:(UnreadTotal*)count; +- (void)recursiveSetNetworkAvailable:(BOOL)flag; - (nullable NSMenuItem*)deepestItemWithPath:(nonnull NSString*)path; @end diff --git a/baRSS/Status Bar Menu/NSMenu+Ext.m b/baRSS/Status Bar Menu/NSMenu+Ext.m index 51ff3a4..3af7bbf 100644 --- a/baRSS/Status Bar Menu/NSMenu+Ext.m +++ b/baRSS/Status Bar Menu/NSMenu+Ext.m @@ -6,12 +6,14 @@ #import "Constants.h" #import "MapUnreadTotal.h" #import "NotifyEndpoint.h" +#import "UpdateScheduler.h" typedef NS_ENUM(NSInteger, MenuItemTag) { /// Used in @c allowDisplayOfHeaderItem: to identify and enable items TagMarkAllRead = 1, TagMarkAllUnread = 2, TagOpenAllUnread = 3, + TagUpdateFeeds = 4, /// Delimiter item between default header and core data items TagHeaderDelimiter = 8, /// Indicator whether unread count is currently shown in menu item title or not @@ -83,6 +85,9 @@ typedef NS_ENUM(NSInteger, MenuItemTag) { } [self addItemIfAllowed:TagMarkAllRead title:NSLocalizedString(@"Mark all read", nil)]; [self addItemIfAllowed:TagMarkAllUnread title:NSLocalizedString(@"Mark all unread", nil)]; + [self addItemIfAllowed:TagUpdateFeeds title:self.isFeedMenu ? NSLocalizedString(@"Update feed", nil) : NSLocalizedString(@"Update feeds", nil)] + .enabled = [UpdateScheduler allowNetworkConnection]; + if (self.numberOfItems > 0) { // in case someone has disabled all header items. Else, during articles menu rebuild it will stay on top. NSMenuItem *sep = [NSMenuItem separatorItem]; @@ -112,6 +117,16 @@ typedef NS_ENUM(NSInteger, MenuItemTag) { } } +/// Call this method whenever network availability changes to mark "Update feeds" button en-/disabled. +- (void)recursiveSetNetworkAvailable:(BOOL)flag { + [self itemWithTag:TagUpdateFeeds].enabled = flag; + for (NSMenuItem *item in self.itemArray) { + if (item.hasSubmenu) { + [item.submenu recursiveSetNetworkAvailable:flag]; + } + } +} + /** Iterate over all menu items in @c self.itemArray and find the item where @c submenu.title matches the first @c sortIndex in @c path. Recursively repeat the process for the items of this submenu and so on. @@ -141,11 +156,13 @@ typedef NS_ENUM(NSInteger, MenuItemTag) { static NSString* const mr[] = {Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead}; static NSString* const mu[] = {Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread}; static NSString* const ou[] = {Pref_globalOpenUnread, Pref_groupOpenUnread, Pref_feedOpenUnread}; + static NSString* const ua[] = {Pref_globalUpdateAll, Pref_groupUpdateAll, Pref_feedUpdateAll}; int i = (self.supermenu == nil ? 0 : (self.isFeedMenu ? 2 : 1)); switch (tag) { case TagMarkAllRead: return UserPrefsBool(mr[i]); case TagMarkAllUnread: return UserPrefsBool(mu[i]); case TagOpenAllUnread: return UserPrefsBool(ou[i]); + case TagUpdateFeeds: return UserPrefsBool(ua[i]); default: return NO; } } @@ -165,6 +182,10 @@ typedef NS_ENUM(NSInteger, MenuItemTag) { /// Prepare @c userInfo dictionary and send @c NSNotification. Callback for every default header menu item. + (void)headerMenuItemCallback:(NSMenuItem*)sender { + if (sender.tag == TagUpdateFeeds) { + [UpdateScheduler forceUpdate:sender.menu.titleIndexPath]; + return; + } BOOL openLinks = NO; NSUInteger limit = 0; if (sender.tag == TagOpenAllUnread) {