feat: "Update feeds" for all menu levels

This commit is contained in:
relikd
2026-01-11 21:35:30 +01:00
parent 26f95c2b13
commit c30d6b82ed
10 changed files with 74 additions and 53 deletions

View File

@@ -15,7 +15,8 @@ NS_ASSUME_NONNULL_BEGIN
// Feed update // Feed update
+ (NSDate*)nextScheduledUpdate; + (NSDate*)nextScheduledUpdate;
+ (NSArray<Feed*>*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(nullable NSManagedObjectContext*)moc; + (NSArray<Feed*>*)feedsThatNeedUpdate:(nullable NSManagedObjectContext*)moc;
+ (NSArray<Feed*>*)feedsWithIndexPath:(nullable NSString*)path inContext:(nullable NSManagedObjectContext*)moc;
// Count elements // Count elements
+ (BOOL)isEmpty; + (BOOL)isEmpty;

View File

@@ -79,14 +79,24 @@
/** /**
List of @c Feed items that need to be updated. Scheduled time is now (or in past). 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). @param moc If @c nil perform requests on main context (ok for reading).
*/ */
+ (NSArray<Feed*>*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(nullable NSManagedObjectContext*)moc { + (NSArray<Feed*>*)feedsThatNeedUpdate:(nullable NSManagedObjectContext*)moc {
NSFetchRequest *fr = [Feed fetchRequest]; NSFetchRequest *fr = [Feed fetchRequest];
if (!forceAll) { // when fetching also get those feeds that would need update soon (now + 2s)
// when fetching also get those feeds that would need update soon (now + 2s) [fr where:@"meta.scheduled <= %@", [NSDate dateWithTimeIntervalSinceNow:+2]];
[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<Feed*>*)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]]; return [fr fetchAllRows:moc ? moc : [self getMainContext]];
} }

View File

@@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString*)updatingXFeeds; + (NSString*)updatingXFeeds;
// Scheduling // Scheduling
+ (void)scheduleNextFeed; + (void)scheduleNextFeed;
+ (void)forceUpdateAllFeeds; + (void)forceUpdate:(NSString*)indexPath;
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block; + (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block;
+ (void)updateAllFavicons; + (void)updateAllFavicons;
// Auto Download & Parse Feed URL // Auto Download & Parse Feed URL

View File

@@ -18,7 +18,6 @@ static NSTimer *_timer;
static SCNetworkReachabilityRef _reachability = NULL; static SCNetworkReachabilityRef _reachability = NULL;
static BOOL _isReachable = YES; static BOOL _isReachable = YES;
static BOOL _updatePaused = NO; static BOOL _updatePaused = NO;
static BOOL _nextUpdateIsForced = NO;
static _Atomic(NSUInteger) _queueSize = 0; static _Atomic(NSUInteger) _queueSize = 0;
@implementation UpdateScheduler @implementation UpdateScheduler
@@ -90,14 +89,9 @@ static _Atomic(NSUInteger) _queueSize = 0;
nextTime = [NSDate dateWithTimeIntervalSinceNow:1]; nextTime = [NSDate dateWithTimeIntervalSinceNow:1];
} }
[self scheduleTimer:nextTime]; [self scheduleTimer:nextTime];
} #ifdef DEBUG
NSLog(@"schedule next update: %@", nextTime);
/// Start download of all feeds (immediatelly) regardless of @c .scheduled property. #endif
+ (void)forceUpdateAllFeeds {
if (![self allowNetworkConnection]) // timer will restart once connection exists
return;
_nextUpdateIsForced = YES;
[self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.05]];
} }
/** /**
@@ -116,13 +110,9 @@ static _Atomic(NSUInteger) _queueSize = 0;
if (!nextTime) if (!nextTime)
nextTime = [NSDate distantFuture]; nextTime = [NSDate distantFuture];
int tolerance = (int)([nextTime timeIntervalSinceNow] * 0.15); 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 < 1 ? 1 : tolerance > 600 ? 600 : tolerance); // at least 1 sec, upto 10 min
_timer.tolerance = tolerance;
_timer.fireDate = nextTime; _timer.fireDate = nextTime;
PostNotification(kNotificationScheduleTimerChanged, nil); PostNotification(kNotificationScheduleTimerChanged, nil);
#ifdef DEBUG
NSLog(@"schedule timer: %@ (+/- %d sec)", nextTime, tolerance);
#endif
} }
+ (void)didWakeAfterSleep { + (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. /// Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user.
+ (void)updateTimerCallback { + (void)updateTimerCallback {
#ifdef DEBUG
NSLog(@"fired");
#endif
BOOL updateAll = _nextUpdateIsForced;
_nextUpdateIsForced = NO;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
NSArray<Feed*> *list = [StoreCoordinator listOfFeedsThatNeedUpdate:updateAll inContext:moc]; NSArray<Feed*> *list = [StoreCoordinator feedsThatNeedUpdate:moc];
//NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); [self update:list userInitiated:NO context:moc];
}
[self downloadList:list userInitiated:updateAll notifications:YES finally:^{
/// 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<Feed*> *list = [StoreCoordinator feedsWithIndexPath:indexPath inContext:moc];
[self update:list userInitiated:YES context:moc];
}
/// Helper method for actual download
+ (void)update:(NSArray<Feed*>*)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 ... [StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
[moc reset]; [moc reset];
[self scheduleNextFeed]; // always reset the timer [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. /// Perform @c FaviconDownload on all core data @c Feed entries.
+ (void)updateAllFavicons { + (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]; [FaviconDownload updateFeed:f finally:nil];
} }

View File

@@ -18,6 +18,8 @@
// menu buttons // menu buttons
/** default: @c NO */ static NSString* const Pref_globalToggleHidden = @"globalToggleHidden"; /** 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_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_globalOpenUnread = @"globalOpenUnread";
/** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread"; /** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread";
/** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread"; /** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread";

View File

@@ -12,7 +12,7 @@ void UserPrefsInit(void) {
NSMutableDictionary *defs = [NSMutableDictionary dictionary]; NSMutableDictionary *defs = [NSMutableDictionary dictionary];
defaultsAppend(defs, @YES, @[ defaultsAppend(defs, @YES, @[
Pref_globalTintMenuIcon, Pref_globalTintMenuIcon,
Pref_globalUpdateAll, Pref_globalUpdateAll, Pref_groupUpdateAll, Pref_feedUpdateAll,
Pref_globalOpenUnread, Pref_groupOpenUnread, Pref_feedOpenUnread, Pref_globalOpenUnread, Pref_groupOpenUnread, Pref_feedOpenUnread,
Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead, Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead,
Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread, Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread,

View File

@@ -54,11 +54,6 @@
tip:NSLocalizedString(@"You can hold down option-key before opening the main menu to temporarily show all hidden entries.", nil) 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]; 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) [self entry:NSLocalizedString(@"“Open all unread”", nil)
help:NSLocalizedString(@"Show button to open unread articles.", nil) help:NSLocalizedString(@"Show button to open unread articles.", nil)
tip: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) 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]; 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.y += PAD_M;
[self intInput:Pref_openFewLinksLimit [self intInput:Pref_openFewLinksLimit
unit:NSLocalizedString(@"%ld unread", nil) unit:NSLocalizedString(@"%ld unread", nil)

View File

@@ -8,12 +8,12 @@
#import "NotifyEndpoint.h" #import "NotifyEndpoint.h"
#import "NSView+Ext.h" #import "NSView+Ext.h"
#import "NSColor+Ext.h" #import "NSColor+Ext.h"
#import "NSMenu+Ext.h"
@interface BarStatusItem() @interface BarStatusItem()
@property (strong) BarMenu *barMenu; @property (strong) BarMenu *barMenu;
@property (strong) NSStatusItem *statusItem; @property (strong) NSStatusItem *statusItem;
@property (assign) NSInteger unreadCountTotal; @property (assign) NSInteger unreadCountTotal;
@property (weak) NSMenuItem *updateAllItem;
/// Set to `true` if user toggled the `"Show hidden feeds"` menu option. /// Set to `true` if user toggled the `"Show hidden feeds"` menu option.
@property (assign) BOOL optShowHidden; @property (assign) BOOL optShowHidden;
/// Set to `true` if menu bar was opened while holding down option-key. /// Set to `true` if menu bar was opened while holding down option-key.
@@ -49,8 +49,8 @@
/// Fired when network conditions change. /// Fired when network conditions change.
- (void)networkChanged:(NSNotification*)notify { - (void)networkChanged:(NSNotification*)notify {
BOOL available = [[notify object] boolValue]; BOOL available = [[notify object] boolValue];
self.updateAllItem.enabled = available;
[self updateBarIcon]; [self updateBarIcon];
[self.statusItem.menu recursiveSetNetworkAvailable:available];
} }
/// Fired when a single feed has been updated. Object contains relative unread count change. /// 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 // Separator between main header and default header
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
} }
@@ -220,10 +213,4 @@
self.barMenu.showHidden = self.optShowHidden; 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 @end

View File

@@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)insertDefaultHeader; - (void)insertDefaultHeader;
// Update menu // Update menu
- (void)setHeaderHasUnread:(UnreadTotal*)count; - (void)setHeaderHasUnread:(UnreadTotal*)count;
- (void)recursiveSetNetworkAvailable:(BOOL)flag;
- (nullable NSMenuItem*)deepestItemWithPath:(nonnull NSString*)path; - (nullable NSMenuItem*)deepestItemWithPath:(nonnull NSString*)path;
@end @end

View File

@@ -6,12 +6,14 @@
#import "Constants.h" #import "Constants.h"
#import "MapUnreadTotal.h" #import "MapUnreadTotal.h"
#import "NotifyEndpoint.h" #import "NotifyEndpoint.h"
#import "UpdateScheduler.h"
typedef NS_ENUM(NSInteger, MenuItemTag) { typedef NS_ENUM(NSInteger, MenuItemTag) {
/// Used in @c allowDisplayOfHeaderItem: to identify and enable items /// Used in @c allowDisplayOfHeaderItem: to identify and enable items
TagMarkAllRead = 1, TagMarkAllRead = 1,
TagMarkAllUnread = 2, TagMarkAllUnread = 2,
TagOpenAllUnread = 3, TagOpenAllUnread = 3,
TagUpdateFeeds = 4,
/// Delimiter item between default header and core data items /// Delimiter item between default header and core data items
TagHeaderDelimiter = 8, TagHeaderDelimiter = 8,
/// Indicator whether unread count is currently shown in menu item title or not /// 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:TagMarkAllRead title:NSLocalizedString(@"Mark all read", nil)];
[self addItemIfAllowed:TagMarkAllUnread title:NSLocalizedString(@"Mark all unread", 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) { if (self.numberOfItems > 0) {
// in case someone has disabled all header items. Else, during articles menu rebuild it will stay on top. // in case someone has disabled all header items. Else, during articles menu rebuild it will stay on top.
NSMenuItem *sep = [NSMenuItem separatorItem]; 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 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. 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 mr[] = {Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead};
static NSString* const mu[] = {Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread}; static NSString* const mu[] = {Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread};
static NSString* const ou[] = {Pref_globalOpenUnread, Pref_groupOpenUnread, Pref_feedOpenUnread}; 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)); int i = (self.supermenu == nil ? 0 : (self.isFeedMenu ? 2 : 1));
switch (tag) { switch (tag) {
case TagMarkAllRead: return UserPrefsBool(mr[i]); case TagMarkAllRead: return UserPrefsBool(mr[i]);
case TagMarkAllUnread: return UserPrefsBool(mu[i]); case TagMarkAllUnread: return UserPrefsBool(mu[i]);
case TagOpenAllUnread: return UserPrefsBool(ou[i]); case TagOpenAllUnread: return UserPrefsBool(ou[i]);
case TagUpdateFeeds: return UserPrefsBool(ua[i]);
default: return NO; 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. /// Prepare @c userInfo dictionary and send @c NSNotification. Callback for every default header menu item.
+ (void)headerMenuItemCallback:(NSMenuItem*)sender { + (void)headerMenuItemCallback:(NSMenuItem*)sender {
if (sender.tag == TagUpdateFeeds) {
[UpdateScheduler forceUpdate:sender.menu.titleIndexPath];
return;
}
BOOL openLinks = NO; BOOL openLinks = NO;
NSUInteger limit = 0; NSUInteger limit = 0;
if (sender.tag == TagOpenAllUnread) { if (sender.tag == TagOpenAllUnread) {