diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index b4e08e862..64e8f8d99 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -167,7 +167,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) private var unreadCounts = [String: Int]() // [feedID: Int] - private let opmlFilePath: String + private lazy var opmlFile: OPMLFile = { + OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self) + }() private var _flattenedFeeds = Set() private var flattenedFeedsNeedUpdate = true @@ -251,8 +253,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.type = type self.dataFolder = dataFolder - self.opmlFilePath = (dataFolder as NSString).appendingPathComponent("Subscriptions.opml") - let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3") self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID) @@ -396,6 +396,44 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } + func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) { + var feedsToAdd = Set() + + items.forEach { (item) in + + if let feedSpecifier = item.feedSpecifier { + let feed = newFeed(with: feedSpecifier) + feedsToAdd.insert(feed) + return + } + + 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 { + loadOPMLItems(itemChildren, parentFolder: parentFolder) + } + return + } + + if let folder = ensureFolder(with: folderName) { + if let itemChildren = item.children { + loadOPMLItems(itemChildren, parentFolder: folder) + } + } + } + + if let parentFolder = parentFolder { + for feed in feedsToAdd { + parentFolder.addFeed(feed) + } + } else { + for feed in feedsToAdd { + addFeed(feed) + } + } + + } + public func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { return delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag) } @@ -501,19 +539,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, structureDidChange() } - func loadOPML(_ opmlDocument: RSOPMLDocument) { - guard let children = opmlDocument.children else { - return - } - loadOPMLItems(children, parentFolder: nil) - structureDidChange() - - DispatchQueue.main.async { - self.refreshAll() { result in } - } - - } - public func updateUnreadCounts(for feeds: Set) { if feeds.isEmpty { return @@ -590,32 +615,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return database.fetchArticleIDsForStatusesWithoutArticles() } - public func opmlDocument() -> String { - let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters() - let openingText = - """ - - - - - \(escapedTitle) - - - - """ - - let middleText = OPMLString(indentLevel: 0) - - let closingText = - """ - - - """ - - let opml = openingText + middleText + closingText - return opml - } - public func unreadCount(for feed: Feed) -> Int { return unreadCounts[feed.feedID] ?? 0 } @@ -773,7 +772,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, @objc func saveToDiskIfNeeded() { if dirty && !isDeleted { - saveToDisk() + dirty = false + opmlFile.save() } } @@ -967,7 +967,7 @@ private extension Account { func pullObjectsFromDisk() { loadAccountMetadata() loadFeedMetadata() - loadOPMLFile(path: opmlFilePath) + opmlFile.load() } func loadAccountMetadata() { @@ -991,52 +991,6 @@ private extension Account { feedMetadata.values.forEach { $0.delegate = self } } - func loadOPMLFile(path: String) { - let opmlFileURL = URL(fileURLWithPath: path) - var fileData: Data? - do { - fileData = try Data(contentsOf: opmlFileURL) - } catch { - // Commented out because it’s not an error on first run. - // TODO: make it so we know if it’s first run or not. - //NSApplication.shared.presentError(error) - return - } - guard let opmlData = fileData else { - return - } - - let parserData = ParserData(url: opmlFileURL.absoluteString, data: opmlData) - var opmlDocument: RSOPMLDocument? - - do { - opmlDocument = try RSOPMLParser.parseOPML(with: parserData) - } catch { - os_log(.error, log: log, "OPML Import failed: %@.", error.localizedDescription) - return - } - guard let parsedOPML = opmlDocument, let children = parsedOPML.children else { - return - } - - BatchUpdate.shared.perform { - loadOPMLItems(children, parentFolder: nil) - } - } - - func saveToDisk() { - dirty = false - - let opmlDocumentString = opmlDocument() - do { - let url = URL(fileURLWithPath: opmlFilePath) - try opmlDocumentString.write(to: url, atomically: true, encoding: .utf8) - } - catch let error as NSError { - os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription) - } - } - func queueSaveFeedMetadataIfNeeded() { Account.saveQueue.add(self, #selector(saveFeedMetadataIfNeeded)) } @@ -1120,44 +1074,6 @@ private extension Account { _idToFeedDictionary = idDictionary feedDictionaryNeedsUpdate = false } - - func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) { - var feedsToAdd = Set() - - items.forEach { (item) in - - if let feedSpecifier = item.feedSpecifier { - let feed = newFeed(with: feedSpecifier) - feedsToAdd.insert(feed) - return - } - - 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 { - loadOPMLItems(itemChildren, parentFolder: parentFolder) - } - return - } - - if let folder = ensureFolder(with: folderName) { - if let itemChildren = item.children { - loadOPMLItems(itemChildren, parentFolder: folder) - } - } - } - - if let parentFolder = parentFolder { - for feed in feedsToAdd { - parentFolder.addFeed(feed) - } - } else { - for feed in feedsToAdd { - addFeed(feed) - } - } - - } func updateUnreadCount() { if fetchingAllUnreadCounts { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 61b586995..7723cfa9c 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71D22835E9800D9D53D /* FeedSpecifier.swift */; }; 5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */; }; 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; }; + 5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; }; 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; }; 51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; }; 51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; }; @@ -140,6 +141,7 @@ 5165D71D22835E9800D9D53D /* FeedSpecifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedSpecifier.swift; sourceTree = ""; }; 5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLFeedFinder.swift; sourceTree = ""; }; 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialFeedDownloader.swift; sourceTree = ""; }; + 5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = ""; }; 51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = ""; }; 51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = ""; }; 51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = ""; }; @@ -333,6 +335,7 @@ 841974241F6DDCE4006346C4 /* AccountDelegate.swift */, 51E3EB40229AF61B00645299 /* AccountError.swift */, 846E77531F6F00E300A165E2 /* AccountManager.swift */, + 5170743B232AEDB500A461A3 /* OPMLFile.swift */, 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */, 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, @@ -591,6 +594,7 @@ 5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */, 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, 552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */, + 5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */, 552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index a9d2df907..d1c1820c9 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -77,11 +77,14 @@ final class LocalAccountDelegate: AccountDelegate { return } - // We use the same mechanism to load local accounts as we do to load the subscription - // OPML all accounts. - BatchUpdate.shared.perform { - account.loadOPML(loadDocument) + guard let children = loadDocument.children else { + return } + + BatchUpdate.shared.perform { + account.loadOPMLItems(children, parentFolder: nil) + } + completion(.success(())) } diff --git a/Frameworks/Account/OPMLFile.swift b/Frameworks/Account/OPMLFile.swift new file mode 100644 index 000000000..37c5699f5 --- /dev/null +++ b/Frameworks/Account/OPMLFile.swift @@ -0,0 +1,112 @@ +// +// OPMLFile.swift +// Account +// +// Created by Maurice Parker on 9/12/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log +import RSCore +import RSParser + +final class OPMLFile: NSObject, NSFilePresenter { + + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "account") + + private let filename: String + private let account: Account + private let operationQueue: OperationQueue + + var presentedItemURL: URL? { + return URL(string: filename) + } + + var presentedItemOperationQueue: OperationQueue { + return operationQueue + } + + init(filename: String, account: Account) { + self.filename = filename + self.account = account + operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 1 + } + + func load() { + let opmlFileURL = URL(fileURLWithPath: filename) + var fileData: Data? + do { + fileData = try Data(contentsOf: opmlFileURL) + } catch { + // Commented out because it’s not an error on first run. + // TODO: make it so we know if it’s first run or not. + //NSApplication.shared.presentError(error) + return + } + guard let opmlData = fileData else { + return + } + + let parserData = ParserData(url: opmlFileURL.absoluteString, data: opmlData) + var opmlDocument: RSOPMLDocument? + + do { + opmlDocument = try RSOPMLParser.parseOPML(with: parserData) + } catch { + os_log(.error, log: log, "OPML Import failed: %@.", error.localizedDescription) + return + } + guard let parsedOPML = opmlDocument, let children = parsedOPML.children else { + return + } + + BatchUpdate.shared.perform { + account.loadOPMLItems(children, parentFolder: nil) + } + } + + func save() { + + let opmlDocumentString = opmlDocument() + do { + let url = URL(fileURLWithPath: filename) + try opmlDocumentString.write(to: url, atomically: true, encoding: .utf8) + } catch let error as NSError { + os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription) + } + } + +} + +private extension OPMLFile { + + func opmlDocument() -> String { + let escapedTitle = account.nameForDisplay.rs_stringByEscapingSpecialXMLCharacters() + let openingText = + """ + + + + + \(escapedTitle) + + + + """ + + let middleText = account.OPMLString(indentLevel: 0) + + let closingText = + """ + + + """ + + let opml = openingText + middleText + closingText + return opml + } + +}