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

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

View File

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

View File

@@ -31,13 +31,22 @@
#pragma mark - Open & Save Panel
/// Display Open File Panel to select @c .opml file.
+ (void)showImportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc success:(nullable void(^)(NSArray<Feed*> *added))block {
/// 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 {
NSOpenPanel *op = [NSOpenPanel openPanel];
op.allowedFileTypes = @[@"opml"];
[op beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
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
@@ -98,9 +85,9 @@
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
@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];
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Import of %lu feed items", nil), count];
@@ -109,42 +96,43 @@
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", nil)];
alert.accessoryView = [self radioGroupCreate:@[NSLocalizedString(@"Append", nil),
NSLocalizedString(@"Overwrite", nil)]];
NSModalResponse code = [alert runModal];
if (code == NSAlertSecondButtonReturn) { // cancel button
return NO;
if ([alert runModal] == NSAlertFirstButtonReturn) {
return [self radioGroupSelection:alert.accessoryView];
}
if ([self radioGroupSelection:alert.accessoryView] == 1) { // overwrite selected
for (FeedGroup *g in [StoreCoordinator sortedListOfRootObjectsInContext:moc]) {
[moc deleteObject:g];
}
}
return YES;
return -1; // cancel button
}
/**
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 {
NSData *data = [NSData dataWithContentsOfURL:fileURL];
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 if ([self askToAppendOrOverwriteAlert:doc inContext:moc]) {
NSMutableArray<Feed*> *list = [NSMutableArray array];
int32_t idx = 0;
if (moc.deletedObjects.count == 0) // if there are deleted objects, user choose to overwrite all items
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);
+ (void)importOPMLDocument:(RSOPMLItem*)doc inContext:(NSManagedObjectContext*)moc {
NSInteger select = [self askToAppendOrOverwriteAlert:doc inContext:moc];
if (select < 0 || select > 1) // not a valid selection (or cancel button)
return;
[moc.undoManager beginUndoGrouping];
int32_t idx = 0;
if (select == 1) { // overwrite selected
for (FeedGroup *fg in [StoreCoordinator sortedListOfRootObjectsInContext:moc]) {
[moc deleteObject:fg]; // Not a batch delete request to support undo
}
[[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 {
NSXMLElement *head = [NSXMLElement elementWithName:@"head"];
[head addChild:[NSXMLElement elementWithName:@"title" stringValue:@"baRSS feeds"]];
[head addChild:[NSXMLElement elementWithName:@"ownerName" stringValue:@"baRSS"]];
[head addChild:[NSXMLElement elementWithName:@"dateCreated" stringValue:[self currentDayAsStringISO8601:YES]]];
head.children = @[[NSXMLElement elementWithName:@"title" stringValue:@"baRSS feeds"],
[NSXMLElement elementWithName:@"ownerName" stringValue:@"baRSS"],
[NSXMLElement elementWithName:@"dateCreated" stringValue:[self currentDayAsStringISO8601:YES]] ];
NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
for (FeedGroup *item in list) {
@@ -216,9 +204,8 @@
}
NSXMLElement *opml = [NSXMLElement elementWithName:@"opml"];
[opml addAttribute:[NSXMLNode attributeWithName:@"version" stringValue:@"1.0"]];
[opml addChild:head];
[opml addChild:body];
opml.attributes = @[[NSXMLNode attributeWithName:@"version" stringValue:@"1.0"]];
opml.children = @[head, body];
NSXMLDocument *xml = [NSXMLDocument documentWithRootElement:opml];
xml.version = @"1.0";

View File

@@ -27,10 +27,13 @@
#import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
#import "OpmlExport.h"
#import "FeedDownload.h"
@interface SettingsFeeds ()
@property (weak) IBOutlet NSOutlineView *outlineView;
@property (weak) IBOutlet NSTreeController *dataStore;
@property (weak) IBOutlet NSProgressIndicator *spinner;
@property (weak) IBOutlet NSTextField *spinnerLabel;
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong) NSUndoManager *undoManager;
@@ -44,6 +47,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)viewDidLoad {
[super viewDidLoad];
[self activateSpinner:([FeedDownload isUpdating] ? -1 : 0)]; // start spinner if update is in progress when preferences open
[self.outlineView registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]];
[self.dataStore setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
@@ -53,9 +57,61 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
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.
@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.
*/
- (void)saveWithUnpredictableChange {
NSSet<Feed*> *arr = [self.dataStore.managedObjectContext.insertedObjects
filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@", [Feed class]]];
// dont use unless you merge changes from main
// 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 restoreFeedCountsAndIndexPaths:[arr valueForKeyPath:@"objectID"]]; // main context will not create undo group
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
[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]) {
NSInteger tag = sender.menu.highlightedItem.tag;
if (tag == 101) {
[OpmlExport showImportDialog:self.view.window withTreeController:self.dataStore];
[OpmlExport showImportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
} else if (tag == 102) {
[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
- (BOOL)respondsToSelector:(SEL)aSelector {
if (aSelector == @selector(undo:)) return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0;
if (aSelector == @selector(redo:)) return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0;
if (aSelector == @selector(undo:))
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:)) {
BOOL outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]];
BOOL hasSelection = (self.dataStore.selectedNodes.count > 0);

View File

@@ -10,6 +10,8 @@
<connections>
<outlet property="dataStore" destination="JPf-gH-wxm" id="9qy-D6-L4R"/>
<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"/>
</connections>
</customObject>
@@ -182,20 +184,8 @@ CA
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canRemove" id="XYY-gx-tiN"/>
</connections>
</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">
<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"/>
<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"/>
@@ -208,9 +198,21 @@ CA
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="JCF-ey-YUL"/>
</connections>
</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">
<rect key="frame" x="295" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<rect key="frame" x="128" y="-1" width="25" height="23"/>
<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">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -219,6 +221,19 @@ CA
<action selector="shareMenu:" target="-2" id="JJq-7D-Bti"/>
</connections>
</button>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="fos-vP-s2s">
<rect key="frame" x="168" y="3" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="44U-lx-hnq">
<rect key="frame" x="190" y="4" width="112" height="14"/>
<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>
<point key="canvasLocation" x="27" y="882.5"/>
</customView>

View File

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