diff options
Diffstat (limited to 'Sora/Data/Settings')
| -rw-r--r-- | Sora/Data/Settings/SettingsManager.swift | 420 |
1 files changed, 296 insertions, 124 deletions
diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift index b07b38c..32a1b86 100644 --- a/Sora/Data/Settings/SettingsManager.swift +++ b/Sora/Data/Settings/SettingsManager.swift @@ -1,89 +1,158 @@ // swiftlint:disable file_length +import Observation @preconcurrency import SwiftUI @MainActor -class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_length +@Observable +class SettingsManager { // swiftlint:disable:this type_body_length + private enum StorageKey { + static let detailViewQuality = "detailViewQuality" + static let thumbnailQuality = "thumbnailQuality" + static let searchSuggestionsMode = "searchSuggestionsMode" + static let thumbnailGridColumns = "thumbnailGridColumns" + static let enableShareShortcut = "enableShareShortcut" + static let displayDetailsInformationBar = "displayDetailsInformationBar" + static let preloadedCarouselImages = "preloadedCarouselImages" + static let enableSync = "enableSync" + static let alternativeThumbnailGrid = "alternativeThumbnailGrid" + static let uniformThumbnailGrid = "uniformThumbnailGrid" + static let showHeldMoebooruPosts = "showHeldMoebooruPosts" + static let sendBooruUserAgent = "sendBooruUserAgent" + static let customBooruUserAgent = "customBooruUserAgent" + static let saveTagsToFile = "saveTagsToFile" + static let bookmarks = "bookmarks" + static let favorites = "favorites" + static let displayRatings = "displayRatings" + static let blurRatings = "blurRatings" + static let searchHistory = "searchHistory" + static let preferredBooru = "preferredBooru" + static let customProviders = "customProviders" + static let folders = "folders" + static let providerCredentials = "providerCredentials" + } + + @ObservationIgnored private let userDefaults: UserDefaults + // MARK: - Stored Properties - @AppStorage("detailViewQuality") - var detailViewQuality: BooruPostFileType = .original + var detailViewQuality: BooruPostFileType { + didSet { userDefaults.set(detailViewQuality.rawValue, forKey: StorageKey.detailViewQuality) } + } - @AppStorage("thumbnailQuality") - private var _thumbnailQuality: BooruPostFileType = .preview + var thumbnailQuality: BooruPostFileType { + didSet { userDefaults.set(thumbnailQuality.rawValue, forKey: StorageKey.thumbnailQuality) } + } - @AppStorage("searchSuggestionsMode") - var searchSuggestionsMode: SettingsSearchSuggestionsMode = .disabled + var searchSuggestionsMode: SettingsSearchSuggestionsMode { + didSet { + userDefaults.set( + searchSuggestionsMode.rawValue, + forKey: StorageKey.searchSuggestionsMode + ) + } + } - @AppStorage("thumbnailGridColumns") - var thumbnailGridColumns = 2 + var thumbnailGridColumns: Int { + didSet { userDefaults.set(thumbnailGridColumns, forKey: StorageKey.thumbnailGridColumns) } + } - @AppStorage("enableShareShortcut") - var enableShareShortcut = false + var enableShareShortcut: Bool { + didSet { userDefaults.set(enableShareShortcut, forKey: StorageKey.enableShareShortcut) } + } - @AppStorage("displayDetailsInformationBar") - var displayDetailsInformationBar = true + var displayDetailsInformationBar: Bool { + didSet { + userDefaults.set( + displayDetailsInformationBar, + forKey: StorageKey.displayDetailsInformationBar + ) + } + } - @AppStorage("preloadedCarouselImages") - var preloadedCarouselImages = 3 + var preloadedCarouselImages: Int { + didSet { userDefaults.set(preloadedCarouselImages, forKey: StorageKey.preloadedCarouselImages) } + } - @AppStorage("enableSync") - var enableSync: Bool = false + var enableSync: Bool { + didSet { userDefaults.set(enableSync, forKey: StorageKey.enableSync) } + } - @AppStorage("alternativeThumbnailGrid") - var alternativeThumbnailGrid = false + var alternativeThumbnailGrid: Bool { + didSet { + userDefaults.set(alternativeThumbnailGrid, forKey: StorageKey.alternativeThumbnailGrid) + } + } - @AppStorage("uniformThumbnailGrid") - private var _uniformThumbnailGrid: Bool = false + var uniformThumbnailGrid: Bool { + didSet { userDefaults.set(uniformThumbnailGrid, forKey: StorageKey.uniformThumbnailGrid) } + } - @AppStorage("showHeldMoebooruPosts") - var showHeldMoebooruPosts = false + var showHeldMoebooruPosts: Bool { + didSet { userDefaults.set(showHeldMoebooruPosts, forKey: StorageKey.showHeldMoebooruPosts) } + } + + var sendBooruUserAgent: Bool { + didSet { userDefaults.set(sendBooruUserAgent, forKey: StorageKey.sendBooruUserAgent) } + } + + var customBooruUserAgent: String { + didSet { userDefaults.set(customBooruUserAgent, forKey: StorageKey.customBooruUserAgent) } + } - private var syncObservation: NSObjectProtocol? + @ObservationIgnored private var syncObservation: NSObjectProtocol? #if os(macOS) - @AppStorage("saveTagsToFile") - var saveTagsToFile = false + var saveTagsToFile: Bool { + didSet { userDefaults.set(saveTagsToFile, forKey: StorageKey.saveTagsToFile) } + } #endif // MARK: - Private Properties - private var bookmarksCache: [SettingsBookmark] = [] - private var favoritesCache: [SettingsFavoritePost] = [] - private var searchHistoryCache: [BooruSearchQuery] = [] - private var blurRatingsCache: [BooruRating] = [] - private var displayRatingsCache: [BooruRating] = [] - private var uniformThumbnailGridCache: Bool = false - private var thumbnailQualityCache: BooruPostFileType = .preview - private var isUpdatingCache = false - private var pendingSyncKeys: Set<SettingsSyncKey> = [] - private let syncCoordinator = SettingsSyncCoordinator() + private var bookmarksCache: [SettingsBookmark] + private var favoritesCache: [SettingsFavoritePost] + private var searchHistoryCache: [BooruSearchQuery] + private var blurRatingsCache: [BooruRating] + private var displayRatingsCache: [BooruRating] + @ObservationIgnored private var isUpdatingCache = false + @ObservationIgnored private var pendingSyncKeys: Set<SettingsSyncKey> = [] + @ObservationIgnored private let syncCoordinator = SettingsSyncCoordinator() // MARK: - Codable Properties - @AppStorage("bookmarks") - private var bookmarksData = Data() + private var bookmarksData: Data { + didSet { userDefaults.set(bookmarksData, forKey: StorageKey.bookmarks) } + } - @AppStorage("favorites") - private var favoritesData = Data() + private var favoritesData: Data { + didSet { userDefaults.set(favoritesData, forKey: StorageKey.favorites) } + } - @AppStorage("displayRatings") - private var displayRatingsData = SettingsManager.encode(BooruRating.allCases) ?? Data() + private var displayRatingsData: Data { + didSet { userDefaults.set(displayRatingsData, forKey: StorageKey.displayRatings) } + } - @AppStorage("blurRatings") - private var blurRatingsData = SettingsManager.encode([.explicit as BooruRating]) ?? Data() + private var blurRatingsData: Data { + didSet { userDefaults.set(blurRatingsData, forKey: StorageKey.blurRatings) } + } - @AppStorage("searchHistory") - private var searchHistoryData = Data() + private var searchHistoryData: Data { + didSet { userDefaults.set(searchHistoryData, forKey: StorageKey.searchHistory) } + } - @AppStorage("preferredBooru") - private var preferredBooruData = Data() + private var preferredBooruData: Data { + didSet { userDefaults.set(preferredBooruData, forKey: StorageKey.preferredBooru) } + } - @AppStorage("customProviders") - private var customProvidersData = Data() + private var customProvidersData: Data { + didSet { userDefaults.set(customProvidersData, forKey: StorageKey.customProviders) } + } - @AppStorage("folders") - private var foldersData = Data() + private var foldersData: Data { + didSet { userDefaults.set(foldersData, forKey: StorageKey.folders) } + } - @AppStorage("providerCredentials") - private var providerCredentialsData = Data() + private var providerCredentialsData: Data { + didSet { userDefaults.set(providerCredentialsData, forKey: StorageKey.providerCredentials) } + } // MARK: - Computed Properties var bookmarks: [SettingsBookmark] { @@ -101,7 +170,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "bookmarks", - localData: $bookmarksData, + localData: storageBinding(for: \.bookmarksData), newValue: sortedBookmarks, encodedData: payload?.encodedData ) { $0 } @@ -128,7 +197,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "favorites", - localData: $favoritesData, + localData: storageBinding(for: \.favoritesData), newValue: sortedFavorites, encodedData: payload?.encodedData ) { $0 } @@ -140,24 +209,6 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } } - var uniformThumbnailGrid: Bool { - get { uniformThumbnailGridCache } - - set { - _uniformThumbnailGrid = newValue - uniformThumbnailGridCache = newValue - } - } - - var thumbnailQuality: BooruPostFileType { - get { thumbnailQualityCache } - - set { - _thumbnailQuality = newValue - thumbnailQualityCache = newValue - } - } - var displayRatings: [BooruRating] { get { displayRatingsCache } @@ -198,13 +249,17 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l defer { isUpdatingCache = false } + let sortedSearchHistory = newValue.sorted { $0.date > $1.date } + let payload = SettingsCodec.encodeOnce(sortedSearchHistory) + syncableData( key: "searchHistory", - localData: $searchHistoryData, - newValue: newValue, - ) { $0.sorted { $0.date > $1.date } } + localData: storageBinding(for: \.searchHistoryData), + newValue: sortedSearchHistory, + encodedData: payload?.encodedData + ) { $0 } - searchHistoryCache = newValue.sorted { $0.date > $1.date } + searchHistoryCache = sortedSearchHistory pendingSyncKeys.insert(.searchHistory) triggerBatchedSync() @@ -238,7 +293,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "customProviders", - localData: $customProvidersData, + localData: storageBinding(for: \.customProvidersData), newValue: newValue, ) { $0 } pendingSyncKeys.insert(.customProviders) @@ -265,7 +320,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "folders", - localData: $foldersData, + localData: storageBinding(for: \.foldersData), newValue: newValue, ) { $0 } } @@ -305,7 +360,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "providerAPIKeys", - localData: $providerCredentialsData, + localData: storageBinding(for: \.providerCredentialsData), newValue: mergedCredentials, ) { $0 } } @@ -351,7 +406,97 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } // MARK: - Initialisation - init() { + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + detailViewQuality = Self.stringValue( + forKey: StorageKey.detailViewQuality, + defaultValue: .original, + userDefaults: userDefaults + ) + thumbnailQuality = Self.stringValue( + forKey: StorageKey.thumbnailQuality, + defaultValue: .preview, + userDefaults: userDefaults + ) + searchSuggestionsMode = Self.stringValue( + forKey: StorageKey.searchSuggestionsMode, + defaultValue: .disabled, + userDefaults: userDefaults + ) + thumbnailGridColumns = Self.integerValue( + forKey: StorageKey.thumbnailGridColumns, + defaultValue: 2, + userDefaults: userDefaults + ) + enableShareShortcut = Self.boolValue( + forKey: StorageKey.enableShareShortcut, + defaultValue: false, + userDefaults: userDefaults + ) + displayDetailsInformationBar = Self.boolValue( + forKey: StorageKey.displayDetailsInformationBar, + defaultValue: true, + userDefaults: userDefaults + ) + preloadedCarouselImages = Self.integerValue( + forKey: StorageKey.preloadedCarouselImages, + defaultValue: 3, + userDefaults: userDefaults + ) + enableSync = Self.boolValue( + forKey: StorageKey.enableSync, + defaultValue: false, + userDefaults: userDefaults + ) + alternativeThumbnailGrid = Self.boolValue( + forKey: StorageKey.alternativeThumbnailGrid, + defaultValue: false, + userDefaults: userDefaults + ) + uniformThumbnailGrid = Self.boolValue( + forKey: StorageKey.uniformThumbnailGrid, + defaultValue: false, + userDefaults: userDefaults + ) + showHeldMoebooruPosts = Self.boolValue( + forKey: StorageKey.showHeldMoebooruPosts, + defaultValue: false, + userDefaults: userDefaults + ) + sendBooruUserAgent = Self.boolValue( + forKey: StorageKey.sendBooruUserAgent, + defaultValue: true, + userDefaults: userDefaults + ) + customBooruUserAgent = userDefaults.string(forKey: StorageKey.customBooruUserAgent) ?? "" + bookmarksCache = [] + favoritesCache = [] + searchHistoryCache = [] + blurRatingsCache = [] + displayRatingsCache = [] + bookmarksData = userDefaults.data(forKey: StorageKey.bookmarks) ?? Data() + favoritesData = userDefaults.data(forKey: StorageKey.favorites) ?? Data() + displayRatingsData = + userDefaults.data(forKey: StorageKey.displayRatings) + ?? Self.encode(BooruRating.allCases) + ?? Data() + blurRatingsData = + userDefaults.data(forKey: StorageKey.blurRatings) + ?? Self.encode([.explicit as BooruRating]) + ?? Data() + searchHistoryData = userDefaults.data(forKey: StorageKey.searchHistory) ?? Data() + preferredBooruData = userDefaults.data(forKey: StorageKey.preferredBooru) ?? Data() + customProvidersData = userDefaults.data(forKey: StorageKey.customProviders) ?? Data() + foldersData = userDefaults.data(forKey: StorageKey.folders) ?? Data() + providerCredentialsData = userDefaults.data(forKey: StorageKey.providerCredentials) ?? Data() + #if os(macOS) + saveTagsToFile = Self.boolValue( + forKey: StorageKey.saveTagsToFile, + defaultValue: false, + userDefaults: userDefaults + ) + #endif + syncObservation = NotificationCenter.default.addObserver( forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default, @@ -367,9 +512,6 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l loadSearchHistoryCache() loadDisplayRatingsCache() loadBlurRatingsCache() - - uniformThumbnailGridCache = _uniformThumbnailGrid - thumbnailQualityCache = _thumbnailQuality } func updateBookmarks(_ newValue: [SettingsBookmark], encodedData: Data? = nil) async { @@ -384,7 +526,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "bookmarks", - localData: $bookmarksData, + localData: storageBinding(for: \.bookmarksData), newValue: sortedBookmarks, encodedData: resolvedEncodedData ) { $0 } @@ -408,7 +550,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "favorites", - localData: $favoritesData, + localData: storageBinding(for: \.favoritesData), newValue: sortedFavorites, encodedData: resolvedEncodedData ) { $0 } @@ -427,13 +569,17 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l defer { isUpdatingCache = false } + let sortedSearchHistory = newValue.sorted { $0.date > $1.date } + let payload = SettingsCodec.encodeOnce(sortedSearchHistory) + syncableData( key: "searchHistory", - localData: $searchHistoryData, - newValue: newValue, - ) { $0.sorted { $0.date > $1.date } } + localData: storageBinding(for: \.searchHistoryData), + newValue: sortedSearchHistory, + encodedData: payload?.encodedData + ) { $0 } - searchHistoryCache = newValue.sorted { $0.date > $1.date } + searchHistoryCache = sortedSearchHistory pendingSyncKeys.insert(.searchHistory) triggerBatchedSync() @@ -484,20 +630,62 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "providerAPIKeys", - localData: $providerCredentialsData, + localData: storageBinding(for: \.providerCredentialsData), newValue: mergedCredentials, ) { $0 } } // MARK: - Private Helpers + @MainActor private static func encode<T: Encodable>(_ value: T) -> Data? { SettingsCodec.encode(value) } + @MainActor private static func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T? { SettingsCodec.decode(type, from: data) } + @MainActor + private static func boolValue( + forKey key: String, + defaultValue: Bool, + userDefaults: UserDefaults + ) -> Bool { + (userDefaults.object(forKey: key) as? Bool) ?? defaultValue + } + + @MainActor + private static func integerValue( + forKey key: String, + defaultValue: Int, + userDefaults: UserDefaults + ) -> Int { + (userDefaults.object(forKey: key) as? Int) ?? defaultValue + } + + @MainActor + private static func stringValue<Value: RawRepresentable>( + forKey key: String, + defaultValue: Value, + userDefaults: UserDefaults + ) -> Value where Value.RawValue == String { + guard let rawValue = userDefaults.string(forKey: key) else { + return defaultValue + } + + return Value(rawValue: rawValue) ?? defaultValue + } + + private func storageBinding(for keyPath: ReferenceWritableKeyPath<SettingsManager, Data>) + -> Binding<Data> + { + Binding( + get: { self[keyPath: keyPath] }, + set: { self[keyPath: keyPath] = $0 } + ) + } + private func syncableData<T: Codable>( key: String, localData: Data, @@ -570,18 +758,14 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } private func performBatchedSync(for keys: Set<SettingsSyncKey>) { - var didChange = false - for key in keys { - didChange = triggerSyncIfNeeded(for: key) || didChange - } - - if didChange { - objectWillChange.send() + _ = triggerSyncIfNeeded(for: key) } } private func backupBookmarks() async { + let backupData = Self.encode(bookmarksCache) + await Task.detached { guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) @@ -596,7 +780,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l let timestamp = Int(Date().timeIntervalSince1970) let backupFile = backupDirectory.appendingPathComponent("bookmarks_backup_\(timestamp).json") - if let data = await Self.encode(self.bookmarksCache) { + if let data = backupData { try? data.write(to: backupFile) } @@ -631,6 +815,8 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } private func backupFavorites() async { + let backupData = Self.encode(favoritesCache) + await Task.detached { guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) @@ -645,7 +831,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l let timestamp = Int(Date().timeIntervalSince1970) let backupFile = backupDirectory.appendingPathComponent("favorites_backup_\(timestamp).json") - if let data = await Self.encode(self.favoritesCache) { + if let data = backupData { try? data.write(to: backupFile) } @@ -915,47 +1101,33 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } func syncFromCloud() { - if self.enableSync { - Task.detached { [weak self] in + if enableSync { + Task { [weak self] in guard let self else { return } if let data = NSUbiquitousKeyValueStore.default.data(forKey: "bookmarks") { - await MainActor.run { - self.bookmarksData = data - } + bookmarksData = data } if let data = NSUbiquitousKeyValueStore.default.data(forKey: "searchHistory") { - await MainActor.run { - self.searchHistoryData = data - } + searchHistoryData = data } if let data = NSUbiquitousKeyValueStore.default.data(forKey: "customProviders") { - await MainActor.run { - self.customProvidersData = data - } + customProvidersData = data } - await MainActor.run { - self.loadBookmarksCache() - self.loadSearchHistoryCache() - self.objectWillChange.send() - } + loadBookmarksCache() + loadSearchHistoryCache() } } } func triggerSyncIfNeededForAll() { let keysToSync: [SettingsSyncKey] = [.bookmarks, .favorites, .searchHistory, .customProviders] - var didChange = false for keyToSync in keysToSync { - didChange = triggerSyncIfNeeded(for: keyToSync) || didChange - } - - if didChange { - objectWillChange.send() + _ = triggerSyncIfNeeded(for: keyToSync) } } @@ -1224,7 +1396,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "folders", - localData: $foldersData, + localData: storageBinding(for: \.foldersData), newValue: updated, ) { $0 } } |