diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index cc437d40a..c7851965f 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -80,6 +80,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, private var _flattenedFeeds = Set() private var flattenedFeedsNeedUpdate = true + private let feedMetadataPath: String + private typealias FeedMetadataDictionary = [String: FeedMetadata] + private var feedMetadata = FeedMetadataDictionary() + private var feedMetadataDirty = false { + didSet { + queueSaveMetadataIfNeeded() + } + } + private var startingUp = true private struct SettingsKey { @@ -137,6 +146,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3") self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID) + self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist") let settingsODBFilePath = (dataFolder as NSString).appendingPathComponent("Settings.odb") self.settingsODB = ODB(filepath: settingsODBFilePath) self.settingsODB.vacuum() @@ -167,6 +177,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, delegate.refreshAll(for: self) } + func metadata(for feed: Feed) -> FeedMetadata { + if let d = feedMetadata[feed.feedID] { + assert(d.delegate === self) + return d + } + let d = FeedMetadata(feedID: feed.feedID) + d.delegate = self + feedMetadata[feed.feedID] = d + return d + } + public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) { feed.takeSettings(from: parsedFeed) @@ -559,6 +580,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } + @objc func saveMetadataIfNeeded() { + if feedMetadataDirty { + saveFeedMetadata() + } + } + // MARK: - Hashable public func hash(into hasher: inout Hasher) { @@ -590,6 +617,15 @@ extension Account { } } +// MARK: - FeedMetadataDelegate + +extension Account: FeedMetadataDelegate { + + func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) { + feedMetadataDirty = true + } +} + // MARK: - Disk (Private) private extension Account { @@ -614,7 +650,7 @@ private extension Account { } func pullObjectsFromDisk() { - + // 9/16/2018: Turning a corner — we used to store data in a plist file, // but now we’re switching over to OPML. Read the plist file one last time, // then rename it so we never read from it again. @@ -654,9 +690,20 @@ private extension Account { return } + importFeedMetadata() importOPMLFile(path: opmlFilePath) } + func importFeedMetadata() { + let url = URL(fileURLWithPath: feedMetadataPath) + guard let data = try? Data(contentsOf: url) else { + return + } + let decoder = PropertyListDecoder() + feedMetadata = (try? decoder.decode(FeedMetadataDictionary.self, from: data)) ?? FeedMetadataDictionary() + feedMetadata.values.forEach { $0.delegate = self } + } + func importOPMLFile(path: String) { let opmlFileURL = URL(fileURLWithPath: path) var fileData: Data? @@ -703,6 +750,33 @@ private extension Account { NSApplication.shared.presentError(error) } } + + func queueSaveMetadataIfNeeded() { + Account.saveQueue.add(self, #selector(saveMetadataIfNeeded)) + } + + private func metadataForOnlySubscribedToFeeds() -> FeedMetadataDictionary { + let feedIDs = idToFeedDictionary.keys + return feedMetadata.filter { (feedID: String, metadata: FeedMetadata) -> Bool in + return feedIDs.contains(feedID) + } + } + + func saveFeedMetadata() { + feedMetadataDirty = false + + let d = metadataForOnlySubscribedToFeeds() + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + let url = URL(fileURLWithPath: feedMetadataPath) + do { + let data = try encoder.encode(d) + try data.write(to: url) + } + catch { + assertionFailure(error.localizedDescription) + } + } } // MARK: - Private diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 945978437..280808cb4 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -35,33 +35,33 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha public var homePageURL: String? { get { - return settingsTable.string(for: Key.homePageURL) + return metadata?.homePageURL } set { if let url = newValue { - settingsTable.setString(url.rs_normalizedURL(), for: Key.homePageURL) + metadata?.homePageURL = url.rs_normalizedURL() } else { - settingsTable.setString(nil, for: Key.homePageURL) + metadata?.homePageURL = nil } } } public var iconURL: String? { get { - return settingsTable.string(for: Key.iconURL) + return metadata?.iconURL } set { - settingsTable.setString(newValue, for: Key.iconURL) + metadata?.iconURL = newValue } } public var faviconURL: String? { get { - return settingsTable.string(for: Key.faviconURL) + return metadata?.faviconURL } set { - settingsTable.setString(newValue, for: Key.faviconURL) + metadata?.faviconURL = newValue } } @@ -80,17 +80,17 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha public var authors: Set? { get { - guard let authorsJSON = settingsTable.string(for: Key.authors) else { - return nil + if let authorsArray = metadata?.authors { + return Set(authorsArray) } - return Author.authorsWithJSON(authorsJSON) + return nil } set { - if let authorsJSON = newValue?.json() { - settingsTable.setString(authorsJSON, for: Key.authors) + if let authorsSet = newValue { + metadata?.authors = Array(authorsSet) } else { - settingsTable.setString(nil, for: Key.authors) + metadata?.authors = nil } } } @@ -118,22 +118,19 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha public var conditionalGetInfo: HTTPConditionalGetInfo? { get { - let lastModified = settingsTable.string(for: Key.conditionalGetLastModified) - let etag = settingsTable.string(for: Key.conditionalGetEtag) - return HTTPConditionalGetInfo(lastModified: lastModified, etag: etag) + return metadata?.conditionalGetInfo } set { - settingsTable.setString(newValue?.lastModified, for: Key.conditionalGetLastModified) - settingsTable.setString(newValue?.etag, for: Key.conditionalGetEtag) + metadata?.conditionalGetInfo = newValue } } public var contentHash: String? { get { - return settingsTable.string(for: Key.contentHash) + return metadata?.contentHash } set { - settingsTable.setString(newValue, for: Key.contentHash) + metadata?.contentHash = newValue } } @@ -173,6 +170,15 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha private let settingsTable: ODBRawValueTable private let accountID: String // Used for hashing and equality; account may turn nil + private var _metadata: FeedMetadata? + private var metadata: FeedMetadata? { + if let cachedMetadata = _metadata { + return cachedMetadata + } + _metadata = account?.metadata(for: self) + return _metadata + } + // MARK: - Init public init(account: Account, url: String, feedID: String) { diff --git a/Frameworks/Account/FeedMetadata.swift b/Frameworks/Account/FeedMetadata.swift index 5571fab84..23116ebbd 100644 --- a/Frameworks/Account/FeedMetadata.swift +++ b/Frameworks/Account/FeedMetadata.swift @@ -96,9 +96,8 @@ final class FeedMetadata: Codable { weak var delegate: FeedMetadataDelegate? - init(feedID: String, delegate: FeedMetadataDelegate) { + init(feedID: String) { self.feedID = feedID - self.delegate = delegate } func valueDidChange(_ key: CodingKeys) {