summaryrefslogtreecommitdiff
path: root/Sora/Data
diff options
context:
space:
mode:
Diffstat (limited to 'Sora/Data')
-rw-r--r--Sora/Data/Booru/BooruManager.swift41
-rw-r--r--Sora/Data/ColumnsDataCache.swift14
-rw-r--r--Sora/Data/ImageCacheManager.swift24
-rw-r--r--Sora/Data/Settings/SettingsManager.swift146
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