Show time of next update
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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="<string>" id="yyA-K6-M3v">
|
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="<string>" 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user