Show time of next update

This commit is contained in:
relikd
2019-01-25 23:56:12 +01:00
parent d5354bb681
commit bc2181ee56
5 changed files with 85 additions and 38 deletions

View File

@@ -39,7 +39,7 @@ ToDo
- [ ] Show statistics - [ ] Show statistics
- [x] How often gets the feed updated (min, max, avg) - [x] How often gets the feed updated (min, max, avg)
- [ ] Automatically choose best interval? - [ ] Automatically choose best interval?
- [ ] Show time of next update - [x] Show time of next update
- [ ] Feeds with authentication - [ ] Feeds with authentication

View File

@@ -26,6 +26,7 @@
@class Feed; @class Feed;
@interface FeedDownload : NSObject @interface FeedDownload : NSObject
@property (class, readonly) NSDate *dateScheduled;
@property (class, readonly) BOOL allowNetworkConnection; @property (class, readonly) BOOL allowNetworkConnection;
@property (class, readonly) BOOL isUpdating; @property (class, readonly) BOOL isUpdating;
@property (class, setter=setPaused:) BOOL isPaused; @property (class, setter=setPaused:) BOOL isPaused;

View File

@@ -29,6 +29,7 @@
#import <SystemConfiguration/SystemConfiguration.h> #import <SystemConfiguration/SystemConfiguration.h>
static NSTimer *_timer;
static SCNetworkReachabilityRef _reachability = NULL; static SCNetworkReachabilityRef _reachability = NULL;
static BOOL _isReachable = NO; static BOOL _isReachable = NO;
static BOOL _isUpdating = NO; static BOOL _isUpdating = NO;
@@ -41,6 +42,9 @@ static BOOL _nextUpdateIsForced = NO;
#pragma mark - User Interaction - #pragma mark - User Interaction -
/// @return Date when background update will fire. If updates are paused, date is @c distantFuture.
+ (NSDate *)dateScheduled { return _timer.fireDate; }
/// @return @c YES if current network state is reachable and updates are not paused by user. /// @return @c YES if current network state is reachable and updates are not paused by user.
+ (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); } + (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); }
@@ -80,9 +84,8 @@ static BOOL _nextUpdateIsForced = NO;
+ (void)scheduleUpdateForUpcomingFeeds { + (void)scheduleUpdateForUpcomingFeeds {
if (![self allowNetworkConnection]) // timer will restart once connection exists if (![self allowNetworkConnection]) // timer will restart once connection exists
return; return;
NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update
if (!nextTime) return; // no timer means no feeds to update if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time
if ([nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time
nextTime = [NSDate dateWithTimeIntervalSinceNow:1]; nextTime = [NSDate dateWithTimeIntervalSinceNow:1];
} }
[self scheduleTimer:nextTime]; [self scheduleTimer:nextTime];
@@ -104,16 +107,16 @@ static BOOL _nextUpdateIsForced = NO;
@param nextTime If @c nil timer will be disabled with a @c .fireDate very far in the future. @param nextTime If @c nil timer will be disabled with a @c .fireDate very far in the future.
*/ */
+ (void)scheduleTimer:(NSDate*)nextTime { + (void)scheduleTimer:(NSDate*)nextTime {
static NSTimer *timer; static dispatch_once_t onceToken;
if (!timer) { dispatch_once(&onceToken, ^{
timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES]; _timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
} });
if (!nextTime) if (!nextTime)
nextTime = [NSDate dateWithTimeIntervalSinceNow:NSTimeIntervalSince1970]; nextTime = [NSDate distantFuture];
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;
} }
/** /**

View File

@@ -37,6 +37,9 @@
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes; @property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong) NSUndoManager *undoManager; @property (strong) NSUndoManager *undoManager;
@property (strong) NSTimer *timerStatusInfo;
@property (strong) NSDateComponentsFormatter *intervalFormatter;
@end @end
@implementation SettingsFeeds @implementation SettingsFeeds
@@ -47,7 +50,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
[self activateSpinner:([FeedDownload isUpdating] ? -1 : 0)]; // start spinner if update is in progress when preferences open
[self.outlineView registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]]; [self.outlineView registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]];
[self.dataStore setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]]; [self.dataStore setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
@@ -69,6 +71,64 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
} }
#pragma mark - Activity Spinner & Status Info
/// Initialize status info timer
- (void)viewWillAppear {
self.intervalFormatter = [[NSDateComponentsFormatter alloc] init];
self.intervalFormatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; // e.g., '30 min'
self.intervalFormatter.maximumUnitCount = 1;
self.timerStatusInfo = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:self selector:@selector(keepTimerRunning) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timerStatusInfo forMode:NSRunLoopCommonModes];
// start spinner if update is in progress when preferences open
[self activateSpinner:([FeedDownload isUpdating] ? -1 : 0)];
}
/// Timer cleanup
- (void)viewWillDisappear {
// in viewWillDisappear otherwise dealloc will not be called
[self.timerStatusInfo invalidate];
self.timerStatusInfo = nil;
self.intervalFormatter = nil;
}
/// Callback method to update status info. Will be called more often when interval is getting shorter.
- (void)keepTimerRunning {
NSDate *date = [FeedDownload dateScheduled];
if (date) {
double nextFire = fabs(date.timeIntervalSinceNow);
if (nextFire > 60) { // update 1/min
nextFire = fmod(nextFire, 60); // next update will align with minute
} else {
nextFire = 1; // update 1/sec
}
NSString *str = [self.intervalFormatter stringFromTimeInterval: date.timeIntervalSinceNow];
self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil), str];
[self.timerStatusInfo setFireDate:[NSDate dateWithTimeIntervalSinceNow: nextFire]];
}
}
/// Start ( @c c @c > @c 0 ) or stop ( @c c @c = @c 0 ) activity spinner. Also, sets status info.
- (void)activateSpinner:(NSInteger)c {
if (c == 0) {
[self.spinner stopAnimation:nil];
self.spinnerLabel.stringValue = @"";
[self.timerStatusInfo fire];
} else {
[self.timerStatusInfo setFireDate:[NSDate distantFuture]];
[self.spinner startAnimation:nil];
if (c == 1) { // exactly one feed
self.spinnerLabel.stringValue = NSLocalizedString(@"Updating 1 feed …", nil);
} else if (c < 0) { // unknown number of feeds
self.spinnerLabel.stringValue = NSLocalizedString(@"Updating feeds …", nil);
} else {
self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c];
}
}
}
#pragma mark - Notification callback methods #pragma mark - Notification callback methods
@@ -89,25 +149,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self activateSpinner:[notify.object integerValue]]; [self activateSpinner:[notify.object integerValue]];
} }
/// Start or stop activity spinner (will run on main thread). If @c c @c == @c 0 stop spinner.
- (void)activateSpinner:(NSInteger)c {
dispatch_async(dispatch_get_main_queue(), ^{
if (c == 0) {
[self.spinner stopAnimation:nil];
self.spinnerLabel.stringValue = @"";
} else {
[self.spinner startAnimation:nil];
if (c < 0) { // unknown number of feeds
self.spinnerLabel.stringValue = NSLocalizedString(@"Updating feeds …", nil);
} else if (c == 1) {
self.spinnerLabel.stringValue = NSLocalizedString(@"Updating 1 feed …", nil);
} else {
self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c];
}
}
});
}
#pragma mark - Persist state #pragma mark - Persist state
@@ -133,6 +174,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self.undoManager endUndoGrouping]; [self.undoManager endUndoGrouping];
if (!flag && self.dataStore.managedObjectContext.hasChanges) { if (!flag && self.dataStore.managedObjectContext.hasChanges) {
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES]; [StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
[FeedDownload scheduleUpdateForUpcomingFeeds];
[self.timerStatusInfo fire];
return YES; return YES;
} }
[self.undoManager disableUndoRegistration]; [self.undoManager disableUndoRegistration];

View File

@@ -221,19 +221,19 @@ CA
<action selector="shareMenu:" target="-2" id="JJq-7D-Bti"/> <action selector="shareMenu:" target="-2" id="JJq-7D-Bti"/>
</connections> </connections>
</button> </button>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="fos-vP-s2s">
<rect key="frame" x="168" y="3" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="44U-lx-hnq"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="44U-lx-hnq">
<rect key="frame" x="190" y="4" width="112" height="14"/> <rect key="frame" x="166" y="4" width="141" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="&lt;string&gt;" id="yyA-K6-M3v"> <textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="&lt;string&gt;" id="yyA-K6-M3v">
<font key="font" metaFont="smallSystem"/> <font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="fos-vP-s2s">
<rect key="frame" x="301" y="3" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
</progressIndicator>
</subviews> </subviews>
<point key="canvasLocation" x="27" y="882.5"/> <point key="canvasLocation" x="27" y="882.5"/>
</customView> </customView>