diff options
Diffstat (limited to 'Sora')
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 40 | ||||
| -rw-r--r-- | Sora/Data/Booru/BooruNetworkImageLoader.swift | 61 | ||||
| -rw-r--r-- | Sora/Data/Booru/BooruRequestConfiguration.swift | 66 | ||||
| -rw-r--r-- | Sora/Data/ImageCacheManager.swift | 59 | ||||
| -rw-r--r-- | Sora/Views/FavoritePostThumbnailView.swift | 8 | ||||
| -rw-r--r-- | Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift | 11 | ||||
| -rw-r--r-- | Sora/Views/Post/Details/PostDetailsImageView.swift | 33 | ||||
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridThumbnailView.swift | 8 |
8 files changed, 235 insertions, 51 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index f5fcf89..6333453 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -62,8 +62,8 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng pageCache.countLimit = 50 pageCache.totalCostLimit = 50 * 1_024 * 1_024 - self.referer = Self.baseReferer(for: provider.domain) - self.userAgent = Self.resolvedUserAgent( + self.referer = BooruRequestConfiguration.baseReferer(for: provider.domain) + self.userAgent = BooruRequestConfiguration.resolvedUserAgent( sendUserAgent: sendUserAgent, customUserAgent: customUserAgent ) @@ -202,30 +202,6 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng return components.url } - private static func resolvedUserAgent( - sendUserAgent: Bool, - customUserAgent: String - ) -> String? { - guard sendUserAgent else { return nil } - - let trimmedCustomUserAgent = customUserAgent.trimmingCharacters( - in: .whitespacesAndNewlines - ) - - guard trimmedCustomUserAgent.isEmpty else { - return trimmedCustomUserAgent - } - - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" - let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" - - return "Sora/\(version) (Build \(buildNumber))" - } - - private static func baseReferer(for domain: String) -> String { - "https://\(domain)/" - } - func clearCachedPages() { pageCache.removeAllObjects() urlCache.removeAll() @@ -599,13 +575,13 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } func requestURL(_ url: URL) async throws -> Data { - var headers = HTTPHeaders([HTTPHeader(name: "Referer", value: referer)]) - - if let userAgent { - headers.add(name: "User-Agent", value: userAgent) - } + let request = BooruRequestConfiguration.request( + url: url, + referer: referer, + userAgent: userAgent + ) - return try await AF.request(url, headers: headers) + return try await AF.request(request) .validate(statusCode: 200..<300) .serializingData() .value diff --git a/Sora/Data/Booru/BooruNetworkImageLoader.swift b/Sora/Data/Booru/BooruNetworkImageLoader.swift new file mode 100644 index 0000000..c2a0f8a --- /dev/null +++ b/Sora/Data/Booru/BooruNetworkImageLoader.swift @@ -0,0 +1,61 @@ +import CoreGraphics +import Foundation +import ImageIO +import NetworkImage + +actor BooruNetworkImageLoader: NetworkImageLoader { + private let domain: String + private let sendUserAgent: Bool + private let customUserAgent: String + private var ongoingTasks: [URL: Task<CGImage, Error>] = [:] + + init( + domain: String, + sendUserAgent: Bool, + customUserAgent: String + ) { + self.domain = domain + self.sendUserAgent = sendUserAgent + self.customUserAgent = customUserAgent + } + + func image(from url: URL) async throws -> CGImage { + if let task = ongoingTasks[url] { + return try await task.value + } + + let domain = self.domain + let sendUserAgent = self.sendUserAgent + let customUserAgent = self.customUserAgent + + let task = Task<CGImage, Error> { + guard let data = await ImageCacheManager.shared.loadImageData( + for: url, + domain: domain, + sendUserAgent: sendUserAgent, + customUserAgent: customUserAgent + ) else { + throw URLError(.badServerResponse) + } + + guard + let source = CGImageSourceCreateWithData(data as CFData, nil), + let image = CGImageSourceCreateImageAtIndex( + source, + 0, + [kCGImageSourceShouldCache: true] as CFDictionary + ) + else { + throw URLError(.cannotDecodeContentData) + } + + return image + } + + ongoingTasks[url] = task + + defer { ongoingTasks.removeValue(forKey: url) } + + return try await task.value + } +} diff --git a/Sora/Data/Booru/BooruRequestConfiguration.swift b/Sora/Data/Booru/BooruRequestConfiguration.swift new file mode 100644 index 0000000..2b786cf --- /dev/null +++ b/Sora/Data/Booru/BooruRequestConfiguration.swift @@ -0,0 +1,66 @@ +import Foundation + +enum BooruRequestConfiguration { + static func resolvedUserAgent( + sendUserAgent: Bool, + customUserAgent: String + ) -> String? { + guard sendUserAgent else { return nil } + + let trimmedCustomUserAgent = customUserAgent.trimmingCharacters( + in: .whitespacesAndNewlines + ) + + guard trimmedCustomUserAgent.isEmpty else { + return trimmedCustomUserAgent + } + + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" + + return "Sora/\(version) (Build \(buildNumber))" + } + + static func baseReferer(for domain: String) -> String { + "https://\(domain)/" + } + + static func request( + url: URL, + referer: String, + userAgent: String?, + accept: String? = nil + ) -> URLRequest { + var request = URLRequest(url: url) + + request.setValue(referer, forHTTPHeaderField: "Referer") + + if let userAgent { + request.setValue(userAgent, forHTTPHeaderField: "User-Agent") + } + + if let accept { + request.setValue(accept, forHTTPHeaderField: "Accept") + } + + return request + } + + static func request( + url: URL, + domain: String, + sendUserAgent: Bool, + customUserAgent: String, + accept: String? = nil + ) -> URLRequest { + request( + url: url, + referer: baseReferer(for: domain), + userAgent: resolvedUserAgent( + sendUserAgent: sendUserAgent, + customUserAgent: customUserAgent + ), + accept: accept + ) + } +} diff --git a/Sora/Data/ImageCacheManager.swift b/Sora/Data/ImageCacheManager.swift index 5561f24..c5a4c6e 100644 --- a/Sora/Data/ImageCacheManager.swift +++ b/Sora/Data/ImageCacheManager.swift @@ -35,9 +35,22 @@ final class ImageCacheManager { } // MARK: - Public Methods - func preloadImages(_ urls: [URL]) { + func preloadImages( + _ urls: [URL], + domain: String, + sendUserAgent: Bool, + customUserAgent: String + ) { let newURLs = urls.filter { url in - !preloadingURLs.contains(url) && cache.cachedResponse(for: URLRequest(url: url)) == nil + let request = BooruRequestConfiguration.request( + url: url, + domain: domain, + sendUserAgent: sendUserAgent, + customUserAgent: customUserAgent, + accept: "image/*" + ) + + return !preloadingURLs.contains(url) && cache.cachedResponse(for: request) == nil } for url in newURLs { @@ -47,8 +60,15 @@ final class ImageCacheManager { guard let self else { return } let completionSignal = DispatchSemaphore(value: 0) - - URLSession.shared.dataTask(with: url) { data, response, _ in + let request = BooruRequestConfiguration.request( + url: url, + domain: domain, + sendUserAgent: sendUserAgent, + customUserAgent: customUserAgent, + accept: "image/*" + ) + + URLSession.shared.dataTask(with: request) { data, response, _ in Task { @MainActor [weak self] in guard let self else { completionSignal.signal() @@ -65,7 +85,7 @@ final class ImageCacheManager { storeCachedResponse( data: data, response: response, - for: url + for: request ) } } @@ -81,29 +101,42 @@ final class ImageCacheManager { preloadingURLs.removeAll() } - func storeCachedResponse(data: Data, response: URLResponse, for url: URL) { + func storeCachedResponse(data: Data, response: URLResponse, for request: URLRequest) { cache.storeCachedResponse( CachedURLResponse(response: response, data: data), - for: URLRequest(url: url) + for: request ) } - func getCachedResponse(for url: URL) -> CachedURLResponse? { - cache.cachedResponse(for: URLRequest(url: url)) + func getCachedResponse(for request: URLRequest) -> CachedURLResponse? { + cache.cachedResponse(for: request) } - func loadImageData(for url: URL) async -> Data? { - if let cachedResponse = getCachedResponse(for: url) { + func loadImageData( + for url: URL, + domain: String, + sendUserAgent: Bool, + customUserAgent: String + ) async -> Data? { + let request = BooruRequestConfiguration.request( + url: url, + domain: domain, + sendUserAgent: sendUserAgent, + customUserAgent: customUserAgent, + accept: "image/*" + ) + + if let cachedResponse = getCachedResponse(for: request) { return cachedResponse.data } do { - let (data, response) = try await URLSession.shared.data(from: url) + let (data, response) = try await URLSession.shared.data(for: request) storeCachedResponse( data: data, response: response, - for: url + for: request ) return data diff --git a/Sora/Views/FavoritePostThumbnailView.swift b/Sora/Views/FavoritePostThumbnailView.swift index 661f34a..41113b9 100644 --- a/Sora/Views/FavoritePostThumbnailView.swift +++ b/Sora/Views/FavoritePostThumbnailView.swift @@ -5,6 +5,13 @@ struct FavoritePostThumbnailView: View { @EnvironmentObject var settings: SettingsManager let favorite: SettingsFavoritePost let onRemove: () -> Void + private var networkImageLoader: BooruNetworkImageLoader { + BooruNetworkImageLoader( + domain: favorite.provider.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent + ) + } private var thumbnailURL: URL? { switch settings.thumbnailQuality { @@ -55,6 +62,7 @@ struct FavoritePostThumbnailView: View { } placeholder: { PostGridThumbnailPlaceholderView() } + .networkImageLoader(networkImageLoader) } } diff --git a/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift b/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift index f733a89..e43be47 100644 --- a/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift +++ b/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift @@ -14,7 +14,9 @@ struct PostDetailsCarouselView: View { posts: [BooruPost], loadingStage: Binding<BooruPostLoadingState>, focusedPost: BooruPost? = nil, - onFocusedPostChange: @escaping (BooruPost) -> Void = { _ in } + onFocusedPostChange: @escaping (BooruPost) -> Void = { _ in + // Default no-op callback for previews and callers that don't need focus updates. + } ) { self.posts = posts self.focusedPost = focusedPost @@ -101,7 +103,12 @@ struct PostDetailsCarouselView: View { urlsToPreload.append(posts[index].previewURL) } - cacheManager.preloadImages(urlsToPreload) + cacheManager.preloadImages( + urlsToPreload, + domain: manager.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent + ) } private func syncCurrentIndexWithFocus() { diff --git a/Sora/Views/Post/Details/PostDetailsImageView.swift b/Sora/Views/Post/Details/PostDetailsImageView.swift index 1e0da72..fd61afd 100644 --- a/Sora/Views/Post/Details/PostDetailsImageView.swift +++ b/Sora/Views/Post/Details/PostDetailsImageView.swift @@ -2,7 +2,7 @@ import NetworkImage import SwiftUI import UserNotifications -struct PostDetailsImageView<Placeholder: View>: View { +struct PostDetailsImageView<Placeholder: View>: View { // swiftlint:disable:this type_body_length @EnvironmentObject var settings: SettingsManager @EnvironmentObject var manager: BooruManager var url: URL? @@ -10,6 +10,13 @@ struct PostDetailsImageView<Placeholder: View>: View { var finalLoadingState: BooruPostLoadingState let placeholder: () -> Placeholder let post: BooruPost? + private var networkImageLoader: BooruNetworkImageLoader { + BooruNetworkImageLoader( + domain: manager.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent + ) + } #if os(iOS) var keyWindow: UIWindow? { @@ -53,7 +60,12 @@ struct PostDetailsImageView<Placeholder: View>: View { guard let imageURL = url else { return } Task(priority: .userInitiated) { - guard let imageData = await ImageCacheManager.shared.loadImageData(for: imageURL), + guard let imageData = await ImageCacheManager.shared.loadImageData( + for: imageURL, + domain: manager.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent + ), let uiImage = UIImage(data: imageData) else { return } @@ -79,7 +91,12 @@ struct PostDetailsImageView<Placeholder: View>: View { #if os(iOS) Task(priority: .userInitiated) { guard let imageURL = url else { return } - guard let imageData = await ImageCacheManager.shared.loadImageData(for: imageURL), + guard let imageData = await ImageCacheManager.shared.loadImageData( + for: imageURL, + domain: manager.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent + ), let uiImage = UIImage(data: imageData) else { return } @@ -125,6 +142,7 @@ struct PostDetailsImageView<Placeholder: View>: View { placeholder() .onAppear { loadingState = .loadingPreview } } + .networkImageLoader(networkImageLoader) #if os(macOS) return content.overlay( @@ -214,8 +232,15 @@ struct PostDetailsImageView<Placeholder: View>: View { let detailViewQuality = settings.detailViewQuality let saveTagsToFile = settings.saveTagsToFile let post = self.post + let request = BooruRequestConfiguration.request( + url: url, + domain: manager.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent, + accept: "image/*" + ) - URLSession.shared.dataTask(with: url) { data, _, _ in + URLSession.shared.dataTask(with: request) { data, _, _ in guard let data, let post else { return } let picturesURL = FileManager.default.homeDirectoryForCurrentUser diff --git a/Sora/Views/Post/Grid/PostGridThumbnailView.swift b/Sora/Views/Post/Grid/PostGridThumbnailView.swift index 5316731..5f7829e 100644 --- a/Sora/Views/Post/Grid/PostGridThumbnailView.swift +++ b/Sora/Views/Post/Grid/PostGridThumbnailView.swift @@ -10,6 +10,13 @@ struct PostGridThumbnailView: View { let endOfData: Bool let onLoadNextPage: () async -> Void let selectedPost: BooruPost? + private var networkImageLoader: BooruNetworkImageLoader { + BooruNetworkImageLoader( + domain: manager.domain, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent + ) + } private var thumbnailURL: URL? { switch settings.thumbnailQuality { @@ -74,6 +81,7 @@ struct PostGridThumbnailView: View { } placeholder: { PostGridThumbnailPlaceholderView() } + .networkImageLoader(networkImageLoader) } } } |