summaryrefslogtreecommitdiff
path: root/Sora/Data/Settings
diff options
context:
space:
mode:
Diffstat (limited to 'Sora/Data/Settings')
-rw-r--r--Sora/Data/Settings/SettingsManager.swift420
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 }
}