Auto update feeds. Menu rebuild so far only after close.

This commit is contained in:
relikd
2018-10-28 15:25:52 +01:00
parent 5fcaf97fe0
commit 080991ebc4
10 changed files with 357 additions and 49 deletions

View File

@@ -35,7 +35,7 @@ ToDo
- [ ] Tick mark feed items based on prefs - [ ] Tick mark feed items based on prefs
- [ ] Open a few links (# editable) - [ ] Open a few links (# editable)
- [ ] Performance: Update menu partially - [ ] Performance: Update menu partially
- [ ] Start on login - [x] Start on login
- [x] Make it system default application - [x] Make it system default application
- [ ] Display license info (e.g., RSXML) - [ ] Display license info (e.g., RSXML)
- [ ] Short article names - [ ] Short article names
@@ -47,14 +47,16 @@ ToDo
- [ ] Status menu - [ ] Status menu
- [ ] Update menu header after mark (un)read - [ ] Update menu header after mark (un)read
- [ ] Pause updates functionality - [ ] Pause updates functionality
- [ ] Update all feeds functionality - [x] Update all feeds functionality
- [ ] Hold only relevant information in memory
- [ ] Edit feed - [ ] Edit feed
- [ ] Show statistics - [ ] Show statistics
- [ ] How often gets the feed updated (min, max, avg) - [ ] How often gets the feed updated (min, max, avg)
- [ ] Automatically choose best interval? - [ ] Automatically choose best interval?
- [ ] Auto fix 301 Redirect or ask user - [ ] Show time of next update
- [x] Auto fix 301 Redirect or ask user
- [ ] Make `feed://` URLs clickable - [ ] Make `feed://` URLs clickable
- [ ] Feeds with authentication - [ ] Feeds with authentication
- [ ] Show proper feed icon - [ ] Show proper feed icon
@@ -64,15 +66,18 @@ ToDo
- [ ] Other - [ ] Other
- [ ] App Icon - [ ] App Icon
- [ ] Translate text to different languages - [ ] Translate text to different languages
- [ ] Automatically update feeds with chosen interval - [x] Automatically update feeds with chosen interval
- [ ] Reuse ETag and Modification date - [x] Reuse ETag and Modification date
- [ ] Append only new items, keep sorting - ~~[ ] Append only new items, keep sorting~~
- [ ] Delete old ones eventually - [x] Delete old ones eventually
- [x] Pause on internet connection lost
- [ ] Download with ephemeral url session?
- [ ] Purge cache - [ ] Purge cache
- [ ] Manually or automatically - [ ] Manually or automatically
- [ ] Add something to restore a broken state - [ ] Add something to restore a broken state
- [ ] Code Documentation (mostly methods) - [ ] Code Documentation (mostly methods)
- [ ] Add Sandboxing - [ ] Add Sandboxing
- [ ] Disable Startup checkbox (or other workaround)
- [ ] Additional features - [ ] Additional features

View File

@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G65" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1"> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G65" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1">
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class"> <entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
<attribute name="httpEtag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="httpModified" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="Binary" customClassName="NSImage" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/> <attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/> <attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/> <attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
@@ -11,15 +8,17 @@
<relationship name="items" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="FeedItem" inverseName="feed" inverseEntity="FeedItem" syncable="YES"/> <relationship name="items" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="FeedItem" inverseName="feed" inverseEntity="FeedItem" syncable="YES"/>
</entity> </entity>
<entity name="FeedConfig" representedClassName="FeedConfig" syncable="YES" codeGenerationType="class"> <entity name="FeedConfig" representedClassName="FeedConfig" syncable="YES" codeGenerationType="class">
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/> <attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refreshNum" optional="YES" attributeType="Integer 32" usesScalarValueType="YES" syncable="YES"/> <attribute name="refreshNum" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refreshUnit" optional="YES" attributeType="Integer 16" usesScalarValueType="YES" customClassName="NSUInteger" syncable="YES"/> <attribute name="refreshUnit" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" customClassName="NSUInteger" syncable="YES"/>
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/> <attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="type" optional="YES" attributeType="Integer 16" usesScalarValueType="YES" syncable="YES"/> <attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
<attribute name="url" optional="YES" attributeType="String" syncable="YES"/> <attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedConfig" inverseName="parent" inverseEntity="FeedConfig" syncable="YES"/> <relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedConfig" inverseName="parent" inverseEntity="FeedConfig" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="config" inverseEntity="Feed" syncable="YES"/> <relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="config" inverseEntity="Feed" syncable="YES"/>
<relationship name="meta" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="FeedMeta" inverseName="config" inverseEntity="FeedMeta" syncable="YES"/>
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="children" inverseEntity="FeedConfig" syncable="YES"/> <relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="children" inverseEntity="FeedConfig" syncable="YES"/>
</entity> </entity>
<entity name="FeedItem" representedClassName="FeedItem" syncable="YES" codeGenerationType="class"> <entity name="FeedItem" representedClassName="FeedItem" syncable="YES" codeGenerationType="class">
@@ -33,9 +32,16 @@
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/> <attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/> <relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/>
</entity> </entity>
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
<attribute name="httpEtag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="httpModified" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="Binary" customClassName="NSImage" syncable="YES"/>
<relationship name="config" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="meta" inverseEntity="FeedConfig" syncable="YES"/>
</entity>
<elements> <elements>
<element name="Feed" positionX="-209" positionY="-3" width="128" height="165"/> <element name="Feed" positionX="-229.09375" positionY="-2.30859375" width="128" height="120"/>
<element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="195"/> <element name="FeedConfig" positionX="-441.87109375" positionY="-47.390625" width="128" height="225"/>
<element name="FeedItem" positionX="-20" positionY="81" width="128" height="180"/> <element name="FeedItem" positionX="-28.140625" positionY="-17.359375" width="128" height="180"/>
<element name="FeedMeta" positionX="-234" positionY="72" width="128" height="105"/>
</elements> </elements>
</model> </model>

View File

@@ -24,5 +24,9 @@
#import <RSXML/RSXML.h> #import <RSXML/RSXML.h>
@interface FeedDownload : NSObject @interface FeedDownload : NSObject
+ (void)getFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block; + (void)newFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block;
+ (void)registerNetworkChangeNotification;
+ (void)unregisterNetworkChangeNotification;
+ (BOOL)isNetworkReachable;
+ (void)scheduleNextUpdate:(BOOL)forceUpdate;
@end @end

View File

@@ -21,21 +21,37 @@
// SOFTWARE. // SOFTWARE.
#import "FeedDownload.h" #import "FeedDownload.h"
#import "StoreCoordinator.h"
#import <SystemConfiguration/SystemConfiguration.h>
static SCNetworkReachabilityRef _reachability = NULL;
static BOOL _isReachable = NO;
@implementation FeedDownload @implementation FeedDownload
+ (void)getFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block { + (NSMutableURLRequest*)newRequestURL:(NSString*)url {
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
req.timeoutInterval = 30; req.timeoutInterval = 30;
req.cachePolicy = NSURLRequestReloadIgnoringCacheData; req.cachePolicy = NSURLRequestReloadIgnoringCacheData;
// [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"]; // [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"];
// [req setValue:@"wII2pETT9EGmlqyCHBFJpm25/7w" forHTTPHeaderField:@"If-None-Match"]; // ETag // [req setValue:@"wII2pETT9EGmlqyCHBFJpm25/7w" forHTTPHeaderField:@"If-None-Match"]; // ETag
[[[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { return req;
}
+ (NSURLRequest*)newRequest:(FeedConfig*)config {
NSMutableURLRequest *req = [self newRequestURL:config.url];
NSString* etag = [config.meta.httpEtag stringByReplacingOccurrencesOfString:@"-gzip" withString:@""];
if (config.meta.httpModified.length > 0)
[req setValue:config.meta.httpModified forHTTPHeaderField:@"If-Modified-Since"];
if (etag.length > 0)
[req setValue:etag forHTTPHeaderField:@"If-None-Match"]; // ETag
return req;
}
+ (void)newFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block {
[[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequestURL:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
// NSString* etag = [httpResponse allHeaderFields][@"Etag"];
// if (etag.length > 5 && [[etag substringFromIndex:etag.length - 5] isEqualToString:@"-gzip"]) {
// etag = [etag substringToIndex:etag.length - 5];
// }
if (error || [httpResponse statusCode] == 304) { if (error || [httpResponse statusCode] == 304) {
block(nil, error, httpResponse); block(nil, error, httpResponse);
return; return;
@@ -47,4 +63,154 @@
}] resume]; }] resume];
} }
#pragma mark - Update existing feeds -
+ (void)scheduleNextUpdate:(BOOL)forceUpdate {
static NSTimer *_updateTimer;
@synchronized (_updateTimer) {
if (_updateTimer) {
[_updateTimer invalidate];
_updateTimer = nil;
}
}
if (!_isReachable) return; // cancel timer entirely (will be restarted once connection exists)
NSDate *nextTime = [NSDate dateWithTimeIntervalSinceNow:0.2];
if (!forceUpdate) {
nextTime = [StoreCoordinator nextScheduledUpdate];
if (!nextTime || [nextTime timeIntervalSinceNow] < 0) { // mostly, if app was closed for a long time
nextTime = [NSDate dateWithTimeIntervalSinceNow:2]; // TODO: retry in 2 sec?
}
}
NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15;
_updateTimer = [NSTimer timerWithTimeInterval:0 target:[self class] selector:@selector(scheduledUpdateTimer:) userInfo:@(forceUpdate) repeats:NO];
_updateTimer.fireDate = nextTime;
_updateTimer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec
[[NSRunLoop mainRunLoop] addTimer:_updateTimer forMode:NSRunLoopCommonModes];
}
+ (void)scheduledUpdateTimer:(NSTimer*)timer {
NSLog(@"fired");
BOOL forceAll = [timer.userInfo boolValue];
// TODO: check internet connection
// TODO: disable menu item 'update all' during update
NSArray<FeedConfig*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:forceAll];
if (list.count == 0) {
NSLog(@"ERROR: Something went wrong, timer fired too early.");
// thechnically should never happen, anyway we need to reset the timer
[self scheduleNextUpdate:NO]; // NO, since forceAll will get ALL items and shouldn't be 0
return; // nothing to do here
}
NSUndoManager *um = list.firstObject.managedObjectContext.undoManager;
[um beginUndoGrouping];
dispatch_group_t group = dispatch_group_create();
for (FeedConfig *c in list) {
[self downloadFeedForConfig:c group:group];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[um endUndoGrouping];
[self scheduleNextUpdate:NO]; // after forced update, continue regular cycle
});
}
+ (void)downloadFeedForConfig:(FeedConfig*)config group:(dispatch_group_t)group {
if (!_isReachable) return;
dispatch_group_enter(group);
[[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequest:config] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[config.managedObjectContext.undoManager beginUndoGrouping];
if (error) {
int16_t n = config.errorCount + 1;
config.errorCount = (n < 1 ? 1 : (n > 19 ? 19 : n)); // between: 2 sec and 6 days
NSTimeInterval retryWaitTime = pow(2, config.errorCount); // 2^n seconds
config.scheduled = [NSDate dateWithTimeIntervalSinceNow:retryWaitTime];
// TODO: remove logging
NSLog(@"Error loading: %@ (%d)", response.URL, config.errorCount);
} else {
config.errorCount = 0; // reset counter
[self downloadSuccessful:data forFeed:config response:(NSHTTPURLResponse*)response];
}
[config.managedObjectContext.undoManager endUndoGrouping];
dispatch_group_leave(group);
}] resume];
}
+ (void)downloadSuccessful:(NSData*)data forFeed:(FeedConfig*)config response:(NSHTTPURLResponse*)http {
if ([http statusCode] != 304) {
// should be fine to call synchronous since dataTask is already in the background (always? proof?)
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:config.url];
RSParsedFeed *parsed = RSParseFeedSync(xml, NULL);
if (parsed) {
// TODO: add support for media player?
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
[StoreCoordinator overwriteConfig:config withFeed:parsed];
}
}
config.meta.httpModified = [http allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
config.meta.httpEtag = [http allHeaderFields][@"Etag"];
// Don't update redirected url since it happened in the background; User may not recognize url
[config calculateAndSetScheduled];
[config mergeChangesAndSave];
[[NSNotificationCenter defaultCenter] postNotificationName:@"baRSS-notification-feed-updated" object:config];
}
#pragma mark - Network Connection -
+ (BOOL)isNetworkReachable { return _isReachable; }
+ (void)registerNetworkChangeNotification {
// https://stackoverflow.com/questions/11240196/notification-when-wifi-connected-os-x
if (_reachability != NULL) return;
_reachability = SCNetworkReachabilityCreateWithName(NULL, "1.1.1.1");
if (_reachability == NULL) return;
// If reachability information is available now, we don't get a callback later
SCNetworkConnectionFlags flags;
if (SCNetworkReachabilityGetFlags(_reachability, &flags))
networkReachabilityCallback(_reachability, flags, NULL);
if (!SCNetworkReachabilitySetCallback(_reachability, networkReachabilityCallback, NULL) ||
!SCNetworkReachabilityScheduleWithRunLoop(_reachability, [[NSRunLoop currentRunLoop] getCFRunLoop], kCFRunLoopCommonModes))
{
CFRelease(_reachability);
_reachability = NULL;
}
}
+ (void)unregisterNetworkChangeNotification {
if (_reachability != NULL) {
SCNetworkReachabilitySetCallback(_reachability, nil, nil);
SCNetworkReachabilitySetDispatchQueue(_reachability, nil);
CFRelease(_reachability);
_reachability = NULL;
}
}
static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkConnectionFlags flags, void *object) {
if (_reachability == NULL)
return;
_isReachable = [FeedDownload hasConnectivity:flags];
[[NSNotificationCenter defaultCenter] postNotificationName:@"baRSS-notification-network-status-change"
object:[NSNumber numberWithBool:_isReachable]];
if (_isReachable) {
NSLog(@"reachable");
} else {
NSLog(@"not reachable");
}
// schedule regardless of state (if not reachable timer will be canceled)
[FeedDownload scheduleNextUpdate:NO];
}
+ (BOOL)hasConnectivity:(SCNetworkReachabilityFlags)flags {
if ((flags & kSCNetworkReachabilityFlagsReachable) == 0)
return NO;
if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0)
return YES;
if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0 &&
((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0 ||
(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0))
return YES; // no-intervention AND ( on-demand OR on-traffic )
return NO;
}
@end @end

View File

@@ -42,8 +42,11 @@ typedef BOOL (^FeedConfigRecursiveItemsBlock) (FeedConfig *parent, FeedItem *ite
@property (getter=typ, setter=setTyp:) FeedConfigType typ; @property (getter=typ, setter=setTyp:) FeedConfigType typ;
@property (readonly) NSArray<FeedConfig*> *sortedChildren; @property (readonly) NSArray<FeedConfig*> *sortedChildren;
@property (readonly) NSIndexPath *indexPath;
- (BOOL)descendantFeedItems:(FeedConfigRecursiveItemsBlock)block; - (BOOL)descendantFeedItems:(FeedConfigRecursiveItemsBlock)block;
- (void)calculateAndSetScheduled;
- (void)mergeChangesAndSave;
- (NSString*)readableRefreshString; - (NSString*)readableRefreshString;
- (NSString*)readableDescription; - (NSString*)readableDescription;
@end @end

View File

@@ -40,6 +40,12 @@
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]]; return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
} }
- (NSIndexPath *)indexPath {
if (self.parent == nil)
return [NSIndexPath indexPathWithIndex:(NSUInteger)self.sortIndex];
return [self.parent.indexPath indexPathByAddingIndex:(NSUInteger)self.sortIndex];
}
/** /**
Iterate over all descendant @c FeedItems in sub groups Iterate over all descendant @c FeedItems in sub groups
@@ -61,6 +67,25 @@
return YES; return YES;
} }
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
- (NSTimeInterval)timeInterval {
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
return self.refreshNum * unit[self.refreshUnit % 5];
}
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
- (void)calculateAndSetScheduled {
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
}
/// Update item with @c mergeChanges:YES and save the context
- (void)mergeChangesAndSave {
[self.managedObjectContext performBlockAndWait:^{
[self.managedObjectContext refreshObject:self mergeChanges:YES];
[self.managedObjectContext save:nil];
}];
}
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h ) /// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (NSString*)readableRefreshString { - (NSString*)readableRefreshString {
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]]; return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];

View File

@@ -35,6 +35,8 @@
@property (weak) IBOutlet NSPopover *warningPopover; @property (weak) IBOutlet NSPopover *warningPopover;
@property (copy) NSString *previousURL; @property (copy) NSString *previousURL;
@property (copy) NSString *httpDate;
@property (copy) NSString *httpEtag;
@property (strong) NSError *feedError; @property (strong) NSError *feedError;
@property (strong) RSParsedFeed *feedResult; @property (strong) RSParsedFeed *feedResult;
@@ -107,15 +109,19 @@
item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem; item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem;
if (self.shouldDeletePrevArticles) { if (self.shouldDeletePrevArticles) {
[StoreCoordinator overwriteConfig:item withFeed:self.feedResult];
[item.managedObjectContext performBlockAndWait:^{ [item.managedObjectContext performBlockAndWait:^{
if (item.feed) // TODO: move to separate function and add icon download
[item.managedObjectContext deleteObject:(NSManagedObject*)item.feed]; if (!item.meta) {
if (self.feedResult) item.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:item.managedObjectContext];
item.feed = [StoreCoordinator createFeedFrom:self.feedResult inContext:item.managedObjectContext]; }
item.meta.httpEtag = self.httpEtag;
item.meta.httpModified = self.httpDate;
}]; }];
} }
if ([item.managedObjectContext hasChanges]) { if ([item.managedObjectContext hasChanges]) {
self.objectIsModified = YES; self.objectIsModified = YES;
[item calculateAndSetScheduled];
[item.managedObjectContext performBlockAndWait:^{ [item.managedObjectContext performBlockAndWait:^{
[item.managedObjectContext refreshObject:item mergeChanges:YES]; [item.managedObjectContext refreshObject:item mergeChanges:YES];
}]; }];
@@ -146,11 +152,16 @@
self.feedError = nil; self.feedError = nil;
[self.spinnerURL startAnimation:nil]; [self.spinnerURL startAnimation:nil];
[self.spinnerName startAnimation:nil]; [self.spinnerName startAnimation:nil];
[FeedDownload getFeed:self.previousURL block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) { [FeedDownload newFeed:self.previousURL block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) {
self.feedResult = result; self.feedResult = result;
// [httpResponse allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified" self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
// [httpResponse allHeaderFields][@"Etag"]; self.httpEtag = [response allHeaderFields][@"Etag"];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (response && ![response.URL.absoluteString isEqualToString:self.url.stringValue]) {
// URL was redirected, so replace original text field value with new one
self.url.stringValue = response.URL.absoluteString;
self.previousURL = self.url.stringValue;
}
// TODO: play error sound? // TODO: play error sound?
self.feedError = error; // warning indicator .hidden is bound to feedError self.feedError = error; // warning indicator .hidden is bound to feedError
self.objectNeedsSaving = YES; // stays YES if this block runs after updateRepresentedObject: self.objectNeedsSaving = YES; // stays YES if this block runs after updateRepresentedObject:

View File

@@ -22,6 +22,7 @@
#import "BarMenu.h" #import "BarMenu.h"
#import "StoreCoordinator.h" #import "StoreCoordinator.h"
#import "FeedDownload.h"
#import "DrawImage.h" #import "DrawImage.h"
#import "Preferences.h" #import "Preferences.h"
#import "NSMenuItem+Info.h" #import "NSMenuItem+Info.h"
@@ -44,12 +45,34 @@
self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength]; self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
self.barItem.highlightMode = YES; self.barItem.highlightMode = YES;
[self rebuildMenu]; [self rebuildMenu];
// [self donothing]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChange:) name:@"baRSS-notification-network-status-change" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:@"baRSS-notification-feed-updated" object:nil];
[FeedDownload registerNetworkChangeNotification];
[FeedDownload performSelectorInBackground:@selector(scheduleNextUpdate:) withObject:[NSNumber numberWithBool:NO]];
return self; return self;
} }
- (void)dealloc {
[FeedDownload unregisterNetworkChangeNotification];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)networkChange:(NSNotification*)notify {
BOOL available = [[notify object] boolValue];
[self.barItem.menu itemWithTag:TagUpdateFeed].enabled = available;
[self updateBarIcon];
// TODO: Disable 'update all' menu item?
}
- (void)feedUpdated:(NSNotification*)notify {
FeedConfig *config = notify.object;
NSLog(@"%@", config.indexPath);
[self rebuildMenu];
}
- (void)rebuildMenu { - (void)rebuildMenu {
self.barItem.menu = [self generateMainMenu]; self.barItem.menu = [self generateMainMenu];
[self updateBarIcon];
} }
- (void)donothing { - (void)donothing {
@@ -79,13 +102,14 @@
*/ */
- (void)updateBarIcon { - (void)updateBarIcon {
// TODO: Option: icon choice // TODO: Option: icon choice
// TODO: Show paused icon if no internet connection
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) { if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) {
self.barItem.title = [NSString stringWithFormat:@"%d", self.unreadCountTotal]; self.barItem.title = [NSString stringWithFormat:@"%d", self.unreadCountTotal];
} else { } else {
self.barItem.title = @""; self.barItem.title = @"";
} }
// BOOL hasNet = [FeedDownload isNetworkReachable];
if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"tintMenuBarIcon"]) { if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"tintMenuBarIcon"]) {
self.barItem.image = [RSSIcon templateIcon:16 tint:[NSColor rssOrange]]; self.barItem.image = [RSSIcon templateIcon:16 tint:[NSColor rssOrange]];
} else { } else {
@@ -122,7 +146,6 @@
} }
} }
[self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)]; [self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)];
[self updateBarIcon];
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
@@ -285,7 +308,8 @@
} }
- (void)updateAllFeeds:(NSMenuItem*)sender { - (void)updateAllFeeds:(NSMenuItem*)sender {
NSLog(@"1update all"); // TODO: Disable 'update all' menu item during update?
[FeedDownload scheduleNextUpdate:YES];
} }
/** /**

View File

@@ -30,6 +30,8 @@
+ (void)saveContext:(NSManagedObjectContext*)context; + (void)saveContext:(NSManagedObjectContext*)context;
+ (void)deleteUnreferencedFeeds; + (void)deleteUnreferencedFeeds;
+ (NSArray<FeedConfig*>*)sortedFeedConfigItems; + (NSArray<FeedConfig*>*)sortedFeedConfigItems;
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll;
+ (NSDate*)nextScheduledUpdate;
+ (id)objectWithID:(NSManagedObjectID*)objID; + (id)objectWithID:(NSManagedObjectID*)objID;
+ (Feed*)createFeedFrom:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context; + (void)overwriteConfig:(FeedConfig*)config withFeed:(RSParsedFeed*)obj;
@end @end

View File

@@ -63,30 +63,92 @@
return result; return result;
} }
+ (NSArray<FeedConfig*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll {
NSManagedObjectContext *moc = [self getContext];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name];
if (!forceAll) {
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d AND scheduled <= %@", FEED, [NSDate date]];
} else {
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d", FEED];
}
NSError *err;
NSArray *result = [moc executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return result;
}
+ (NSDate*)nextScheduledUpdate {
NSExpression *exp = [NSExpression expressionForFunction:@"min:" arguments:@[[NSExpression expressionForKeyPath:@"scheduled"]]];
NSExpressionDescription *expDesc = [[NSExpressionDescription alloc] init];
[expDesc setName:@"earliestDate"];
[expDesc setExpression:exp];
[expDesc setExpressionResultType:NSDateAttributeType];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: FeedConfig.entity.name];
fr.predicate = [NSPredicate predicateWithFormat:@"type = %d", FEED];
[fr setResultType:NSDictionaryResultType];
[fr setPropertiesToFetch:@[expDesc]];
NSError *err;
NSArray *fetchResults = [[self getContext] executeFetchRequest:fr error:&err];
if (err) NSLog(@"%@", err);
return [fetchResults firstObject][@"earliestDate"]; // can be nil
}
+ (id)objectWithID:(NSManagedObjectID*)objID { + (id)objectWithID:(NSManagedObjectID*)objID {
return [[self getContext] objectWithID:objID]; return [[self getContext] objectWithID:objID];
} }
+ (Feed*)createFeedFrom:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context {
+ (void)overwriteConfig:(FeedConfig*)config withFeed:(RSParsedFeed*)obj {
NSArray<NSString*> *readURLs = [self alreadyReadURLsInFeed:config.feed];
[config.managedObjectContext performBlockAndWait:^{
if (config.feed)
[config.managedObjectContext deleteObject:(NSManagedObject*)config.feed];
if (obj) {
config.feed = [StoreCoordinator createFeedFrom:obj inContext:config.managedObjectContext alreadyRead:readURLs];
}
}];
}
#pragma mark - Helper methods -
+ (FeedItem*)createFeedItemFrom:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)context {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context];
b.guid = entry.guid;
b.title = entry.title;
b.abstract = entry.abstract;
b.body = entry.body;
b.author = entry.author;
b.link = entry.link;
b.published = entry.datePublished;
return b;
}
+ (Feed*)createFeedFrom:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context alreadyRead:(NSArray<NSString*>*)urls {
Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context]; Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context];
a.title = obj.title; a.title = obj.title;
a.subtitle = obj.subtitle; a.subtitle = obj.subtitle;
a.link = obj.link; a.link = obj.link;
for (RSParsedArticle *entry in obj.articles) { for (RSParsedArticle *article in obj.articles) {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context]; FeedItem *b = [self createFeedItemFrom:article inContext:context];
b.guid = entry.guid; if ([urls containsObject:b.link]) {
b.title = entry.title; b.unread = NO;
b.abstract = entry.abstract; }
b.body = entry.body;
b.author = entry.author;
b.link = entry.link;
b.published = entry.datePublished;
// TODO: remove NSLog()
if (!entry.datePublished)
NSLog(@"No date for feed '%@'", obj.urlString);
[a addItemsObject:b]; [a addItemsObject:b];
} }
return a; return a;
} }
+ (NSArray<NSString*>*)alreadyReadURLsInFeed:(Feed*)local {
if (!local || !local.items) return nil;
NSMutableArray<NSString*> *mArr = [NSMutableArray arrayWithCapacity:local.items.count];
for (FeedItem *f in local.items) {
if (!f.unread) {
[mArr addObject:f.link];
}
}
return mArr;
}
@end @end