diff options
Diffstat (limited to 'Sora/Data')
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 41 | ||||
| -rw-r--r-- | Sora/Data/ColumnsDataCache.swift | 14 | ||||
| -rw-r--r-- | Sora/Data/ImageCacheManager.swift | 24 | ||||
| -rw-r--r-- | Sora/Data/Settings/SettingsManager.swift | 146 |
4 files changed, 197 insertions, 28 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index b5b5ea7..f8aa9d0 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -25,6 +25,8 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng private let cacheDuration: TimeInterval private let credentials: BooruProviderCredentials? private let userAgent: String + private var urlCache: [String: URL] = [:] + private var lastPostCount = 0 // MARK: - Computed Properties var tags: [String] { @@ -146,6 +148,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng func clearCachedPages() { pageCache.removeAllObjects() + urlCache.removeAll() } func performSearch(settings: SettingsManager? = nil) async { @@ -240,13 +243,20 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng // MARK: - Private Methods func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { let tagString = tags.joined(separator: "+") + let cacheKey = "posts_\(page)_\(limit)_\(tagString)" + + if let cachedURL = urlCache[cacheKey] { + return cachedURL + } + + let url: URL? switch flavor { case .danbooru: - return URL(string: "https://\(domain)/posts.json?page=\(page)&tags=\(tagString)") + url = URL(string: "https://\(domain)/posts.json?page=\(page)&tags=\(tagString)") case .moebooru: - return URL(string: "https://\(domain)/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") + url = URL(string: "https://\(domain)/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") case .gelbooru: var urlString = @@ -259,10 +269,14 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng urlString += "&api_key=\(validCredentials.apiKey)&user_id=\(validCredentials.userID)" } - return URL( - string: urlString - ) + url = URL(string: urlString) + } + + if let constructedURL = url { + urlCache[cacheKey] = constructedURL } + + return url } private func urlForTags(limit: Int, order: String = "count") -> URL? { @@ -328,6 +342,10 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng if replace { posts = [] currentPage = 1 + + postIndexMap.removeAll() + + lastPostCount = 0 } endOfData = newPosts.isEmpty @@ -339,8 +357,12 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng self.posts += newPosts - for (offset, post) in newPosts.enumerated() { - self.postIndexMap[post.id] = oldCount + offset + if newPosts.count > 10 || self.posts.count - lastPostCount > 50 { + for (offset, post) in newPosts.enumerated() { + self.postIndexMap[post.id] = oldCount + offset + } + + lastPostCount = self.posts.count } } } @@ -358,5 +380,8 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } // MARK: - Deinitialisation - deinit { currentTask?.cancel() } + deinit { + currentTask?.cancel() + urlCache.removeAll() + } } diff --git a/Sora/Data/ColumnsDataCache.swift b/Sora/Data/ColumnsDataCache.swift index 4858e80..bec37fb 100644 --- a/Sora/Data/ColumnsDataCache.swift +++ b/Sora/Data/ColumnsDataCache.swift @@ -1,5 +1,17 @@ -struct ColumnsDataCache { +struct ColumnsDataCache: Equatable { let data: [[BooruPost]] let columnCount: Int let posts: [BooruPost] + + static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.columnCount == rhs.columnCount else { return false } + guard lhs.posts.count == rhs.posts.count else { return false } + guard !lhs.posts.isEmpty, !rhs.posts.isEmpty else { + return lhs.posts.isEmpty == rhs.posts.isEmpty + } + guard lhs.posts.first?.id == rhs.posts.first?.id else { return false } + guard lhs.posts.last?.id == rhs.posts.last?.id else { return false } + + return true + } } diff --git a/Sora/Data/ImageCacheManager.swift b/Sora/Data/ImageCacheManager.swift index 8b886df..cd8b4e2 100644 --- a/Sora/Data/ImageCacheManager.swift +++ b/Sora/Data/ImageCacheManager.swift @@ -15,20 +15,30 @@ final class ImageCacheManager { ) private var cancellables = Set<AnyCancellable>() private let downloadQueue = OperationQueue() + private var preloadingURLs = Set<URL>() // MARK: - Initialisation private init() { downloadQueue.maxConcurrentOperationCount = 5 + downloadQueue.qualityOfService = .utility } // MARK: - Public Methods func preloadImages(_ urls: [URL]) { - for url in urls { + let newURLs = urls.filter { !preloadingURLs.contains($0) } + + for url in newURLs { + preloadingURLs.insert(url) + downloadQueue.addOperation { let cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { CachedURLResponse(response: $0.response, data: $0.data) } .sink( - receiveCompletion: { _ in () }, + receiveCompletion: { _ in + DispatchQueue.main.async { [weak self] in + self?.preloadingURLs.remove(url) + } + }, receiveValue: { [weak self] cachedResponse in self?.cache.storeCachedResponse(cachedResponse, for: URLRequest(url: url)) } @@ -40,4 +50,14 @@ final class ImageCacheManager { } } } + + func clearCache() { + cache.removeAllCachedResponses() + cancellables.removeAll() + preloadingURLs.removeAll() + } + + func getCachedResponse(for url: URL) -> CachedURLResponse? { + cache.cachedResponse(for: URLRequest(url: url)) + } } diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift index 53d699e..30fe587 100644 --- a/Sora/Data/Settings/SettingsManager.swift +++ b/Sora/Data/Settings/SettingsManager.swift @@ -5,7 +5,7 @@ import SwiftUI @MainActor class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_length // MARK: - Stored Properties - @AppStorage("detailViewType") + @AppStorage("detailViewQuality") var detailViewQuality: BooruPostFileType = .original @AppStorage("thumbnailQuality") @@ -49,6 +49,8 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l private var displayRatingsCache: [BooruRating] = [] private var uniformThumbnailGridCache: Bool = false private var thumbnailQualityCache: BooruPostFileType = .preview + private var isUpdatingCache = false + private var pendingSyncKeys: Set<SettingsSyncKey> = [] // MARK: - Codable Properties @AppStorage("bookmarks") @@ -80,12 +82,22 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l get { bookmarksCache } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + syncableData( key: "bookmarks", localData: $bookmarksData, newValue: newValue ) { $0.sorted { $0.date > $1.date } } - triggerSyncIfNeeded(for: .bookmarks) + + bookmarksCache = newValue.sorted { $0.date > $1.date } + + pendingSyncKeys.insert(.bookmarks) + triggerBatchedSync() } } @@ -111,7 +123,14 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l get { displayRatingsCache } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + displayRatingsData = Self.encode(newValue) ?? displayRatingsData + displayRatingsCache = newValue } } @@ -119,7 +138,14 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l get { blurRatingsCache } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + blurRatingsData = Self.encode(newValue) ?? blurRatingsData + blurRatingsCache = newValue } } @@ -127,12 +153,22 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l get { searchHistoryCache } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + syncableData( key: "searchHistory", localData: $searchHistoryData, newValue: newValue, ) { $0.sorted { $0.date > $1.date } } - triggerSyncIfNeeded(for: .searchHistory) + + searchHistoryCache = newValue.sorted { $0.date > $1.date } + + pendingSyncKeys.insert(.searchHistory) + triggerBatchedSync() } } @@ -155,12 +191,19 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + syncableData( key: "customProviders", localData: $customProvidersData, newValue: newValue, ) { $0 } - triggerSyncIfNeeded(for: .customProviders) + pendingSyncKeys.insert(.customProviders) + triggerBatchedSync() } } @@ -175,6 +218,12 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + syncableData( key: "folders", localData: $foldersData, @@ -195,6 +244,12 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } set { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + let existingCredentials: [BooruProviderCredentials] = Self.decode([BooruProviderCredentials].self, from: providerCredentialsData) ?? [] let rawCredentials = newValue.map { credentials in @@ -266,39 +321,73 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } func updateBookmarks(_ newValue: [SettingsBookmark]) async { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + syncableData( key: "bookmarks", localData: $bookmarksData, newValue: newValue ) { $0.sorted { $0.date > $1.date } } - loadBookmarksCache() + + bookmarksCache = newValue.sorted { $0.date > $1.date } + await backupBookmarks() - triggerSyncIfNeeded(for: .bookmarks) + pendingSyncKeys.insert(.bookmarks) + triggerBatchedSync() } func updateSearchHistory(_ newValue: [BooruSearchQuery]) { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + syncableData( key: "searchHistory", localData: $searchHistoryData, newValue: newValue, ) { $0.sorted { $0.date > $1.date } } - loadSearchHistoryCache() - triggerSyncIfNeeded(for: .searchHistory) + + searchHistoryCache = newValue.sorted { $0.date > $1.date } + + pendingSyncKeys.insert(.searchHistory) + triggerBatchedSync() } func updateDisplayRatings(_ newValue: [BooruRating]) { - displayRatingsData = Self.encode(newValue) ?? displayRatingsData + guard !isUpdatingCache else { return } - loadDisplayRatingsCache() + isUpdatingCache = true + + defer { isUpdatingCache = false } + + displayRatingsData = Self.encode(newValue) ?? displayRatingsData + displayRatingsCache = newValue } func updateBlurRatings(_ newValue: [BooruRating]) { - blurRatingsData = Self.encode(newValue) ?? blurRatingsData + guard !isUpdatingCache else { return } - loadBlurRatingsCache() + isUpdatingCache = true + + defer { isUpdatingCache = false } + + blurRatingsData = Self.encode(newValue) ?? blurRatingsData + blurRatingsCache = newValue } func updateProviderCredentials(_ newValue: [BooruProviderCredentials]) { + guard !isUpdatingCache else { return } + + isUpdatingCache = true + + defer { isUpdatingCache = false } + let existingCredentials: [BooruProviderCredentials] = Self.decode([BooruProviderCredentials].self, from: providerCredentialsData) ?? [] let rawCredentials = newValue.map { credentials in @@ -371,6 +460,24 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } } + private func triggerBatchedSync() { + guard !pendingSyncKeys.isEmpty else { return } + + let keysToSync = pendingSyncKeys + + pendingSyncKeys.removeAll() + + Task.detached { [weak self] in + await self?.performBatchedSync(for: keysToSync) + } + } + + private func performBatchedSync(for keys: Set<SettingsSyncKey>) async { + for key in keys { + await triggerSyncIfNeeded(for: key) + } + } + private func backupBookmarks() async { await Task.detached { guard @@ -708,7 +815,8 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l await updateBookmarks(updated) } - triggerSyncIfNeeded(for: .bookmarks) + pendingSyncKeys.insert(.bookmarks) + triggerBatchedSync() } func updateBookmarkLastVisit(withID id: UUID, date: Date = Date()) { @@ -722,7 +830,8 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l await updateBookmarks(updated) } - triggerSyncIfNeeded(for: .bookmarks) + pendingSyncKeys.insert(.bookmarks) + triggerBatchedSync() } func incrementBookmarkVisitCount(withID id: UUID) { @@ -736,7 +845,8 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l await updateBookmarks(updated) } - triggerSyncIfNeeded(for: .bookmarks) + pendingSyncKeys.insert(.bookmarks) + triggerBatchedSync() } func folderName(forID id: UUID) -> String? { @@ -764,14 +874,16 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l updated.remove(atOffsets: offsets) updateSearchHistory(updated) - triggerSyncIfNeeded(for: .searchHistory) + pendingSyncKeys.insert(.searchHistory) + triggerBatchedSync() } func removeSearchHistoryEntry(withID id: UUID) { let updated = searchHistory.filter { $0.id != id } updateSearchHistory(updated) - triggerSyncIfNeeded(for: .searchHistory) + pendingSyncKeys.insert(.searchHistory) + triggerBatchedSync() } #if DEBUG |