FeedDownload refactoring: everything parallel!

This commit is contained in:
relikd
2019-01-25 02:12:15 +01:00
parent 8e90ca742f
commit d5354bb681
17 changed files with 427 additions and 330 deletions

View File

@@ -35,14 +35,9 @@ All in all, the software is in a usable state. The remaining features will be ad
ToDo ToDo
---- ----
- [ ] Preferences
- [ ] ~~Choose status bar icon?~~
- [ ] Display license info (e.g., RSXML)
- [ ] Edit feed - [ ] Edit feed
- [ ] Show statistics - [ ] Show statistics
- [ ] 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 - [ ] Show time of next update
- [ ] Feeds with authentication - [ ] Feeds with authentication

View File

@@ -29,7 +29,7 @@
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)context; + (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)context;
+ (instancetype)appendToRootWithDefaultIntervalInContext:(NSManagedObjectContext*)moc; + (instancetype)appendToRootWithDefaultIntervalInContext:(NSManagedObjectContext*)moc;
- (void)calculateAndSetIndexPathString; - (void)calculateAndSetIndexPathString;
- (void)resetArticleCountAndIndexPathString; - (void)calculateAndSetUnreadCount;
- (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag; - (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag;
// Article properties // Article properties
- (NSArray<FeedArticle*>*)sortedArticles; - (NSArray<FeedArticle*>*)sortedArticles;
@@ -37,5 +37,5 @@
- (int)markAllItemsUnread; - (int)markAllItemsUnread;
// Icon // Icon
- (NSImage*)iconImage16; - (NSImage*)iconImage16;
- (BOOL)setIcon:(NSImage*)img replaceExisting:(BOOL)overwrite; - (BOOL)setIconImage:(NSImage*)img;
@end @end

View File

@@ -57,15 +57,11 @@
self.indexPath = pthStr; self.indexPath = pthStr;
} }
/// Reset attributes @c articleCount, @c unreadCount, and @c indexPath. /// Reset attributes @c unreadCount by counting number of articles. @note Remember to update global unread count.
- (void)resetArticleCountAndIndexPathString { - (void)calculateAndSetUnreadCount {
int16_t totalCount = (int16_t)self.articles.count; int32_t unreadCount = (int32_t)[[self.articles valueForKeyPath:@"@sum.unread"] integerValue];
int16_t unreadCount = (int16_t)[[self.articles valueForKeyPath:@"@sum.unread"] integerValue];
if (self.articleCount != totalCount)
self.articleCount = totalCount;
if (self.unreadCount != unreadCount) if (self.unreadCount != unreadCount)
self.unreadCount = unreadCount; // remember to update global total unread count self.unreadCount = unreadCount;
[self calculateAndSetIndexPathString];
} }
@@ -89,12 +85,10 @@
[self addMissingArticles:obj updateLinks:urls]; // will remove links in 'urls' that should be kept [self addMissingArticles:obj updateLinks:urls]; // will remove links in 'urls' that should be kept
[self deleteArticlesWithLink:urls]; // remove old, outdated articles [self deleteArticlesWithLink:urls]; // remove old, outdated articles
// Get new total article count and post unread-count-change notification // Get new total article count and post unread-count-change notification
int32_t totalCount = (int32_t)self.articles.count;
if (self.articleCount != totalCount)
self.articleCount = totalCount;
if (flag) { if (flag) {
NSNumber *cDiff = [NSNumber numberWithInteger:self.unreadCount - unreadBefore]; int32_t cDiff = self.unreadCount - unreadBefore;
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff]; if (cDiff != 0)
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:@(cDiff)];
} }
} }
@@ -237,7 +231,7 @@
fa.unread = !readFlag; fa.unread = !readFlag;
} }
int32_t oldCount = self.unreadCount; int32_t oldCount = self.unreadCount;
int32_t newCount = (readFlag ? 0 : self.articleCount); int32_t newCount = (readFlag ? 0 : (int32_t)self.articles.count);
if (self.unreadCount != newCount) if (self.unreadCount != newCount)
self.unreadCount = newCount; self.unreadCount = newCount;
return newCount - oldCount; return newCount - oldCount;
@@ -258,7 +252,7 @@
[img setSize:NSMakeSize(16, 16)]; [img setSize:NSMakeSize(16, 16)];
return img; return img;
} }
else if (self.articleCount == 0) else if (self.articles.count == 0)
{ {
static NSImage *warningIcon; static NSImage *warningIcon;
if (!warningIcon) { if (!warningIcon) {
@@ -277,22 +271,20 @@
} }
/** /**
Set (or overwrite) favicon icon or delete relationship if icon is @c nil. Set favicon icon or delete relationship if @c img is not a valid image.
@param overwrite If @c NO write image only if non is set already. Use @c YES if you want to @c nil. @return @c YES if icon was updated (core data did change).
*/ */
- (BOOL)setIcon:(NSImage*)img replaceExisting:(BOOL)overwrite { - (BOOL)setIconImage:(NSImage*)img {
if (overwrite || !self.icon) { // write if forced or image empty if (img && [img isValid]) {
if (img && [img isValid]) { if (!self.icon)
if (!self.icon) self.icon = [[FeedIcon alloc] initWithEntity:FeedIcon.entity insertIntoManagedObjectContext:self.managedObjectContext];
self.icon = [[FeedIcon alloc] initWithEntity:FeedIcon.entity insertIntoManagedObjectContext:self.managedObjectContext]; self.icon.icon = [img TIFFRepresentation];
self.icon.icon = [img TIFFRepresentation]; return YES;
return YES; } else if (self.icon) {
} else if (self.icon) { [self.managedObjectContext deleteObject:self.icon];
[self.managedObjectContext deleteObject:self.icon]; self.icon = nil;
self.icon = nil; return YES;
return YES;
}
} }
return NO; return NO;
} }

View File

@@ -29,11 +29,42 @@
// TODO: List of hidden preferences for readme // TODO: List of hidden preferences for readme
// TODO: Do we need to search for favicon in places other than '../favicon.ico'? // TODO: Do we need to search for favicon in places other than '../favicon.ico'?
/**
@c notification.object is @c NSNumber of type @c NSUInteger.
Represents number of feeds that are proccessed in background update. Sends @c 0 when all downloads are finished.
*/
static NSString *kNotificationBackgroundUpdateInProgress = @"baRSS-notification-background-update-in-progress";
/**
@c notification.object is @c NSManagedObjectID of type @c Feed.
Called whenever download of a feed finished and object was modified (not if statusCode 304).
*/
static NSString *kNotificationFeedUpdated = @"baRSS-notification-feed-updated"; static NSString *kNotificationFeedUpdated = @"baRSS-notification-feed-updated";
/**
@c notification.object is @c NSManagedObjectID of type @c Feed.
Called whenever the icon attribute of an item was updated.
*/
static NSString *kNotificationFeedIconUpdated = @"baRSS-notification-feed-icon-updated";
/**
@c notification.object is @c NSNumber of type @c BOOL.
@c YES if network became reachable. @c NO on connection lost.
*/
static NSString *kNotificationNetworkStatusChanged = @"baRSS-notification-network-status-changed"; static NSString *kNotificationNetworkStatusChanged = @"baRSS-notification-network-status-changed";
/**
@c notification.object is @c NSNumber of type @c NSInteger.
Represents a relative change (e.g., negative if items were marked read)
*/
static NSString *kNotificationTotalUnreadCountChanged = @"baRSS-notification-total-unread-count-changed"; static NSString *kNotificationTotalUnreadCountChanged = @"baRSS-notification-total-unread-count-changed";
/**
@c notification.object is either @c nil or @c NSNumber of type @c NSInteger.
If new count is known an absoulte number is passed.
Else @c nil if count has to be fetched from core data.
*/
static NSString *kNotificationTotalUnreadCountReset = @"baRSS-notification-total-unread-count-reset"; static NSString *kNotificationTotalUnreadCountReset = @"baRSS-notification-total-unread-count-reset";
/**
Internal developer method for benchmarking purposes.
*/
extern uint64_t dispatch_benchmark(size_t count, void (^block)(void)); extern uint64_t dispatch_benchmark(size_t count, void (^block)(void));
//void benchmark(char *desc, dispatch_block_t b){printf("%s: %llu ns\n", desc, dispatch_benchmark(1, b));} //void benchmark(char *desc, dispatch_block_t b){printf("%s: %llu ns\n", desc, dispatch_benchmark(1, b));}
#define benchmark(desc,block) printf(desc": %llu ns\n", dispatch_benchmark(1, block)); #define benchmark(desc,block) printf(desc": %llu ns\n", dispatch_benchmark(1, block));

View File

@@ -1,7 +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="14315.18" systemVersion="17G3025" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1"> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G4015" 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="articleCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/> <attribute name="indexPath" optional="YES" attributeType="String" 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"/>
@@ -48,7 +47,7 @@
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed" syncable="YES"/> <relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed" syncable="YES"/>
</entity> </entity>
<elements> <elements>
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="195"/> <element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="180"/>
<element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/> <element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/>
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="150"/> <element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="150"/>
<element name="FeedIcon" positionX="-202.79296875" positionY="137.71875" width="128" height="75"/> <element name="FeedIcon" positionX="-202.79296875" positionY="137.71875" width="128" height="75"/>

View File

@@ -26,6 +26,10 @@
@class Feed; @class Feed;
@interface FeedDownload : NSObject @interface FeedDownload : NSObject
@property (class, readonly) BOOL allowNetworkConnection;
@property (class, readonly) BOOL isUpdating;
@property (class, setter=setPaused:) BOOL isPaused;
// Register for network change notifications // Register for network change notifications
+ (void)registerNetworkChangeNotification; + (void)registerNetworkChangeNotification;
+ (void)unregisterNetworkChangeNotification; + (void)unregisterNetworkChangeNotification;
@@ -35,12 +39,8 @@
// Downloading // Downloading
+ (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(NSArray<RSHTMLMetadataFeedLink*> *list))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block; + (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(NSArray<RSHTMLMetadataFeedLink*> *list))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block;
+ (void)autoDownloadAndParseURL:(NSString*)urlStr; + (void)autoDownloadAndParseURL:(NSString*)urlStr;
+ (void)batchDownloadRSSAndFavicons:(NSArray<Feed*> *)list showErrorAlert:(BOOL)flag rssFinished:(void(^)(NSArray<Feed*> *successful, BOOL *cancelFavicons))blockXml finally:(void(^)(BOOL successful))blockFavicon; + (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block;
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block ; + (void)batchDownloadFeeds:(NSArray<Feed*> *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block;
// User interaction
+ (BOOL)allowNetworkConnection;
+ (BOOL)isPaused;
+ (void)setPaused:(BOOL)flag;
@end @end

View File

@@ -31,23 +31,24 @@
static SCNetworkReachabilityRef _reachability = NULL; static SCNetworkReachabilityRef _reachability = NULL;
static BOOL _isReachable = NO; static BOOL _isReachable = NO;
static BOOL _isUpdating = NO;
static BOOL _updatePaused = NO; static BOOL _updatePaused = NO;
static BOOL _nextUpdateIsForced = NO; static BOOL _nextUpdateIsForced = NO;
@implementation FeedDownload @implementation FeedDownload
#pragma mark - User Interaction - #pragma mark - User Interaction -
/// @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 { + (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); }
return (_isReachable && !_updatePaused);
} /// @return @c YES if batch update is running
+ (BOOL)isUpdating { return _isUpdating; }
/// @return @c YES if update is paused by user. /// @return @c YES if update is paused by user.
+ (BOOL)isPaused { + (BOOL)isPaused { return _updatePaused; }
return _updatePaused;
}
/// Set paused flag and cancel timer regardless of network connectivity. /// Set paused flag and cancel timer regardless of network connectivity.
+ (void)setPaused:(BOOL)flag { + (void)setPaused:(BOOL)flag {
@@ -94,7 +95,7 @@ static BOOL _nextUpdateIsForced = NO;
if (![self allowNetworkConnection]) // timer will restart once connection exists if (![self allowNetworkConnection]) // timer will restart once connection exists
return; return;
_nextUpdateIsForced = YES; _nextUpdateIsForced = YES;
[self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.2]]; [self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.05]];
} }
/** /**
@@ -130,31 +131,13 @@ static BOOL _nextUpdateIsForced = NO;
NSArray<Feed*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc]; NSArray<Feed*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc];
//NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); //NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early.");
[FeedDownload batchUpdateFeeds:list showErrorAlert:NO finally:^(NSArray<Feed*> *successful, NSArray<Feed*> *failed) { [self batchDownloadFeeds:list favicons:updateAll showErrorAlert:NO finally:^{
[self saveContext:moc andPostChanges:successful]; [StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
[moc reset]; [moc reset];
if (updateAll) { // forced update will also download missing feed icons
NSArray<Feed*> *missingIcons = [StoreCoordinator listOfFeedsMissingIconsInContext:moc];
[self batchDownloadFavicons:missingIcons replaceExisting:NO finally:^{
[self saveContext:moc andPostChanges:successful];
[moc reset];
}];
}
[self resumeUpdates]; // always reset the timer [self resumeUpdates]; // always reset the timer
}]; }];
} }
/**
Perform save on context and all parents. Then post @c FeedUpdated notification.
*/
+ (void)saveContext:(NSManagedObjectContext*)moc andPostChanges:(NSArray<Feed*>*)changedFeeds {
[StoreCoordinator saveContext:moc andParent:YES];
if (changedFeeds && changedFeeds.count > 0) {
NSArray<NSManagedObjectID*> *list = [changedFeeds valueForKeyPath:@"objectID"];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:list];
}
}
#pragma mark - Request Generator - #pragma mark - Request Generator -
@@ -179,20 +162,20 @@ static BOOL _nextUpdateIsForced = NO;
req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
req.HTTPShouldHandleCookies = NO; req.HTTPShouldHandleCookies = NO;
// req.timeoutInterval = 30; // req.timeoutInterval = 30;
// [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"];
// [req setValue:@"wII2pETT9EGmlqyCHBFJpm25/7w" forHTTPHeaderField:@"If-None-Match"]; // ETag
return req; return req;
} }
/// @return New request with etag and modified headers set. /// @return New request with etag and modified headers set (or not, if @c flag @c == @c YES ).
+ (NSURLRequest*)newRequest:(FeedMeta*)meta { + (NSURLRequest*)newRequest:(FeedMeta*)meta ignoreCache:(BOOL)flag {
NSMutableURLRequest *req = [self newRequestURL:meta.url]; NSMutableURLRequest *req = [self newRequestURL:meta.url];
NSString* etag = [meta.etag stringByReplacingOccurrencesOfString:@"-gzip" withString:@""]; if (!flag) {
if (meta.modified.length > 0) NSString* etag = [meta.etag stringByReplacingOccurrencesOfString:@"-gzip" withString:@""];
[req setValue:meta.modified forHTTPHeaderField:@"If-Modified-Since"]; if (meta.modified.length > 0)
if (etag.length > 0) [req setValue:meta.modified forHTTPHeaderField:@"If-Modified-Since"];
[req setValue:etag forHTTPHeaderField:@"If-None-Match"]; // ETag if (etag.length > 0)
if (!_nextUpdateIsForced) // any FeedMeta-request that is not forced, is a background update [req setValue:etag forHTTPHeaderField:@"If-None-Match"]; // ETag
}
if (!_nextUpdateIsForced) // any request that is not forced, is a background update
req.networkServiceType = NSURLNetworkServiceTypeBackground; req.networkServiceType = NSURLNetworkServiceTypeBackground;
return req; return req;
} }
@@ -246,7 +229,7 @@ static BOOL _nextUpdateIsForced = NO;
@note @c askUser will not be called if url is XML already. @note @c askUser will not be called if url is XML already.
@param urlStr XML URL or HTTP URL that will be parsed to find feed URLs. @param urlStr XML URL or HTTP URL that will be parsed to find feed URLs.
@param askUser Use @c list to present user a list of detected feed URLs. Always before @c block. @param askUser Use @c list to present user a list of detected feed URLs.
@param block Called after webpage has been fully parsed (including html autodetect). @param block Called after webpage has been fully parsed (including html autodetect).
*/ */
+ (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(NSArray<RSHTMLMetadataFeedLink*> *list))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block { + (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(NSArray<RSHTMLMetadataFeedLink*> *list))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block {
@@ -273,41 +256,46 @@ static BOOL _nextUpdateIsForced = NO;
} }
/** /**
Start download request with existing @c Feed object. Reuses etag and modified headers. Start download request with existing @c Feed object. Reuses etag and modified headers (unless articles count is 0).
@param feed @c Feed on which the update is executed. @note Will post a @c kNotificationFeedUpdated notification if download was successful and @b not status code 304.
@param group Mutex to count completion of all downloads.
@param alert If @c YES display Error Popup to user. @param alert If @c YES display Error Popup to user.
@param successful Empty, mutable list that will be returned in @c batchUpdateFeeds:finally:showErrorAlert: finally block @param block Parameter @c success is only @c YES if download was successful or if status code is 304 (not modified).
@param failed Empty, mutable list that will be returned in @c batchUpdateFeeds:finally:showErrorAlert: finally block
*/ */
+ (void)downloadFeed:(Feed*)feed group:(dispatch_group_t)group + (void)backgroundUpdateFeed:(Feed*)feed showErrorAlert:(BOOL)alert finally:(nullable void(^)(BOOL success))block {
errorAlert:(BOOL)alert
successful:(nonnull NSMutableArray<Feed*>*)successful
failed:(nonnull NSMutableArray<Feed*>*)failed
{
if (![self allowNetworkConnection]) { if (![self allowNetworkConnection]) {
[failed addObject:feed]; if (block) block(NO);
return; return;
} }
dispatch_group_enter(group); NSManagedObjectID *oid = feed.objectID;
[self parseFeedRequest:[self newRequest:feed.meta] xmlBlock:nil feedBlock:^(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response) { NSManagedObjectContext *moc = feed.managedObjectContext;
if (!feed.isDeleted) { NSURLRequest *req = [self newRequest:feed.meta ignoreCache:(feed.articles.count == 0)];
if (error) { NSString *reqURL = req.URL.absoluteString;
if (alert) { [self parseFeedRequest:req xmlBlock:nil feedBlock:^(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response) {
NSAlert *alertPopup = [NSAlert alertWithError:error]; Feed *f = [moc objectWithID:oid];
alertPopup.informativeText = [NSString stringWithFormat:@"Error loading source: %@", response.URL.absoluteString]; BOOL success = NO;
[alertPopup runModal]; BOOL needsNotification = NO;
} if (error) {
[feed.meta setErrorAndPostponeSchedule]; if (alert) {
[failed addObject:feed]; NSAlert *alertPopup = [NSAlert alertWithError:error];
} else { alertPopup.informativeText = [NSString stringWithFormat:@"Error loading source: %@", reqURL];
[feed.meta setSucessfulWithResponse:response]; [alertPopup runModal];
if (rss) [feed updateWithRSS:rss postUnreadCountChange:YES]; }
[successful addObject:feed]; // will be added even if statusCode == 304 (rss == nil) // TODO: don't increase error count on forced update
[f.meta setErrorAndPostponeSchedule];
} else {
success = YES;
[f.meta setSucessfulWithResponse:response];
if (rss) {
[f updateWithRSS:rss postUnreadCountChange:YES];
needsNotification = YES;
} }
} }
dispatch_group_leave(group); [StoreCoordinator saveContext:moc andParent:NO];
if (needsNotification)
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:oid];
if (block) block(success);
}]; }];
} }
@@ -322,113 +310,88 @@ static BOOL _nextUpdateIsForced = NO;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc]; Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc];
f.meta.url = url; f.meta.url = url;
[self batchDownloadRSSAndFavicons:@[f] showErrorAlert:YES rssFinished:^(NSArray<Feed *> *successful, BOOL *cancelFavicons) { [self backgroundUpdateBoth:f favicon:YES alert:YES finally:^(BOOL successful){
if (successful.count == 0) { if (!successful) {
*cancelFavicons = YES; [moc deleteObject:f.group];
} else {
[self saveContext:moc andPostChanges:successful];
}
} finally:^(BOOL successful) {
if (successful) {
[StoreCoordinator saveContext:moc andParent:YES];
} else {
[moc rollback];
} }
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset]; [moc reset];
}]; }];
} }
/** /**
Perform a download /update request for the feed data and download missing favicons. Start download of feed xml, then continue with favicon download (optional).
If neither block is set, favicons will be downloaded and stored automatically.
However, you should handle the case @param fav If @c YES continue with favicon download after xml download finished.
@param alert If @c YES display Error Popup to user.
@param list List of feeds that need update. Its sufficient if @c feed.meta.url is set. @param block Parameter @c success is @c YES if xml download succeeded (regardless of favicon result).
@param flag If @c YES display Error Popup to user.
@param blockXml Called after XML is downloaded and parsed.
Parameter @c successful is list of feeds that were downloaded.
Set @c cancelFavicons to @c YES to call @c finally block without downloading favicons. Default: @c NO.
@param blockFavicon Called after all downloads are finished.
@c successful is set to @c NO if favicon download was prohibited in @c blockXml or list is empty.
*/ */
+ (void)batchDownloadRSSAndFavicons:(NSArray<Feed*> *)list + (void)backgroundUpdateBoth:(Feed*)feed favicon:(BOOL)fav alert:(BOOL)alert finally:(nullable void(^)(BOOL success))block {
showErrorAlert:(BOOL)flag [self backgroundUpdateFeed:feed showErrorAlert:alert finally:^(BOOL success) {
rssFinished:(void(^)(NSArray<Feed*> *successful, BOOL * cancelFavicons))blockXml if (fav && success) {
finally:(void(^)(BOOL successful))blockFavicon [self backgroundUpdateFavicon:feed replaceExisting:NO finally:^{
{ if (block) block(YES);
[self batchUpdateFeeds:list showErrorAlert:flag finally:^(NSArray<Feed*> *successful, NSArray<Feed*> *failed) {
BOOL cancelFaviconsDownload = NO;
if (blockXml) {
blockXml(successful, &cancelFaviconsDownload);
}
if (cancelFaviconsDownload || successful.count == 0) {
if (blockFavicon) blockFavicon(NO);
} else {
[self batchDownloadFavicons:successful replaceExisting:NO finally:^{
if (blockFavicon) blockFavicon(YES);
}]; }];
} else {
if (block) block(success);
} }
}]; }];
} }
/** /**
Create download list of feed URLs and download them all at once. Finally, notify when all finished. Start download of all feeds in list. Either with or without favicons.
@param list Download list using @c feed.meta.url as download url. (while reusing etag and modified headers) @param list Download list using @c feed.meta.url as download url. (while reusing etag and modified headers)
@param flag If @c YES display Error Popup to user. @param fav If @c YES continue with favicon download after xml download finished.
@param block Called after all downloads finished @b OR if list is empty (in that case both parameters are @c nil ). @param alert If @c YES display Error Popup to user.
*/
+ (void)batchUpdateFeeds:(NSArray<Feed*> *)list showErrorAlert:(BOOL)flag finally:(void(^)(NSArray<Feed*> *successful, NSArray<Feed*> *failed))block {
if (!list || list.count == 0) {
if (block) block(nil, nil);
return;
}
// else, process all feed items in a batch
NSMutableArray<Feed*> *successful = [NSMutableArray arrayWithCapacity:list.count];
NSMutableArray<Feed*> *failed = [NSMutableArray arrayWithCapacity:list.count];
dispatch_group_t group = dispatch_group_create();
for (Feed *feed in list) {
[self downloadFeed:feed group:group errorAlert:flag successful:successful failed:failed];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (block) block(successful, failed);
});
}
#pragma mark - Favicon -
/**
Create download list of @c favicon.ico URLs and save downloaded images to persistent store.
@param list Download list using @c feed.link as download url. If empty fall back to @c feed.meta.url
@param flag If @c YES display Error Popup to user.
@param block Called after all downloads finished. @param block Called after all downloads finished.
*/ */
+ (void)batchDownloadFavicons:(NSArray<Feed*> *)list replaceExisting:(BOOL)flag finally:(os_block_t)block { + (void)batchDownloadFeeds:(NSArray<Feed*> *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block {
_isUpdating = YES;
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationBackgroundUpdateInProgress object:@(list.count)];
dispatch_group_t group = dispatch_group_create(); dispatch_group_t group = dispatch_group_create();
for (Feed *f in list) { for (Feed *f in list) {
if (!flag && f.icon != nil) {
continue; // skip existing icons if replace == NO
}
NSManagedObjectID *oid = f.objectID;
NSManagedObjectContext *moc = f.managedObjectContext;
NSString *faviconURL = (f.link.length > 0 ? f.link : f.meta.url);
dispatch_group_enter(group); dispatch_group_enter(group);
[self downloadFavicon:faviconURL finished:^(NSImage *img) { [self backgroundUpdateBoth:f favicon:fav alert:alert finally:^(BOOL success){
Feed *feed = [moc objectWithID:oid]; // should also work if context was reset
[feed setIcon:img replaceExisting:flag];
dispatch_group_leave(group); dispatch_group_leave(group);
}]; }];
} }
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (block) block(); if (block) block();
_isUpdating = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationBackgroundUpdateInProgress object:@(0)];
}); });
} }
#pragma mark - Download Favicon -
/**
Start favicon download request on existing @c Feed object.
@note Will post a @c kNotificationFeedIconUpdated notification if icon was updated.
@param overwrite If @c YES and icon is present already, @c block will return immediatelly.
*/
+ (void)backgroundUpdateFavicon:(Feed*)feed replaceExisting:(BOOL)overwrite finally:(nullable os_block_t)block {
if (!overwrite && feed.icon != nil) {
if (block) block();
return; // skip existing icons if replace == NO
}
NSManagedObjectID *oid = feed.objectID;
NSManagedObjectContext *moc = feed.managedObjectContext;
NSString *faviconURL = (feed.link.length > 0 ? feed.link : feed.meta.url);
[self downloadFavicon:faviconURL finished:^(NSImage *img) {
Feed *f = [moc objectWithID:oid];
if (f && [f setIconImage:img]) {
[StoreCoordinator saveContext:moc andParent:NO];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedIconUpdated object:oid];
}
if (block) block();
}];
}
/// Download favicon located at http://.../ @c favicon.ico. Callback @c block will be called on main thread. /// Download favicon located at http://.../ @c favicon.ico. Callback @c block will be called on main thread.
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block { + (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

View File

@@ -126,7 +126,7 @@
if (self.didDownloadFeed) { if (self.didDownloadFeed) {
[meta setEtag:self.httpEtag modified:self.httpDate]; [meta setEtag:self.httpEtag modified:self.httpDate];
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES]; [feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
[feed setIcon:self.favicon replaceExisting:YES]; [feed setIconImage:self.favicon];
} }
} }
@@ -161,7 +161,6 @@
if (self.modalSheet.didCloseAndCancel) if (self.modalSheet.didCloseAndCancel)
return; return;
[self preDownload]; [self preDownload];
// TODO: parse webpage to find feed links instead (automatic link detection)
[FeedDownload newFeed:self.previousURL askUser:^NSString *(NSArray<RSHTMLMetadataFeedLink *> *list) { [FeedDownload newFeed:self.previousURL askUser:^NSString *(NSArray<RSHTMLMetadataFeedLink *> *list) {
return [self letUserChooseXmlUrlFromList:list]; return [self letUserChooseXmlUrlFromList:list];
} block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) { } block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) {

View File

@@ -26,6 +26,6 @@
@class Feed; @class Feed;
@interface OpmlExport : NSObject @interface OpmlExport : NSObject
+ (void)showImportDialog:(NSWindow*)window withTreeController:(NSTreeController*)tree; + (void)showImportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc;
+ (void)showExportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc; + (void)showExportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc;
@end @end

View File

@@ -31,13 +31,22 @@
#pragma mark - Open & Save Panel #pragma mark - Open & Save Panel
/// Display Open File Panel to select @c .opml file. /// Display Open File Panel to select @c .opml file. Perform web requests (feed data & icon) within a single undo group.
+ (void)showImportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc success:(nullable void(^)(NSArray<Feed*> *added))block { + (void)showImportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc {
NSOpenPanel *op = [NSOpenPanel openPanel]; NSOpenPanel *op = [NSOpenPanel openPanel];
op.allowedFileTypes = @[@"opml"]; op.allowedFileTypes = @[@"opml"];
[op beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) { [op beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
if (result == NSModalResponseOK) { if (result == NSModalResponseOK) {
[self importFeedData:op.URL inContext:moc success:block]; NSData *data = [NSData dataWithContentsOfURL:op.URL];
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:@"opml-file-import"];
RSOPMLParser *parser = [RSOPMLParser parserWithXMLData:xml];
[parser parseAsync:^(RSOPMLItem * _Nullable doc, NSError * _Nullable error) {
if (error) {
[NSApp presentError:error];
} else {
[self importOPMLDocument:doc inContext:moc];
}
}];
} }
}]; }];
} }
@@ -67,28 +76,6 @@
}]; }];
} }
/// Handle import dialog and perform web requests (feed data & icon). Creates a single undo group.
+ (void)showImportDialog:(NSWindow*)window withTreeController:(NSTreeController*)tree {
NSManagedObjectContext *moc = tree.managedObjectContext;
//[moc refreshAllObjects];
[moc.undoManager beginUndoGrouping];
[self showImportDialog:window withContext:moc success:^(NSArray<Feed *> *added) {
[StoreCoordinator saveContext:moc andParent:YES];
[FeedDownload batchDownloadRSSAndFavicons:added showErrorAlert:YES rssFinished:^(NSArray<Feed *> *successful, BOOL *cancelFavicons) {
if (successful.count > 0)
[StoreCoordinator saveContext:moc andParent:YES];
// we need to post a reset, since after deletion total unread count is wrong
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
} finally:^(BOOL successful) {
[moc.undoManager endUndoGrouping];
if (successful) {
[StoreCoordinator saveContext:moc andParent:YES];
[tree rearrangeObjects]; // rearrange, because no new items appread instead only icon attrib changed
}
}];
}];
}
#pragma mark - Import #pragma mark - Import
@@ -98,9 +85,9 @@
If user chooses to replace existing items, perform core data request to delete all feeds. If user chooses to replace existing items, perform core data request to delete all feeds.
@param document Used to count feed items that will be imported @param document Used to count feed items that will be imported
@return @c NO if user clicks 'Cancel' button. @c YES otherwise. @return @c -1: User clicked 'Cancel' button. @c 0: Append items. @c 1: Overwrite items.
*/ */
+ (BOOL)askToAppendOrOverwriteAlert:(RSOPMLItem*)document inContext:(NSManagedObjectContext*)moc { + (NSInteger)askToAppendOrOverwriteAlert:(RSOPMLItem*)document inContext:(NSManagedObjectContext*)moc {
NSUInteger count = [self recursiveNumberOfFeeds:document]; NSUInteger count = [self recursiveNumberOfFeeds:document];
NSAlert *alert = [[NSAlert alloc] init]; NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Import of %lu feed items", nil), count]; alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Import of %lu feed items", nil), count];
@@ -109,42 +96,43 @@
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel", nil)];
alert.accessoryView = [self radioGroupCreate:@[NSLocalizedString(@"Append", nil), alert.accessoryView = [self radioGroupCreate:@[NSLocalizedString(@"Append", nil),
NSLocalizedString(@"Overwrite", nil)]]; NSLocalizedString(@"Overwrite", nil)]];
NSModalResponse code = [alert runModal];
if (code == NSAlertSecondButtonReturn) { // cancel button if ([alert runModal] == NSAlertFirstButtonReturn) {
return NO; return [self radioGroupSelection:alert.accessoryView];
} }
if ([self radioGroupSelection:alert.accessoryView] == 1) { // overwrite selected return -1; // cancel button
for (FeedGroup *g in [StoreCoordinator sortedListOfRootObjectsInContext:moc]) {
[moc deleteObject:g];
}
}
return YES;
} }
/** /**
Perform import of @c FeedGroup items. Perform import of @c FeedGroup items.
@param block Called after import finished. Parameter @c added is the list of inserted @c Feed items.
*/ */
+ (void)importFeedData:(NSURL*)fileURL inContext:(NSManagedObjectContext*)moc success:(nullable void(^)(NSArray<Feed*> *added))block { + (void)importOPMLDocument:(RSOPMLItem*)doc inContext:(NSManagedObjectContext*)moc {
NSData *data = [NSData dataWithContentsOfURL:fileURL]; NSInteger select = [self askToAppendOrOverwriteAlert:doc inContext:moc];
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:@"opml-file-import"]; if (select < 0 || select > 1) // not a valid selection (or cancel button)
RSOPMLParser *parser = [RSOPMLParser parserWithXMLData:xml]; return;
[parser parseAsync:^(RSOPMLItem * _Nullable doc, NSError * _Nullable error) {
if (error) { [moc.undoManager beginUndoGrouping];
[NSApp presentError:error];
} else if ([self askToAppendOrOverwriteAlert:doc inContext:moc]) { int32_t idx = 0;
NSMutableArray<Feed*> *list = [NSMutableArray array]; if (select == 1) { // overwrite selected
int32_t idx = 0; for (FeedGroup *fg in [StoreCoordinator sortedListOfRootObjectsInContext:moc]) {
if (moc.deletedObjects.count == 0) // if there are deleted objects, user choose to overwrite all items [moc deleteObject:fg]; // Not a batch delete request to support undo
idx = (int32_t)[StoreCoordinator numberRootItemsInContext:moc];
for (RSOPMLItem *item in doc.children) {
[self importFeed:item parent:nil index:idx inContext:moc appendToList:list];
idx += 1;
}
if (block) block(list);
} }
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:@(0)];
} else {
idx = (int32_t)[StoreCoordinator numberRootItemsInContext:moc];
}
NSMutableArray<Feed*> *list = [NSMutableArray array];
for (RSOPMLItem *item in doc.children) {
[self importFeed:item parent:nil index:idx inContext:moc appendToList:list];
idx += 1;
}
// Persist state, because on crash we have at least inserted items (without articles & icons)
[StoreCoordinator saveContext:moc andParent:YES];
[FeedDownload batchDownloadFeeds:list favicons:YES showErrorAlert:YES finally:^{
[StoreCoordinator saveContext:moc andParent:YES];
[moc.undoManager endUndoGrouping];
}]; }];
} }
@@ -206,9 +194,9 @@
*/ */
+ (NSXMLDocument*)xmlDocumentForFeeds:(NSArray<FeedGroup*>*)list hierarchical:(BOOL)flag { + (NSXMLDocument*)xmlDocumentForFeeds:(NSArray<FeedGroup*>*)list hierarchical:(BOOL)flag {
NSXMLElement *head = [NSXMLElement elementWithName:@"head"]; NSXMLElement *head = [NSXMLElement elementWithName:@"head"];
[head addChild:[NSXMLElement elementWithName:@"title" stringValue:@"baRSS feeds"]]; head.children = @[[NSXMLElement elementWithName:@"title" stringValue:@"baRSS feeds"],
[head addChild:[NSXMLElement elementWithName:@"ownerName" stringValue:@"baRSS"]]; [NSXMLElement elementWithName:@"ownerName" stringValue:@"baRSS"],
[head addChild:[NSXMLElement elementWithName:@"dateCreated" stringValue:[self currentDayAsStringISO8601:YES]]]; [NSXMLElement elementWithName:@"dateCreated" stringValue:[self currentDayAsStringISO8601:YES]] ];
NSXMLElement *body = [NSXMLElement elementWithName:@"body"]; NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
for (FeedGroup *item in list) { for (FeedGroup *item in list) {
@@ -216,9 +204,8 @@
} }
NSXMLElement *opml = [NSXMLElement elementWithName:@"opml"]; NSXMLElement *opml = [NSXMLElement elementWithName:@"opml"];
[opml addAttribute:[NSXMLNode attributeWithName:@"version" stringValue:@"1.0"]]; opml.attributes = @[[NSXMLNode attributeWithName:@"version" stringValue:@"1.0"]];
[opml addChild:head]; opml.children = @[head, body];
[opml addChild:body];
NSXMLDocument *xml = [NSXMLDocument documentWithRootElement:opml]; NSXMLDocument *xml = [NSXMLDocument documentWithRootElement:opml];
xml.version = @"1.0"; xml.version = @"1.0";

View File

@@ -27,10 +27,13 @@
#import "Feed+Ext.h" #import "Feed+Ext.h"
#import "FeedGroup+Ext.h" #import "FeedGroup+Ext.h"
#import "OpmlExport.h" #import "OpmlExport.h"
#import "FeedDownload.h"
@interface SettingsFeeds () @interface SettingsFeeds ()
@property (weak) IBOutlet NSOutlineView *outlineView; @property (weak) IBOutlet NSOutlineView *outlineView;
@property (weak) IBOutlet NSTreeController *dataStore; @property (weak) IBOutlet NSTreeController *dataStore;
@property (weak) IBOutlet NSProgressIndicator *spinner;
@property (weak) IBOutlet NSTextField *spinnerLabel;
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes; @property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong) NSUndoManager *undoManager; @property (strong) NSUndoManager *undoManager;
@@ -44,6 +47,7 @@ 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]]];
@@ -53,9 +57,61 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext]; self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
self.dataStore.managedObjectContext.undoManager = self.undoManager; self.dataStore.managedObjectContext.undoManager = self.undoManager;
self.dataStore.managedObjectContext.automaticallyMergesChangesFromParent = NO;
// Register for notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateIcon:) name:kNotificationFeedUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateIcon:) name:kNotificationFeedIconUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateInProgress:) name:kNotificationBackgroundUpdateInProgress object:nil];
} }
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notification callback methods
/// Callback method fired when feeds have been updated in the background.
- (void)updateIcon:(NSNotification*)notify {
NSManagedObjectID *oid = notify.object;
NSManagedObjectContext *moc = self.dataStore.managedObjectContext;
Feed *feed = [moc objectRegisteredForID:oid];
if (feed) {
if (self.undoManager.groupingLevel == 0) // don't mess around if user is editing something
[moc refreshObject:feed mergeChanges:YES];
[self.dataStore rearrangeObjects];
}
}
/// Callback method fired when background feed update begins and ends.
- (void)updateInProgress:(NSNotification*)notify {
[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
/** /**
Refresh current context from parent context and start new undo grouping. Refresh current context from parent context and start new undo grouping.
@note Should be balanced with @c endCoreDataChangeUndoChanges: @note Should be balanced with @c endCoreDataChangeUndoChanges:
@@ -89,10 +145,13 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
After the user did undo or redo we can't ensure integrity without doing some additional work. After the user did undo or redo we can't ensure integrity without doing some additional work.
*/ */
- (void)saveWithUnpredictableChange { - (void)saveWithUnpredictableChange {
NSSet<Feed*> *arr = [self.dataStore.managedObjectContext.insertedObjects // dont use unless you merge changes from main
filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@", [Feed class]]]; // NSManagedObjectContext *moc = self.dataStore.managedObjectContext;
// NSPredicate *pred = [NSPredicate predicateWithFormat:@"class == %@", [FeedArticle class]];
// NSInteger del = [[[moc.deletedObjects filteredSetUsingPredicate:pred] valueForKeyPath:@"@sum.unread"] integerValue];
// NSInteger ins = [[[moc.insertedObjects filteredSetUsingPredicate:pred] valueForKeyPath:@"@sum.unread"] integerValue];
// NSLog(@"%ld, %ld", del, ins);
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES]; [StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
[StoreCoordinator restoreFeedCountsAndIndexPaths:[arr valueForKeyPath:@"objectID"]]; // main context will not create undo group
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
[self.dataStore rearrangeObjects]; // update ordering [self.dataStore rearrangeObjects]; // update ordering
} }
@@ -150,7 +209,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
if ([sender.menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0,sender.frame.size.height) inView:sender]) { if ([sender.menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0,sender.frame.size.height) inView:sender]) {
NSInteger tag = sender.menu.highlightedItem.tag; NSInteger tag = sender.menu.highlightedItem.tag;
if (tag == 101) { if (tag == 101) {
[OpmlExport showImportDialog:self.view.window withTreeController:self.dataStore]; [OpmlExport showImportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
} else if (tag == 102) { } else if (tag == 102) {
[OpmlExport showExportDialog:self.view.window withContext:self.dataStore.managedObjectContext]; [OpmlExport showExportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
} }
@@ -323,8 +382,10 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
/// Returning @c NO will result in a Action-Not-Available-Buzzer sound /// Returning @c NO will result in a Action-Not-Available-Buzzer sound
- (BOOL)respondsToSelector:(SEL)aSelector { - (BOOL)respondsToSelector:(SEL)aSelector {
if (aSelector == @selector(undo:)) return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0; if (aSelector == @selector(undo:))
if (aSelector == @selector(redo:)) return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0; return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0 && ![FeedDownload isUpdating];
if (aSelector == @selector(redo:))
return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0 && ![FeedDownload isUpdating];
if (aSelector == @selector(copy:) || aSelector == @selector(enterPressed:)) { if (aSelector == @selector(copy:) || aSelector == @selector(enterPressed:)) {
BOOL outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]]; BOOL outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]];
BOOL hasSelection = (self.dataStore.selectedNodes.count > 0); BOOL hasSelection = (self.dataStore.selectedNodes.count > 0);

View File

@@ -10,6 +10,8 @@
<connections> <connections>
<outlet property="dataStore" destination="JPf-gH-wxm" id="9qy-D6-L4R"/> <outlet property="dataStore" destination="JPf-gH-wxm" id="9qy-D6-L4R"/>
<outlet property="outlineView" destination="wP9-Vd-f79" id="nKf-fc-7Np"/> <outlet property="outlineView" destination="wP9-Vd-f79" id="nKf-fc-7Np"/>
<outlet property="spinner" destination="fos-vP-s2s" id="zZp-Op-ftK"/>
<outlet property="spinnerLabel" destination="44U-lx-hnq" id="GGB-H5-7LV"/>
<outlet property="view" destination="zfc-Ie-Sdx" id="65R-bK-FDI"/> <outlet property="view" destination="zfc-Ie-Sdx" id="65R-bK-FDI"/>
</connections> </connections>
</customObject> </customObject>
@@ -182,20 +184,8 @@ CA
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canRemove" id="XYY-gx-tiN"/> <binding destination="JPf-gH-wxm" name="enabled" keyPath="canRemove" id="XYY-gx-tiN"/>
</connections> </connections>
</button> </button>
<button toolTip="Add new line separator" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kn9-pd-A47">
<rect key="frame" x="96" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" title="---" alternateTitle="Add separator" bezelStyle="smallSquare" image="NSPathTemplate" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="r9B-nl-XkX">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="addSeparator:" target="-2" id="dVQ-ge-moI"/>
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="2aK-XU-RUD"/>
</connections>
</button>
<button toolTip="Add new grouping folder" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jPg-sh-1Az"> <button toolTip="Add new grouping folder" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jPg-sh-1Az">
<rect key="frame" x="72" y="-1" width="25" height="23"/> <rect key="frame" x="64" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" alternateTitle="Add group" bezelStyle="smallSquare" image="NSPathTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="rPk-c8-lMe"> <buttonCell key="cell" type="smallSquare" alternateTitle="Add group" bezelStyle="smallSquare" image="NSPathTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="rPk-c8-lMe">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@@ -208,9 +198,21 @@ CA
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="JCF-ey-YUL"/> <binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="JCF-ey-YUL"/>
</connections> </connections>
</button> </button>
<button toolTip="Add new line separator" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kn9-pd-A47">
<rect key="frame" x="88" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" title="---" alternateTitle="Add separator" bezelStyle="smallSquare" image="NSPathTemplate" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="r9B-nl-XkX">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="addSeparator:" target="-2" id="dVQ-ge-moI"/>
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="2aK-XU-RUD"/>
</connections>
</button>
<button toolTip="Import or Export data" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6ul-3K-fOy"> <button toolTip="Import or Export data" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6ul-3K-fOy">
<rect key="frame" x="295" y="-1" width="25" height="23"/> <rect key="frame" x="128" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" alternateTitle="Export" bezelStyle="smallSquare" image="NSShareTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nrA-7c-1sL"> <buttonCell key="cell" type="smallSquare" alternateTitle="Export" bezelStyle="smallSquare" image="NSShareTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nrA-7c-1sL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@@ -219,6 +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">
<rect key="frame" x="190" y="4" width="112" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" 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>
</subviews> </subviews>
<point key="canvasLocation" x="27" y="882.5"/> <point key="canvasLocation" x="27" y="882.5"/>
</customView> </customView>

View File

@@ -25,6 +25,7 @@
#import "BarMenu.h" #import "BarMenu.h"
#import "UserPrefs.h" #import "UserPrefs.h"
#import "StoreCoordinator.h" #import "StoreCoordinator.h"
#import "Constants.h"
#import <ServiceManagement/ServiceManagement.h> #import <ServiceManagement/ServiceManagement.h>
@@ -60,8 +61,10 @@
} }
- (IBAction)fixCache:(NSButton *)sender { - (IBAction)fixCache:(NSButton *)sender {
[StoreCoordinator deleteUnreferencedFeeds]; NSUInteger deleted = [StoreCoordinator deleteUnreferenced];
[StoreCoordinator restoreFeedCountsAndIndexPaths:nil]; [StoreCoordinator restoreFeedCountsAndIndexPaths];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
NSLog(@"Removed %lu unreferenced core data entries.", deleted);
} }
- (IBAction)changeMenuBarIconSetting:(NSButton*)sender { - (IBAction)changeMenuBarIconSetting:(NSButton*)sender {

View File

@@ -24,5 +24,4 @@
@interface BarMenu : NSObject <NSMenuDelegate> @interface BarMenu : NSObject <NSMenuDelegate>
- (void)updateBarIcon; - (void)updateBarIcon;
- (void)asyncReloadUnreadCountAndUpdateBarIcon;
@end @end

View File

@@ -54,13 +54,14 @@
// Unread counter // Unread counter
self.unreadCountTotal = 0; self.unreadCountTotal = 0;
[self updateBarIcon]; [self updateBarIcon];
[self asyncReloadUnreadCountAndUpdateBarIcon]; [self asyncReloadUnreadCountAndUpdateBarIcon:nil];
// Register for notifications // Register for notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedIconUpdated:) name:kNotificationFeedIconUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(asyncReloadUnreadCountAndUpdateBarIcon) name:kNotificationTotalUnreadCountReset object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(asyncReloadUnreadCountAndUpdateBarIcon:) name:kNotificationTotalUnreadCountReset object:nil];
return self; return self;
} }
@@ -70,12 +71,20 @@
#pragma mark - Update Menu Bar Icon #pragma mark - Update Menu Bar Icon
/// Regardless of current unread count, perform new core data fetch on total unread count and update icon. /**
- (void)asyncReloadUnreadCountAndUpdateBarIcon { If notification has @c object use this object to set unread count directly.
dispatch_async(dispatch_get_main_queue(), ^{ If @c object is @c nil perform core data fetch on total unread count and update icon.
self.unreadCountTotal = [StoreCoordinator unreadCountForIndexPathString:nil]; */
- (void)asyncReloadUnreadCountAndUpdateBarIcon:(NSNotification*)notify {
if (notify.object) { // set unread count directly
self.unreadCountTotal = [[notify object] integerValue];
[self updateBarIcon]; [self updateBarIcon];
}); } else {
dispatch_async(dispatch_get_main_queue(), ^{
self.unreadCountTotal = [StoreCoordinator unreadCountForIndexPathString:nil];
[self updateBarIcon];
});
}
} }
/// Update menu bar icon and text according to unread count and user preferences. /// Update menu bar icon and text according to unread count and user preferences.
@@ -117,23 +126,38 @@
@param notify Notification object contains the unread count difference to the current count. May be negative. @param notify Notification object contains the unread count difference to the current count. May be negative.
*/ */
- (void)unreadCountChanged:(NSNotification*)notify { - (void)unreadCountChanged:(NSNotification*)notify {
self.unreadCountTotal += [[notify object] intValue]; self.unreadCountTotal += [[notify object] integerValue];
[self updateBarIcon]; [self updateBarIcon];
} }
/// Callback method fired when feeds have been updated in the background. /// Callback method fired when feeds have been updated in the background.
- (void)feedUpdated:(NSNotification*)notify { - (void)feedUpdated:(NSNotification*)notify {
[self updateFeed:notify.object updateIconOnly:NO];
}
- (void)feedIconUpdated:(NSNotification*)notify {
[self updateFeed:notify.object updateIconOnly:YES];
}
#pragma mark - Rebuild menu after background feed update
/**
Use this method to update a single menu item and all ancestors unread count.
If the menu isn't currently open, nothing will happen.
@param oid @c NSManagedObjectID must be a @c Feed instance object id.
*/
- (void)updateFeed:(NSManagedObjectID*)oid updateIconOnly:(BOOL)flag {
if (self.barItem.menu.numberOfItems > 0) { if (self.barItem.menu.numberOfItems > 0) {
// update items only if menu is already open (e.g., during background update) // update items only if menu is already open (e.g., during background update)
NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
for (NSManagedObjectID *oid in notify.object) { Feed *feed = [moc objectWithID:oid];
Feed *feed = [moc objectWithID:oid]; if ([feed isKindOfClass:[Feed class]]) {
if (!feed) continue;
NSMenu *menu = [self fixUnreadCountForSubmenus:feed]; NSMenu *menu = [self fixUnreadCountForSubmenus:feed];
if (!menu || menu.numberOfItems > 0) if (!flag) [self rebuiltFeedArticle:feed inMenu:menu]; // deepest menu level, feed items
[self rebuiltFeedArticle:feed inMenu:menu]; // deepest menu level, feed items
} }
[self.barItem.menu autoEnableMenuHeader:(self.unreadCountTotal > 0)]; // once per multi-feed update
[moc reset]; [moc reset];
} }
} }
@@ -145,18 +169,27 @@
*/ */
- (nullable NSMenu*)fixUnreadCountForSubmenus:(Feed*)feed { - (nullable NSMenu*)fixUnreadCountForSubmenus:(Feed*)feed {
NSMenu *menu = self.barItem.menu; NSMenu *menu = self.barItem.menu;
[menu autoEnableMenuHeader:(self.unreadCountTotal > 0)];
for (FeedGroup *parent in [feed.group allParents]) { for (FeedGroup *parent in [feed.group allParents]) {
NSInteger offset = [menu feedDataOffset]; NSInteger itemIndex = [menu feedDataOffset] + parent.sortIndex;
NSMenuItem *item = [menu itemAtIndex:offset + parent.sortIndex]; NSMenuItem *item = [menu itemAtIndex:itemIndex];
NSInteger unread = [item setTitleAndUnreadCount:parent]; NSInteger unread = [item setTitleAndUnreadCount:parent];
menu = item.submenu; menu = item.submenu;
if (parent == feed.group) {
// Always set icon. Will flip warning icon to default icon if article count changes.
item.image = [feed iconImage16];
item.enabled = (feed.articles.count > 0);
return menu;
}
if (!menu || menu.numberOfItems == 0) if (!menu || menu.numberOfItems == 0)
return nil; return nil;
if (unread == 0) // if != 0 then 'setTitleAndUnreadCount' was successful (UserPrefs visible) if (unread == 0) // if != 0 then 'setTitleAndUnreadCount' was successful (UserPrefs visible)
unread = [menu coreDataUnreadCount]; unread = [menu coreDataUnreadCount];
[menu autoEnableMenuHeader:(unread > 0)]; // of submenu (including: feed items menu) [menu autoEnableMenuHeader:(unread > 0)]; // of submenu but not articles menu (will be rebuild anyway)
} }
return menu; return nil;
} }
/** /**
@@ -166,6 +199,8 @@
@param menu Deepest menu level which contains only feed items. @param menu Deepest menu level which contains only feed items.
*/ */
- (void)rebuiltFeedArticle:(Feed*)feed inMenu:(NSMenu*)menu { - (void)rebuiltFeedArticle:(Feed*)feed inMenu:(NSMenu*)menu {
if (!menu || menu.numberOfItems == 0) // not opened yet
return;
if (self.currentOpenMenu != menu) { if (self.currentOpenMenu != menu) {
// if the menu isn't open, re-create it dynamically instead // if the menu isn't open, re-create it dynamically instead
menu.itemArray.firstObject.parentItem.submenu = [menu cleanInstanceCopy]; menu.itemArray.firstObject.parentItem.submenu = [menu cleanInstanceCopy];
@@ -360,6 +395,7 @@
Called when user clicks on 'Update all feeds' in the main menu (only). Called when user clicks on 'Update all feeds' in the main menu (only).
*/ */
- (void)updateAllFeeds:(NSMenuItem*)sender { - (void)updateAllFeeds:(NSMenuItem*)sender {
[self asyncReloadUnreadCountAndUpdateBarIcon:nil];
[FeedDownload forceUpdateAllFeeds]; [FeedDownload forceUpdateAllFeeds];
} }

View File

@@ -37,8 +37,9 @@
+ (NSInteger)numberRootItemsInContext:(NSManagedObjectContext*)moc; + (NSInteger)numberRootItemsInContext:(NSManagedObjectContext*)moc;
+ (NSArray<FeedGroup*>*)sortedListOfRootObjectsInContext:(NSManagedObjectContext*)moc; + (NSArray<FeedGroup*>*)sortedListOfRootObjectsInContext:(NSManagedObjectContext*)moc;
// Restore sound state // Restore sound state
+ (void)deleteUnreferencedFeeds; + (NSUInteger)deleteAllGroups;
+ (void)restoreFeedCountsAndIndexPaths:(NSArray<NSManagedObjectID*>*)list; + (NSUInteger)deleteUnreferenced;
+ (void)restoreFeedCountsAndIndexPaths;
+ (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc; + (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc;
+ (NSArray<Feed*>*)listOfFeedsMissingIconsInContext:(NSManagedObjectContext*)moc; + (NSArray<Feed*>*)listOfFeedsMissingIconsInContext:(NSManagedObjectContext*)moc;
@end @end

View File

@@ -24,8 +24,6 @@
#import "AppHook.h" #import "AppHook.h"
#import "Feed+Ext.h" #import "Feed+Ext.h"
#import <RSXML/RSXML.h>
@implementation StoreCoordinator @implementation StoreCoordinator
#pragma mark - Managing contexts #pragma mark - Managing contexts
@@ -40,7 +38,7 @@
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setParentContext:[self getMainContext]]; [context setParentContext:[self getMainContext]];
context.undoManager = nil; context.undoManager = nil;
context.automaticallyMergesChangesFromParent = YES; //context.automaticallyMergesChangesFromParent = YES;
return context; return context;
} }
@@ -178,46 +176,64 @@
#pragma mark - Restore Sound State #pragma mark - Restore Sound State
/** /**
Delete all @c Feed items where @c group @c = @c NULL. Perform batch delete on entities of type @c entity where @c column @c IS @c NULL. If @c column is @c nil, delete all rows.
*/ */
+ (void)deleteUnreferencedFeeds { + (NSUInteger)batchDelete:(NSEntityDescription*)entity nullAttribute:(NSString*)column inContext:(NSManagedObjectContext*)moc {
NSManagedObjectContext *moc = [self getMainContext]; NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: entity.name];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: Feed.entity.name]; if (column && column.length > 0) {
fr.predicate = [NSPredicate predicateWithFormat:@"group = NULL"]; // double nested string, otherwise column is not interpreted as such.
// using @count here to also find items where foreign key is set but referencing a non-existing object.
fr.predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"count(%@) == 0", column]];
}
NSBatchDeleteRequest *bdr = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fr]; NSBatchDeleteRequest *bdr = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fr];
bdr.resultType = NSBatchDeleteResultTypeCount;
NSError *err; NSError *err;
[moc executeRequest:bdr error:&err]; NSBatchDeleteResult *lol = [moc executeRequest:bdr error:&err];
if (err) NSLog(@"%@", err); if (err) NSLog(@"%@", err);
return [lol.result unsignedIntegerValue];
} }
/** /**
Iterate over all @c Feed and re-calculate @c unreadCount, @c articleCount and @c indexPath. Delete all @c FeedGroup items.
Restore will happend on the main context.
@param list A list of @c Feed objectIDs. Acts like a filter, if @c nil performs a fetch on all feed items.
*/ */
+ (void)restoreFeedCountsAndIndexPaths:(NSArray<NSManagedObjectID*>*)list { + (NSUInteger)deleteAllGroups {
NSManagedObjectContext *moc = [self getMainContext]; NSManagedObjectContext *moc = [self getMainContext];
if (!list) { NSUInteger deleted = [self batchDelete:FeedGroup.entity nullAttribute:nil inContext:moc];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: Feed.entity.name]; [self saveContext:moc andParent:YES];
[fr setResultType:NSManagedObjectIDResultType]; return deleted;
list = [self fetchAllRows:fr inContext:moc]; }
/**
Delete all @c Feed items where @c group @c = @c NULL and all @c FeedMeta, @c FeedIcon, @c FeedArticle where @c feed @c = @c NULL.
*/
+ (NSUInteger)deleteUnreferenced {
NSUInteger deleted = 0;
NSManagedObjectContext *moc = [self getMainContext];
deleted += [self batchDelete:Feed.entity nullAttribute:@"group" inContext:moc];
deleted += [self batchDelete:FeedMeta.entity nullAttribute:@"feed" inContext:moc];
deleted += [self batchDelete:FeedIcon.entity nullAttribute:@"feed" inContext:moc];
deleted += [self batchDelete:FeedArticle.entity nullAttribute:@"feed" inContext:moc];
[self saveContext:moc andParent:YES];
return deleted;
}
/**
Iterate over all @c Feed and re-calculate @c unreadCount and @c indexPath.
*/
+ (void)restoreFeedCountsAndIndexPaths {
NSManagedObjectContext *moc = [self getMainContext];
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: Feed.entity.name];
for (Feed *f in [self fetchAllRows:fr inContext:moc]) {
[f calculateAndSetUnreadCount];
[f calculateAndSetIndexPathString];
} }
[moc performBlock:^{ [self saveContext:moc andParent:YES];
for (NSManagedObjectID *moi in list) {
Feed *f = [moc objectWithID:moi];
if ([f isKindOfClass:[Feed class]])
[f resetArticleCountAndIndexPathString];
}
[self saveContext:moc andParent:YES];
}];
} }
/// @return All @c Feed items where @c articles.count @c == @c 0 /// @return All @c Feed items where @c articles.count @c == @c 0
+ (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc { + (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc {
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: Feed.entity.name]; NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName: Feed.entity.name];
// More accurate but with subquery on FeedArticle: "count(articles) == 0" fr.predicate = [NSPredicate predicateWithFormat:@"articles.@count == 0"];
fr.predicate = [NSPredicate predicateWithFormat:@"articleCount == 0"];
return [self fetchAllRows:fr inContext:moc]; return [self fetchAllRows:fr inContext:moc];
} }