Fix status info message in feed settings
This commit is contained in:
@@ -28,7 +28,9 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
|||||||
- *Adding feed:* Inserting feeds when offline will postpone download until network is reachable again
|
- *Adding feed:* Inserting feeds when offline will postpone download until network is reachable again
|
||||||
- *Adding feed:* Inserting feeds when paused will postpone download until unpaused
|
- *Adding feed:* Inserting feeds when paused will postpone download until unpaused
|
||||||
- *Settings, Feeds:* Actions `delete` and `edit` use clicked items instead of selected items
|
- *Settings, Feeds:* Actions `delete` and `edit` use clicked items instead of selected items
|
||||||
- *Settings, Feeds:* Accurate download count instead of `Updating feeds …`
|
- *Settings, Feeds:* Status info with accurate download count (instead of `Updating feeds …`)
|
||||||
|
- *Settings, Feeds:* Status info shows `No network connection` and `Updates paused`
|
||||||
|
- *Settings, Feeds:* After feed edit, run update scheduler immediately
|
||||||
- *Status Bar Menu*: Feed title is updated properly
|
- *Status Bar Menu*: Feed title is updated properly
|
||||||
- *UI:* If an error occurs, show document URL (path to file or web url)
|
- *UI:* If an error occurs, show document URL (path to file or web url)
|
||||||
- Comparison of existing articles with nonexistent guid and link
|
- Comparison of existing articles with nonexistent guid and link
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ NS_INLINE void RegisterNotification(NSNotificationName name, SEL action, id obse
|
|||||||
Represents number of feeds that are proccessed in background update. Sends @c 0 when all downloads are finished.
|
Represents number of feeds that are proccessed in background update. Sends @c 0 when all downloads are finished.
|
||||||
*/
|
*/
|
||||||
static NSNotificationName const kNotificationBackgroundUpdateInProgress = @"baRSS-notification-background-update-in-progress";
|
static NSNotificationName const kNotificationBackgroundUpdateInProgress = @"baRSS-notification-background-update-in-progress";
|
||||||
|
/**
|
||||||
|
@c notification.object is @c nil.
|
||||||
|
Called whenever the update schedule timer is modified.
|
||||||
|
*/
|
||||||
|
static NSNotificationName const kNotificationScheduleTimerChanged = @"baRSS-notification-schedule-timer-changed";
|
||||||
/**
|
/**
|
||||||
@c notification.object is @c NSManagedObjectID of type @c FeedGroup.
|
@c notification.object is @c NSManagedObjectID of type @c FeedGroup.
|
||||||
Called whenever a new feed group was created in @c autoDownloadAndParseURL:
|
Called whenever a new feed group was created in @c autoDownloadAndParseURL:
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
@property (class, readonly) BOOL isUpdating;
|
@property (class, readonly) BOOL isUpdating;
|
||||||
@property (class, setter=setPaused:) BOOL isPaused;
|
@property (class, setter=setPaused:) BOOL isPaused;
|
||||||
|
|
||||||
|
// Getter
|
||||||
|
+ (NSString*)remainingTimeTillNextUpdate:(nullable double*)remaining;
|
||||||
|
+ (NSString*)updatingXFeeds;
|
||||||
|
// Scheduling
|
||||||
+ (void)scheduleNextFeed;
|
+ (void)scheduleNextFeed;
|
||||||
+ (void)forceUpdateAllFeeds;
|
+ (void)forceUpdateAllFeeds;
|
||||||
+ (void)downloadList:(NSArray<Feed*>*)list background:(BOOL)flag finally:(nullable os_block_t)block;
|
+ (void)downloadList:(NSArray<Feed*>*)list background:(BOOL)flag finally:(nullable os_block_t)block;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#import "WebFeed.h"
|
#import "WebFeed.h"
|
||||||
#import "Constants.h"
|
#import "Constants.h"
|
||||||
#import "StoreCoordinator.h"
|
#import "StoreCoordinator.h"
|
||||||
|
#import "NSDate+Ext.h"
|
||||||
|
|
||||||
static NSTimer *_timer;
|
static NSTimer *_timer;
|
||||||
static SCNetworkReachabilityRef _reachability = NULL;
|
static SCNetworkReachabilityRef _reachability = NULL;
|
||||||
@@ -56,21 +57,33 @@ static BOOL _nextUpdateIsForced = NO;
|
|||||||
+ (void)setPaused:(BOOL)flag {
|
+ (void)setPaused:(BOOL)flag {
|
||||||
// TODO: should pause persist between app launches?
|
// TODO: should pause persist between app launches?
|
||||||
_updatePaused = flag;
|
_updatePaused = flag;
|
||||||
|
if (flag) [self scheduleTimer:nil];
|
||||||
|
else [self scheduleNextFeed];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update status. 'Paused', 'No conection', or 'Next update in ...'
|
||||||
|
+ (NSString*)remainingTimeTillNextUpdate:(nullable double*)remaining {
|
||||||
|
double time = fabs(_timer.fireDate.timeIntervalSinceNow);
|
||||||
|
if (remaining)
|
||||||
|
*remaining = time;
|
||||||
|
if (!_isReachable)
|
||||||
|
return NSLocalizedString(@"No network connection", nil);
|
||||||
if (_updatePaused)
|
if (_updatePaused)
|
||||||
[self pauseUpdates];
|
return NSLocalizedString(@"Updates paused", nil);
|
||||||
else
|
if (time > 1e9) // distance future, over 31 years
|
||||||
[self resumeUpdates];
|
return @""; // aka. no feeds in list
|
||||||
|
return [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil),
|
||||||
|
[NSDate stringForRemainingTime:_timer.fireDate]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel current timer and stop any updates until enabled again.
|
/// Update status. 'Updating X feeds …' or empty string if not updating.
|
||||||
+ (void)pauseUpdates {
|
+ (NSString*)updatingXFeeds {
|
||||||
[self scheduleTimer:nil];
|
NSUInteger c = [WebFeed feedsInQueue];
|
||||||
|
switch (c) {
|
||||||
|
case 0: return @"";
|
||||||
|
case 1: return NSLocalizedString(@"Updating 1 feed …", nil);
|
||||||
|
default: return [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start normal (non forced) schedule if network is reachable.
|
|
||||||
+ (void)resumeUpdates {
|
|
||||||
if (_isReachable)
|
|
||||||
[self scheduleNextFeed];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +96,8 @@ static BOOL _nextUpdateIsForced = NO;
|
|||||||
+ (void)scheduleNextFeed {
|
+ (void)scheduleNextFeed {
|
||||||
if (![self allowNetworkConnection]) // timer will restart once connection exists
|
if (![self allowNetworkConnection]) // timer will restart once connection exists
|
||||||
return;
|
return;
|
||||||
|
if ([WebFeed feedsInQueue] > 0) // assume every update ends with scheduleNextFeed
|
||||||
|
return; // skip until called again
|
||||||
NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update
|
NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update
|
||||||
if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time
|
if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time
|
||||||
nextTime = [NSDate dateWithTimeIntervalSinceNow:1];
|
nextTime = [NSDate dateWithTimeIntervalSinceNow:1];
|
||||||
@@ -116,6 +131,7 @@ static BOOL _nextUpdateIsForced = NO;
|
|||||||
NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15;
|
NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15;
|
||||||
_timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec
|
_timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec
|
||||||
_timer.fireDate = nextTime;
|
_timer.fireDate = nextTime;
|
||||||
|
PostNotification(kNotificationScheduleTimerChanged, nil);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,7 +151,7 @@ static BOOL _nextUpdateIsForced = NO;
|
|||||||
[self downloadList:list background:!updateAll finally:^{
|
[self downloadList:list background:!updateAll finally:^{
|
||||||
[StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
|
[StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
|
||||||
[moc reset];
|
[moc reset];
|
||||||
[self resumeUpdates]; // always reset the timer
|
[self scheduleNextFeed]; // always reset the timer
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,9 +209,9 @@ static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetwo
|
|||||||
_isReachable = [UpdateScheduler hasConnectivity:flags];
|
_isReachable = [UpdateScheduler hasConnectivity:flags];
|
||||||
PostNotification(kNotificationNetworkStatusChanged, @(_isReachable));
|
PostNotification(kNotificationNetworkStatusChanged, @(_isReachable));
|
||||||
if (_isReachable) {
|
if (_isReachable) {
|
||||||
[UpdateScheduler resumeUpdates];
|
[UpdateScheduler scheduleNextFeed];
|
||||||
} else {
|
} else {
|
||||||
[UpdateScheduler pauseUpdates];
|
[UpdateScheduler scheduleTimer:nil];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -290,7 +290,6 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
|||||||
}
|
}
|
||||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||||
if (block) block();
|
if (block) block();
|
||||||
PostNotification(kNotificationBackgroundUpdateInProgress, @(_queueSize));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>10782</string>
|
<string>10902</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.news</string>
|
<string>public.app-category.news</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ const NSPasteboardType dragReorder = @"de.relikd.baRSS.drag-reorder";
|
|||||||
|
|
||||||
[UpdateScheduler downloadList:feedsList background:NO finally:^{
|
[UpdateScheduler downloadList:feedsList background:NO finally:^{
|
||||||
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
|
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
|
||||||
[self someDatesChangedScheduleUpdateTimer];
|
[UpdateScheduler scheduleNextFeed];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,5 @@
|
|||||||
|
|
||||||
- (void)beginCoreDataChange;
|
- (void)beginCoreDataChange;
|
||||||
- (BOOL)endCoreDataChangeUndoEmpty:(BOOL)undoEmpty forceUndo:(BOOL)force;
|
- (BOOL)endCoreDataChangeUndoEmpty:(BOOL)undoEmpty forceUndo:(BOOL)force;
|
||||||
- (void)someDatesChangedScheduleUpdateTimer;
|
|
||||||
- (void)restoreOrderingAndIndexPathStr:(NSArray<NSTreeNode*>*)parentsList;
|
- (void)restoreOrderingAndIndexPathStr:(NSArray<NSTreeNode*>*)parentsList;
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
#import "FeedGroup+Ext.h"
|
#import "FeedGroup+Ext.h"
|
||||||
#import "UpdateScheduler.h"
|
#import "UpdateScheduler.h"
|
||||||
#import "SettingsFeedsView.h"
|
#import "SettingsFeedsView.h"
|
||||||
#import "NSDate+Ext.h"
|
|
||||||
|
|
||||||
@interface SettingsFeeds ()
|
@interface SettingsFeeds ()
|
||||||
@property (strong) SettingsFeedsView *view; // override super
|
@property (strong) SettingsFeedsView *view; // override super
|
||||||
@@ -51,7 +50,10 @@
|
|||||||
RegisterNotification(kNotificationFeedUpdated, @selector(feedUpdated:), self);
|
RegisterNotification(kNotificationFeedUpdated, @selector(feedUpdated:), self);
|
||||||
RegisterNotification(kNotificationFeedIconUpdated, @selector(feedUpdated:), self);
|
RegisterNotification(kNotificationFeedIconUpdated, @selector(feedUpdated:), self);
|
||||||
RegisterNotification(kNotificationGroupInserted, @selector(groupInserted:), self);
|
RegisterNotification(kNotificationGroupInserted, @selector(groupInserted:), self);
|
||||||
RegisterNotification(kNotificationBackgroundUpdateInProgress, @selector(updateInProgress:), self);
|
// Status bar
|
||||||
|
RegisterNotification(kNotificationScheduleTimerChanged, @selector(updateStatusInfo), self);
|
||||||
|
RegisterNotification(kNotificationNetworkStatusChanged, @selector(updateStatusInfo), self);
|
||||||
|
RegisterNotification(kNotificationBackgroundUpdateInProgress, @selector(updateStatusInfo), self);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
@@ -62,10 +64,9 @@
|
|||||||
- (void)viewWillAppear {
|
- (void)viewWillAppear {
|
||||||
// needed to scroll outline view to top (if prefs open on another tab)
|
// needed to scroll outline view to top (if prefs open on another tab)
|
||||||
[self.dataStore setSelectionIndexPath:[NSIndexPath indexPathWithIndex:0]];
|
[self.dataStore setSelectionIndexPath:[NSIndexPath indexPathWithIndex:0]];
|
||||||
self.timerStatusInfo = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:self selector:@selector(keepTimerRunning) userInfo:nil repeats:YES];
|
self.timerStatusInfo = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:self selector:@selector(updateStatusInfo) userInfo:nil repeats:YES];
|
||||||
[[NSRunLoop mainRunLoop] addTimer:self.timerStatusInfo forMode:NSRunLoopCommonModes];
|
[[NSRunLoop mainRunLoop] addTimer:self.timerStatusInfo forMode:NSRunLoopCommonModes];
|
||||||
// start spinner if update is in progress when preferences open
|
[self updateStatusInfo];
|
||||||
[self activateSpinner:[UpdateScheduler feedsInQueue]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timer cleanup
|
/// Timer cleanup
|
||||||
@@ -143,12 +144,7 @@
|
|||||||
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
|
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||||
[self.dataStore rearrangeObjects]; // update ordering
|
[self.dataStore rearrangeObjects]; // update ordering
|
||||||
}
|
|
||||||
|
|
||||||
/// Query core data for next update date and set bottom status message
|
|
||||||
- (void)someDatesChangedScheduleUpdateTimer {
|
|
||||||
[UpdateScheduler scheduleNextFeed];
|
[UpdateScheduler scheduleNextFeed];
|
||||||
[self.timerStatusInfo fire];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback method fired when feed (or icon) has been updated in the background.
|
/// Callback method fired when feed (or icon) has been updated in the background.
|
||||||
@@ -172,43 +168,22 @@
|
|||||||
#pragma mark - Activity Spinner & Status Info
|
#pragma mark - Activity Spinner & Status Info
|
||||||
|
|
||||||
|
|
||||||
/// Callback method to update status info. Will be called more often when interval is getting shorter.
|
/// Callback method to update status info. Called more often as the interval is getting shorter.
|
||||||
- (void)keepTimerRunning {
|
- (void)updateStatusInfo {
|
||||||
NSDate *date = [UpdateScheduler dateScheduled];
|
if ([UpdateScheduler feedsInQueue] > 0) {
|
||||||
if (date) {
|
[self.timerStatusInfo setFireDate:[NSDate distantFuture]];
|
||||||
double nextFire = fabs(date.timeIntervalSinceNow);
|
self.view.status.stringValue = [UpdateScheduler updatingXFeeds];
|
||||||
if (nextFire > 1e9) { // distance future, over 31 years
|
[self.view.spinner startAnimation:nil];
|
||||||
self.view.status.stringValue = @"";
|
} else {
|
||||||
return;
|
[self.view.spinner stopAnimation:nil];
|
||||||
}
|
double remaining;
|
||||||
self.view.status.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil),
|
self.view.status.stringValue = [UpdateScheduler remainingTimeTillNextUpdate:&remaining];
|
||||||
[NSDate stringForRemainingTime:date]];
|
if (remaining < 1e5) { // keep timer running if < 28 hours
|
||||||
// Next update is aligned with minute (fmod) else update 1/sec
|
// Next update is aligned with minute (fmod) else update 1/sec
|
||||||
NSDate *nextUpdate = [NSDate dateWithTimeIntervalSinceNow: (nextFire > 60 ? fmod(nextFire, 60) : 1)];
|
NSDate *nextUpdate = [NSDate dateWithTimeIntervalSinceNow: (remaining > 60 ? fmod(remaining, 60) : 1)];
|
||||||
[self.timerStatusInfo setFireDate:nextUpdate];
|
[self.timerStatusInfo setFireDate:nextUpdate];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start ( @c c @c > @c 0 ) or stop ( @c c @c = @c 0 ) activity spinner. Also, sets status info.
|
|
||||||
- (void)activateSpinner:(NSUInteger)c {
|
|
||||||
if (c == 0) {
|
|
||||||
[self.view.spinner stopAnimation:nil];
|
|
||||||
self.view.status.stringValue = @"";
|
|
||||||
[self.timerStatusInfo fire];
|
|
||||||
} else {
|
|
||||||
[self.timerStatusInfo setFireDate:[NSDate distantFuture]];
|
|
||||||
[self.view.spinner startAnimation:nil];
|
|
||||||
if (c == 1) { // exactly one feed
|
|
||||||
self.view.status.stringValue = NSLocalizedString(@"Updating 1 feed …", nil);
|
|
||||||
} else {
|
|
||||||
self.view.status.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Callback method fired when background feed update begins and ends.
|
|
||||||
- (void)updateInProgress:(NSNotification*)notify {
|
|
||||||
[self activateSpinner:[notify.object unsignedIntegerValue]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -252,7 +227,7 @@
|
|||||||
[self.dataStore removeObjectsAtArrangedObjectIndexPaths:[nodes valueForKeyPath:@"indexPath"]];
|
[self.dataStore removeObjectsAtArrangedObjectIndexPaths:[nodes valueForKeyPath:@"indexPath"]];
|
||||||
[self restoreOrderingAndIndexPathStr:parentNodes];
|
[self restoreOrderingAndIndexPathStr:parentNodes];
|
||||||
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
|
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
|
||||||
[self someDatesChangedScheduleUpdateTimer];
|
[UpdateScheduler scheduleNextFeed];
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,16 +300,18 @@
|
|||||||
[self beginCoreDataChange];
|
[self beginCoreDataChange];
|
||||||
if (!fg || ![fg isKindOfClass:[FeedGroup class]]) {
|
if (!fg || ![fg isKindOfClass:[FeedGroup class]]) {
|
||||||
fg = [self insertFeedGroupAtSelection:(flag ? GROUP : FEED)];
|
fg = [self insertFeedGroupAtSelection:(flag ? GROUP : FEED)];
|
||||||
|
} else {
|
||||||
|
flag = (fg.type == GROUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
ModalEditDialog *editDialog = (fg.type == GROUP ? [ModalGroupEdit modalWith:fg] : [ModalFeedEdit modalWith:fg]);
|
ModalEditDialog *editDialog = (flag ? [ModalGroupEdit modalWith:fg] : [ModalFeedEdit modalWith:fg]);
|
||||||
|
|
||||||
[self.view.window beginSheet:[editDialog getModalSheet] completionHandler:^(NSModalResponse returnCode) {
|
[self.view.window beginSheet:[editDialog getModalSheet] completionHandler:^(NSModalResponse returnCode) {
|
||||||
if (returnCode == NSModalResponseOK) {
|
if (returnCode == NSModalResponseOK) {
|
||||||
[editDialog applyChangesToCoreDataObject];
|
[editDialog applyChangesToCoreDataObject];
|
||||||
}
|
}
|
||||||
if ([self endCoreDataChangeUndoEmpty:YES forceUndo:(returnCode != NSModalResponseOK)]) {
|
if ([self endCoreDataChangeUndoEmpty:YES forceUndo:(returnCode != NSModalResponseOK)]) {
|
||||||
if (!flag) [self someDatesChangedScheduleUpdateTimer]; // only for feed edit
|
if (!flag) [UpdateScheduler scheduleNextFeed]; // only for feed edit
|
||||||
[self.dataStore rearrangeObjects]; // update display, edited title or icon
|
[self.dataStore rearrangeObjects]; // update display, edited title or icon
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|||||||
Reference in New Issue
Block a user