import SwiftUI class SettingsManager: ObservableObject { // MARK: - Stored Properties @AppStorage("detailViewType") var detailViewQuality: BooruPostFileType = .original @AppStorage("thumbnailQuality") var thumbnailQuality: BooruPostFileType = .preview @AppStorage("searchSuggestionsMode") var searchSuggestionsMode: SettingsSearchSuggestionsMode = .disabled @AppStorage("thumbnailGridColumns") var thumbnailGridColumns = 2 @AppStorage("enableShareShortcut") var enableShareShortcut = false @AppStorage("displayDetailsInformationBar") var displayDetailsInformationBar = true @AppStorage("preloadedCarouselImages") var preloadedCarouselImages = 3 #if os(macOS) @AppStorage("saveTagsToFile") var saveTagsToFile = false #endif // MARK: - Codable Properties @AppStorage("bookmarks") private var bookmarksData = Data() @AppStorage("displayRatings") private var displayRatingsData = SettingsManager.encode(BooruRating.allCases) ?? Data() @AppStorage("blurRatings") private var blurRatingsData = SettingsManager.encode([.explicit as BooruRating]) ?? Data() @AppStorage("searchHistory") private var searchHistoryData = Data() @AppStorage("preferredBooru") private var preferredBooruData = Data() @AppStorage("customProviders") private var customProvidersData = Data() // MARK: - Computed Properties var bookmarks: [SettingsBookmark] { get { (Self.decode([SettingsBookmark].self, from: bookmarksData) ?? []) .sorted { $0.date > $1.date } } set { bookmarksData = Self.encode(newValue) ?? bookmarksData } } var displayRatings: [BooruRating] { get { Self.decode([BooruRating].self, from: displayRatingsData) ?? BooruRating.allCases } set { displayRatingsData = Self.encode(newValue) ?? displayRatingsData } } var blurRatings: [BooruRating] { get { Self.decode([BooruRating].self, from: blurRatingsData) ?? [.explicit] } set { blurRatingsData = Self.encode(newValue) ?? blurRatingsData } } var searchHistory: [BooruSearchQuery] { get { (Self.decode([BooruSearchQuery].self, from: searchHistoryData) ?? []) .sorted { $0.date > $1.date } } set { searchHistoryData = Self.encode(newValue) ?? searchHistoryData } } var preferredBooru: BooruProvider { get { Self.decode(BooruProvider.self, from: preferredBooruData) ?? .safebooru } set { preferredBooruData = Self.encode(newValue) ?? preferredBooruData } } var customProviders: [BooruProviderCustom] { get { Self.decode( [BooruProviderCustom].self, from: customProvidersData ) ?? [] } set { customProvidersData = Self.encode(newValue) ?? customProvidersData } } // MARK: - Private Helpers private static func encode(_ value: T) -> Data? { try? JSONEncoder().encode(value) } private static func decode(_ type: T.Type, from data: Data) -> T? { try? JSONDecoder().decode(type, from: data) } // MARK: - Public Methods func appendToSearchHistory(_ query: BooruSearchQuery) { self.searchHistory.append(query) } func resetToDefaults() { detailViewQuality = .original thumbnailQuality = .preview searchSuggestionsMode = .disabled thumbnailGridColumns = 2 preferredBooru = .safebooru enableShareShortcut = false displayRatings = BooruRating.allCases blurRatings = [.explicit] displayDetailsInformationBar = true preloadedCarouselImages = 3 #if os(macOS) saveTagsToFile = false #endif } // MARK: - Bookmark Management func addBookmark(provider: BooruProvider, tags: [String]) { bookmarks.append(SettingsBookmark(provider: provider, tags: tags.map { $0.lowercased() })) } func removeBookmark(at offsets: IndexSet) { bookmarks.remove(atOffsets: offsets) } func removeBookmark(withTags tags: [String]) { bookmarks.removeAll { $0.tags.contains(where: tags.contains) } } func removeBookmark(withID id: UUID) { bookmarks.removeAll { $0.id == id } } func exportBookmarks() throws -> Data { try JSONEncoder().encode(bookmarks) } func importBookmarks(from data: Data) throws { let importedBookmarks = try JSONDecoder().decode([SettingsBookmark].self, from: data) let existingIDs = Set(bookmarks.map(\.id)) let newBookmarks = importedBookmarks.filter { !existingIDs.contains($0.id) } bookmarks.append(contentsOf: newBookmarks) } // MARK: - Search History Management func removeSearchHistoryEntry(at offsets: IndexSet) { searchHistory.remove(atOffsets: offsets) } func removeSearchHistoryEntry(withID id: UUID) { searchHistory.removeAll { $0.id == id } } #if DEBUG // https://stackoverflow.com/a/68926484/14452787 private func randomWord() -> String { var word = "" for _ in 0..<5 { word += String(format: "%c", Int.random(in: 97..<123)) as String } return word } func addDummyBookmarks() { for _ in 0..<10 { let randomTags: [String] = Array(repeating: randomWord(), count: Int.random(in: 1...5)) addBookmark(provider: .safebooru, tags: randomTags) } } func addDummySearchHistory() { for _ in 0..<10 { let randomTags: [String] = Array(repeating: randomWord(), count: Int.random(in: 1...5)) appendToSearchHistory( BooruSearchQuery(provider: .safebooru, tags: randomTags, searchedAt: Date()) ) } } #endif }