diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 7915563f5..7f25a70c2 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -290,28 +290,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, delegate.refreshAll(for: self, completion: completion) } - public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping (() -> Void)) { - - feed.takeSettings(from: parsedFeed) - - database.update(feedID: feed.feedID, parsedFeed: parsedFeed) { (newArticles, updatedArticles) in - - var userInfo = [String: Any]() - if let newArticles = newArticles, !newArticles.isEmpty { - self.updateUnreadCounts(for: Set([feed])) - userInfo[UserInfoKey.newArticles] = newArticles - } - if let updatedArticles = updatedArticles, !updatedArticles.isEmpty { - userInfo[UserInfoKey.updatedArticles] = updatedArticles - } - userInfo[UserInfoKey.feeds] = Set([feed]) - - completion() - - NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo) - } + public func importOPML(_ opmlFile: URL, completion: @escaping (Result) -> Void) { + delegate.importOPML(for: self, opmlFile: opmlFile, completion: completion) } - + public func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { // Returns set of Articles whose statuses did change. @@ -413,12 +395,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, structureDidChange() } - public func importOPML(_ opmlDocument: RSOPMLDocument) { + func loadOPML(_ opmlDocument: RSOPMLDocument) { guard let children = opmlDocument.children else { return } - importOPMLItems(children, parentFolder: nil) + loadOPMLItems(children, parentFolder: nil) structureDidChange() DispatchQueue.main.async { @@ -573,6 +555,28 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, feedDictionaryNeedsUpdate = true } + func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping (() -> Void)) { + + feed.takeSettings(from: parsedFeed) + + database.update(feedID: feed.feedID, parsedFeed: parsedFeed) { (newArticles, updatedArticles) in + + var userInfo = [String: Any]() + if let newArticles = newArticles, !newArticles.isEmpty { + self.updateUnreadCounts(for: Set([feed])) + userInfo[UserInfoKey.newArticles] = newArticles + } + if let updatedArticles = updatedArticles, !updatedArticles.isEmpty { + userInfo[UserInfoKey.updatedArticles] = updatedArticles + } + userInfo[UserInfoKey.feeds] = Set([feed]) + + completion() + + NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo) + } + } + // MARK: - Container public func flattenedFeeds() -> Set { @@ -735,12 +739,12 @@ private extension Account { } func pullObjectsFromDisk() { - importAccountMetadata() - importFeedMetadata() - importOPMLFile(path: opmlFilePath) + loadAccountMetadata() + loadFeedMetadata() + loadOPMLFile(path: opmlFilePath) } - func importAccountMetadata() { + func loadAccountMetadata() { let url = URL(fileURLWithPath: metadataPath) guard let data = try? Data(contentsOf: url) else { metadata.delegate = self @@ -751,7 +755,7 @@ private extension Account { metadata.delegate = self } - func importFeedMetadata() { + func loadFeedMetadata() { let url = URL(fileURLWithPath: feedMetadataPath) guard let data = try? Data(contentsOf: url) else { return @@ -761,7 +765,7 @@ private extension Account { feedMetadata.values.forEach { $0.delegate = self } } - func importOPMLFile(path: String) { + func loadOPMLFile(path: String) { let opmlFileURL = URL(fileURLWithPath: path) var fileData: Data? do { @@ -794,7 +798,7 @@ private extension Account { } BatchUpdate.shared.perform { - importOPMLItems(children, parentFolder: nil) + loadOPMLItems(children, parentFolder: nil) } } @@ -901,7 +905,7 @@ private extension Account { feedDictionaryNeedsUpdate = false } - func createFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed { + func ensureFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed { let feedURL = opmlFeedSpecifier.feedURL let metadata = feedMetadata(feedURL: feedURL, feedID: feedURL) let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata) @@ -913,14 +917,14 @@ private extension Account { return feed } - func importOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) { + func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) { var feedsToAdd = Set() items.forEach { (item) in if let feedSpecifier = item.feedSpecifier { - let feed = createFeed(with: feedSpecifier) + let feed = ensureFeed(with: feedSpecifier) feedsToAdd.insert(feed) return } @@ -928,14 +932,14 @@ private extension Account { guard let folderName = item.titleFromAttributes else { // Folder doesn’t have a name, so it won’t be created, and its items will go one level up. if let itemChildren = item.children { - importOPMLItems(itemChildren, parentFolder: parentFolder) + loadOPMLItems(itemChildren, parentFolder: parentFolder) } return } if let folder = ensureFolder(with: folderName) { if let itemChildren = item.children { - importOPMLItems(itemChildren, parentFolder: folder) + loadOPMLItems(itemChildren, parentFolder: folder) } } } diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 144b7ff95..182938fcb 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -20,7 +20,8 @@ protocol AccountDelegate { var refreshProgress: DownloadProgress { get } func refreshAll(for account: Account, completion: (() -> Void)?) - + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) func deleteFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 25fa97483..4073476df 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -62,6 +62,10 @@ final class FeedbinAccountDelegate: AccountDelegate { } } + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { + + } + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { caller.renameTag(oldName: folder.name ?? "", newName: name) { result in diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 5e01774dd..44ee0e23d 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -7,6 +7,7 @@ // import Foundation +import RSParser import RSWeb public enum LocalAccountDelegateError: String, Error { @@ -35,6 +36,44 @@ final class LocalAccountDelegate: AccountDelegate { refresher.refreshFeeds(account.flattenedFeeds()) completion?() } + + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { + + var fileData: Data? + + do { + fileData = try Data(contentsOf: opmlFile) + } catch { + completion(.failure(error)) + return + } + + guard let opmlData = fileData else { + completion(.success(())) + return + } + + let parserData = ParserData(url: opmlFile.absoluteString, data: opmlData) + var opmlDocument: RSOPMLDocument? + + do { + opmlDocument = try RSOPMLParser.parseOPML(with: parserData) + } catch { + completion(.failure(error)) + return + } + + guard let loadDocument = opmlDocument else { + completion(.success(())) + return + } + + // We use the same mechanism to load local accounts as we do to load the subscription + // OPML all accounts. + account.loadOPML(loadDocument) + completion(.success(())) + + } func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { folder.name = name diff --git a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift index a587f3a9a..9247d86f7 100644 --- a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift +++ b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift @@ -64,13 +64,13 @@ class ImportOPMLWindowController: NSWindowController { panel.allowedFileTypes = ["opml", "xml"] panel.allowsOtherFileTypes = false - panel.beginSheetModal(for: hostWindow!) { result in - if result == NSApplication.ModalResponse.OK, let url = panel.url { - DispatchQueue.main.async { - do { - try OPMLImporter.parseAndImport(fileURL: url, account: account) - } - catch let error as NSError { + panel.beginSheetModal(for: hostWindow!) { modalResult in + if modalResult == NSApplication.ModalResponse.OK, let url = panel.url { + account.importOPML(url) { result in + switch result { + case .success: + break + case .failure(let error): NSApplication.shared.presentError(error) } } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 3b275a3fb..82dd181b4 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -89,7 +89,6 @@ 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; 51C45296226509D300C03939 /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; 51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; }; - 51C45298226509E600C03939 /* OPMLImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */; }; 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; }; 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; }; @@ -259,7 +258,6 @@ 84C9FCA42262A1B800D921D6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FCA22262A1B800D921D6 /* LaunchScreen.storyboard */; }; 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; 84D52E951FE588BB00D14F5B /* DetailStatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */; }; - 84DAEE301F86CAFE0058304B /* OPMLImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */; }; 84E185B3203B74E500F69BFA /* SingleLineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */; }; 84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */; }; 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; }; @@ -834,7 +832,6 @@ 84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = ""; }; 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedsController.swift; sourceTree = ""; }; 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStatusBarView.swift; sourceTree = ""; }; - 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLImporter.swift; sourceTree = ""; }; 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleLineTextFieldSizer.swift; sourceTree = ""; }; 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldSizer.swift; sourceTree = ""; }; 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; @@ -1654,7 +1651,6 @@ 84DAEE201F86CAE00058304B /* Importers */ = { isa = PBXGroup; children = ( - 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */, 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */, 84A3EE52223B667F00557320 /* DefaultFeeds.opml */, ); @@ -2319,7 +2315,6 @@ 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */, 51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */, - 51C45298226509E600C03939 /* OPMLImporter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2374,7 +2369,6 @@ 84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */, 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */, 849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */, - 84DAEE301F86CAFE0058304B /* OPMLImporter.swift in Sources */, 849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */, 84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */, 84A14FF320048CA70046AD9A /* SendToMicroBlogCommand.swift in Sources */, diff --git a/Shared/Importers/DefaultFeedsImporter.swift b/Shared/Importers/DefaultFeedsImporter.swift index 8621de096..63ec5aa71 100644 --- a/Shared/Importers/DefaultFeedsImporter.swift +++ b/Shared/Importers/DefaultFeedsImporter.swift @@ -19,7 +19,7 @@ struct DefaultFeedsImporter { appDelegate.logDebugMessage("Importing default feeds.") let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")! - try! OPMLImporter.parseAndImport(fileURL: defaultFeedsURL, account: AccountManager.shared.defaultAccount) + AccountManager.shared.defaultAccount.importOPML(defaultFeedsURL) { result in } } private static func shouldImportDefaultFeeds(_ isFirstRun: Bool) -> Bool { diff --git a/Shared/Importers/OPMLImporter.swift b/Shared/Importers/OPMLImporter.swift deleted file mode 100644 index 85c12dcb4..000000000 --- a/Shared/Importers/OPMLImporter.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// OPMLImporter.swift -// NetNewsWire -// -// Created by Brent Simmons on 10/5/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import RSParser -import Account -import RSCore - -struct OPMLImporter { - - static func parseAndImport(fileURL: URL, account: Account) throws { - - var fileData: Data? - - do { - fileData = try Data(contentsOf: fileURL) - } catch { - print("Error reading OPML file. \(error)") - throw error - } - - guard let opmlData = fileData else { - return - } - - let parserData = ParserData(url: fileURL.absoluteString, data: opmlData) - var opmlDocument: RSOPMLDocument? - - do { - opmlDocument = try RSOPMLParser.parseOPML(with: parserData) - } catch { - print("Error parsing OPML file. \(error)") - throw error - } - - if let opmlDocument = opmlDocument { - BatchUpdate.shared.perform { - account.importOPML(opmlDocument) - } - } - } -}