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
- [x] How often gets the feed updated (min, max, avg)
- [ ] Automatically choose best interval?
- [ ] Show time of next update
- [x] Show time of next update
- [ ] Feeds with authentication

View File

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

View File

@@ -29,6 +29,7 @@
#import <SystemConfiguration/SystemConfiguration.h>
static NSTimer *_timer;
static SCNetworkReachabilityRef _reachability = NULL;
static BOOL _isReachable = NO;
static BOOL _isUpdating = NO;
@@ -41,6 +42,9 @@ static BOOL _nextUpdateIsForced = NO;
#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.
+ (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); }
@@ -80,9 +84,8 @@ static BOOL _nextUpdateIsForced = NO;
+ (void)scheduleUpdateForUpcomingFeeds {
if (![self allowNetworkConnection]) // timer will restart once connection exists
return;
NSDate *nextTime = [StoreCoordinator nextScheduledUpdate];
if (!nextTime) return; // no timer means no feeds to update
if ([nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time
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
nextTime = [NSDate dateWithTimeIntervalSinceNow:1];
}
[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.
*/
+ (void)scheduleTimer:(NSDate*)nextTime {
static NSTimer *timer;
if (!timer) {
timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
});
if (!nextTime)
nextTime = [NSDate dateWithTimeIntervalSinceNow:NSTimeIntervalSince1970];
nextTime = [NSDate distantFuture];
NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15;
timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec
timer.fireDate = nextTime;
_timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec
_timer.fireDate = nextTime;
}
/**

View File

@@ -37,6 +37,9 @@
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong) NSUndoManager *undoManager;
@property (strong) NSTimer *timerStatusInfo;
@property (strong) NSDateComponentsFormatter *intervalFormatter;
@end
@implementation SettingsFeeds
@@ -47,7 +50,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)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.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
@@ -89,25 +149,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[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
@@ -133,6 +174,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self.undoManager endUndoGrouping];
if (!flag && self.dataStore.managedObjectContext.hasChanges) {
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
[FeedDownload scheduleUpdateForUpcomingFeeds];
[self.timerStatusInfo fire];
return YES;
}
[self.undoManager disableUndoRegistration];

View File

@@ -221,19 +221,19 @@ CA
<action selector="shareMenu:" target="-2" id="JJq-7D-Bti"/>
</connections>
</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">
<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"/>
<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"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</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>
<point key="canvasLocation" x="27" y="882.5"/>
</customView>