diff --git a/LANreader.xcodeproj/project.pbxproj b/LANreader.xcodeproj/project.pbxproj index 2c6a3d6..17f47bf 100644 --- a/LANreader.xcodeproj/project.pbxproj +++ b/LANreader.xcodeproj/project.pbxproj @@ -864,7 +864,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_ASSET_PATHS = "\"LANreader/Preview Content\""; DEVELOPMENT_TEAM = UUEBW58SA6; ENABLE_PREVIEWS = YES; @@ -876,7 +876,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.jif.LANreader; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -893,7 +893,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_ASSET_PATHS = "\"LANreader/Preview Content\""; DEVELOPMENT_TEAM = UUEBW58SA6; ENABLE_PREVIEWS = YES; @@ -905,7 +905,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.jif.LANreader; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -972,7 +972,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = UUEBW58SA6; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Action/Info.plist; @@ -984,7 +984,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.jif.LANreader.Action; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1001,7 +1001,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = UUEBW58SA6; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Action/Info.plist; @@ -1013,7 +1013,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.jif.LANreader.Action; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/LANreader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LANreader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17afd97..90dd84a 100644 --- a/LANreader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LANreader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "8e8ca36c02abc7775a3ab81f9468c0df5fe22c2c", - "version" : "1.1.7" + "revision" : "9b77fbd07b9529312f7e9adb10f5131acd9e2363", + "version" : "1.2.0" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "2ec6c3a15293efff6083966b38439a4004f25565", - "version" : "1.3.0" + "revision" : "7ab04c6e2e6a73d34d5a762970ef88bf0aedb084", + "version" : "1.4.0" } }, { diff --git a/LANreader/Page/PageImageV2.swift b/LANreader/Page/PageImageV2.swift index 19a25ab..25bf0d6 100644 --- a/LANreader/Page/PageImageV2.swift +++ b/LANreader/Page/PageImageV2.swift @@ -13,25 +13,25 @@ import Logging @SharedReader(.appStorage(SettingsKey.splitWideImage)) var splitImage = false @SharedReader(.appStorage(SettingsKey.splitPiorityLeft)) var piorityLeft = false - var image: Data? let pageId: String let suffix: String let pageNumber: Int var loading: Bool = false var progress: Double = 0 var errorMessage = "" - var pageMode: PageMode = .normal + var pageMode: PageMode let cached: Bool var id: String { "\(pageId)-\(suffix)" } + let folder: URL? let path: URL? let pathLeft: URL? let pathRight: URL? - init(archiveId: String, pageId: String, pageNumber: Int, pageMode: PageMode = .normal, cached: Bool = false) { + init(archiveId: String, pageId: String, pageNumber: Int, pageMode: PageMode = .loading, cached: Bool = false) { self.pageId = pageId self.pageNumber = pageNumber self.pageMode = pageMode @@ -42,14 +42,12 @@ import Logging } else { LANraragiService.downloadPath } - self.path = imagePath? - .appendingPathComponent(archiveId, conformingTo: .folder) + self.folder = imagePath?.appendingPathComponent(archiveId, conformingTo: .folder) + self.path = self.folder? .appendingPathComponent("\(pageNumber)", conformingTo: .image) - self.pathLeft = imagePath? - .appendingPathComponent(archiveId, conformingTo: .folder) + self.pathLeft = self.folder? .appendingPathComponent("\(pageNumber)-left", conformingTo: .image) - self.pathRight = imagePath? - .appendingPathComponent(archiveId, conformingTo: .folder) + self.pathRight = self.folder? .appendingPathComponent("\(pageNumber)-right", conformingTo: .image) } } @@ -60,7 +58,7 @@ import Logging case subscribeToProgress(DownloadRequest) case cancelSubscribeImageProgress case setProgress(Double) - case setImage(Data, Data?, Data?) + case setImage(PageMode, Bool) case setError(String) case insertPage(PageMode) } @@ -93,26 +91,34 @@ import Logging } state.loading = true + let previousPageMode = state.pageMode + if force { - state.image = nil - } else { - switch state.pageMode { - case .normal: - if let path = state.path { - state.image = try? Data(contentsOf: path) - } - case .left: - if let path = state.pathLeft { - state.image = try? Data(contentsOf: path) - } - case .right: - if let path = state.pathRight { - state.image = try? Data(contentsOf: path) + state.pageMode = .loading + } else if state.pageMode == .loading { + if state.splitImage && !state.fallback { + if state.piorityLeft && + FileManager.default.fileExists( + atPath: state.pathLeft?.path(percentEncoded: false) ?? "" + ) { + state.pageMode = .left + return .send(.insertPage(.right)) + } else if FileManager.default.fileExists( + atPath: state.pathRight?.path(percentEncoded: false) ?? "" + ) { + state.pageMode = .right + return .send(.insertPage(.left)) } } + if FileManager.default.fileExists(atPath: state.path?.path(percentEncoded: false) ?? "") { + state.pageMode = .normal + return .none + } + } else { + return .none } - if state.image == nil { + if state.pageMode == .loading { if state.cached { state.loading = false return .send(.setError(String(localized: "archive.cache.page.load.failed"))) @@ -121,38 +127,28 @@ import Logging do { let task = service.fetchArchivePage(page: state.pageId, pageNumber: state.pageNumber) await send(.subscribeToProgress(task)) - let imageData = try await task - .serializingData() + let imageUrl = try await task + .serializingDownloadedFileURL() .value await send(.cancelSubscribeImageProgress) if !state.showOriginal { await send(.setProgress(2.0)) } - let (processedImage, leftImage, rightImage) = imageService.resizeImage( - data: imageData, + let splitted = imageService.resizeImage( + imageUrl: imageUrl, + destinationUrl: state.folder!, + pageNumber: String(state.pageNumber), split: state.splitImage && !state.fallback, skip: state.showOriginal ) - await send(.setImage(processedImage, leftImage, rightImage)) + await send(.setImage(previousPageMode, splitted)) } catch { logger.error("failed to load image. \(error)") } await send(.setIsLoading(false)) } } - } else { - if state.cached && state.pageMode == .normal && state.splitImage && !state.fallback { - if state.piorityLeft, let path = state.pathLeft, let leftImage = try? Data(contentsOf: path) { - state.pageMode = .left - state.image = leftImage - return .send(.insertPage(.right)) - } else if let path = state.pathRight, let rightImage = try? Data(contentsOf: path) { - state.pageMode = .right - state.image = rightImage - return .send(.insertPage(.left)) - } - } } state.loading = false return .none @@ -162,34 +158,23 @@ import Logging case let .setProgress(progres): state.progress = progres return .none - case let .setImage(processedImage, leftImage, rightImage): + case let .setImage(previousPageMode, splitted): state.progress = 0 state.loading = false - if leftImage != nil && rightImage != nil { - if let path = state.pathLeft { - try? leftImage!.write(to: path) - } - if let path = state.pathRight { - try? rightImage!.write(to: path) + if splitted { + if previousPageMode == .left || previousPageMode == .right { + state.pageMode = previousPageMode + return .none } - switch state.pageMode { - case .normal: - if state.piorityLeft { - state.pageMode = .left - state.image = leftImage - return .send(.insertPage(.right)) - } else { - state.pageMode = .right - state.image = rightImage - return .send(.insertPage(.left)) - } - case .left: - state.image = leftImage - case .right: - state.image = rightImage + if state.piorityLeft { + state.pageMode = .left + return .send(.insertPage(.right)) + } else { + state.pageMode = .right + return .send(.insertPage(.left)) } } else { - state.image = processedImage + state.pageMode = .normal } return .none case let .setError(message): @@ -215,17 +200,7 @@ struct PageImageV2: View { // If not wrapped in ZStack, TabView will render ALL pages when initial load ZStack { if visible { - if let imageData = store.image { - if let uiImage = UIImage(data: imageData) { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(contentMode: .fit) - .draggableAndZoomable(contentSize: geometrySize) - } else { - Image(systemName: "rectangle.slash") - .frame(height: geometrySize.height) - } - } else { + if store.pageMode == .loading { ProgressView( value: store.progress > 1 ? 1 : store.progress, total: 1 @@ -243,10 +218,31 @@ struct PageImageV2: View { .task { store.send(.load(false)) } + } else { + let contentPath = { + switch store.pageMode { + case .left: + return store.pathLeft + case .right: + return store.pathRight + default: + return store.path + } + }() + AsyncImage(url: contentPath) { image in + image + .resizable() + .aspectRatio(contentMode: .fit) + .draggableAndZoomable(contentSize: geometrySize) + } placeholder: { + Image(systemName: "rectangle.slash") + .frame(height: geometrySize.height) + } } } else { Color.clear } + } .onAppear { visible = true @@ -258,6 +254,7 @@ struct PageImageV2: View { } enum PageMode: String { + case loading case left case right case normal diff --git a/LANreader/Service/ImageService.swift b/LANreader/Service/ImageService.swift index 9cca8a3..6ba101a 100644 --- a/LANreader/Service/ImageService.swift +++ b/LANreader/Service/ImageService.swift @@ -1,43 +1,35 @@ import Foundation import UIKit import CoreGraphics -import func AVFoundation.AVMakeRect import Dependencies class ImageService { private static var _shared: ImageService? - // swiftlint:disable large_tuple - func resizeImage(data: Data, split: Bool, skip: Bool) -> (Data, Data?, Data?) { - var imageData: Data = data - var leftImageData: Data? - var rightImageData: Data? - guard !skip, let image = UIImage(data: data) else { return (imageData, leftImageData, rightImageData) } - let screenRect = AVMakeRect(aspectRatio: image.size, insideRect: UIScreen.main.bounds) - let imagePixels = image.size.width * image.scale * image.size.height * image.scale - let screenPixels = screenRect.size.width * UIScreen.main.scale * screenRect.size.height * UIScreen.main.scale + func resizeImage(imageUrl: URL, destinationUrl: URL, pageNumber: String, split: Bool, skip: Bool) -> Bool { + try? FileManager.default.createDirectory(at: destinationUrl, withIntermediateDirectories: true) + let mainPath = destinationUrl.appendingPathComponent(pageNumber, conformingTo: .image) - let drawSize = CGSize( - width: screenRect.size.width * 1.5, - height: screenRect.size.height * 1.5 - ) - - if imagePixels > screenPixels * 2 { - let renderer = UIGraphicsImageRenderer(size: drawSize) - let image = renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: drawSize)) - } - imageData = image.jpegData(compressionQuality: 0.8) ?? data + // if use UIImage(contentsOfFile:) directly, IOSurface creation failed warning may happen + // Same thing happens in PageImageV2 + guard let imageData = try? Data(contentsOf: imageUrl), + let image = UIImage(data: imageData) else { return false } + var splitted = false + if skip { + try? FileManager.default.moveItem(at: imageUrl, to: mainPath) + } else { + try? image.heicData()?.write(to: mainPath) } if split && (image.size.width / image.size.height > 1.2) { - leftImageData = image.leftHalf?.jpegData(compressionQuality: 0.8) - rightImageData = image.rightHalf?.jpegData(compressionQuality: 0.8) + let leftPath = destinationUrl.appendingPathComponent("\(pageNumber)-left", conformingTo: .image) + let rightPath = destinationUrl.appendingPathComponent("\(pageNumber)-right", conformingTo: .image) + try? image.leftHalf?.heicData()?.write(to: leftPath) + try? image.rightHalf?.heicData()?.write(to: rightPath) + splitted = true } - - return (imageData, leftImageData, rightImageData) + return splitted } - // swiftlint:enable large_tuple public static var shared: ImageService { if _shared == nil { diff --git a/LANreader/Service/LANraragiService.swift b/LANreader/Service/LANraragiService.swift index 57dd464..014c00b 100644 --- a/LANreader/Service/LANraragiService.swift +++ b/LANreader/Service/LANraragiService.swift @@ -300,21 +300,15 @@ extension LANraragiService: URLSessionDelegate, URLSessionDownloadDelegate { if let archiveId = task.originalRequest?.value(forHTTPHeaderField: "X-Archive-Id"), let pageNumber = task.originalRequest?.value(forHTTPHeaderField: "X-Page-Number"), - let imageData = try? Data(contentsOf: location) { - let (processImage, leftImage, rightImage) = imageService.resizeImage( - data: imageData, split: splitImage && !fallback, skip: showOriginal + let cachePath = LANraragiService.cachePath { + let folder = cachePath.appendingPathComponent(archiveId, conformingTo: .folder) + _ = imageService.resizeImage( + imageUrl: location, + destinationUrl: folder, + pageNumber: pageNumber, + split: splitImage && !fallback, + skip: showOriginal ) - let folder = LANraragiService.cachePath! - .appendingPathComponent(archiveId, conformingTo: .folder) - let path = folder.appendingPathComponent(pageNumber, conformingTo: .image) - try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) - try? processImage.write(to: path) - if leftImage != nil && rightImage != nil { - let leftPath = folder.appendingPathComponent("\(pageNumber)-left", conformingTo: .image) - let rightPath = folder.appendingPathComponent("\(pageNumber)-right", conformingTo: .image) - try? leftImage?.write(to: leftPath) - try? rightImage?.write(to: rightPath) - } } } } diff --git a/LANreaderTests/Service/LANraragiServiceTest.swift b/LANreaderTests/Service/LANraragiServiceTest.swift index 139365d..981c95a 100644 --- a/LANreaderTests/Service/LANraragiServiceTest.swift +++ b/LANreaderTests/Service/LANraragiServiceTest.swift @@ -190,7 +190,7 @@ class LANraragiServiceTest: XCTestCase { stub(condition: isHost("localhost") && isPath("/api/categories/SET_12345678") && isMethodPUT() - && hasBody("name=name&pinned=0&search=search".data(using: .utf8)!) + && hasBody(Data("name=name&pinned=0&search=search".utf8)) && hasHeaderNamed("Authorization", value: "Bearer YXBpS2V5")) { _ in HTTPStubsResponse( fileAtPath: OHPathForFile("UpdateSearchCategoryResponse.json", type(of: self))!, @@ -284,7 +284,7 @@ class LANraragiServiceTest: XCTestCase { stub(condition: isHost("localhost") && isPath("/api/archives/id/metadata") && isMethodPUT() - && hasBody("tags=tags&title=name".data(using: .utf8)!) + && hasBody(Data("tags=tags&title=name".utf8)) && hasHeaderNamed("Authorization", value: "Bearer YXBpS2V5")) { _ in HTTPStubsResponse( fileAtPath: OHPathForFile("SetArchiveMetadataResponse.json", type(of: self))!, @@ -303,7 +303,7 @@ class LANraragiServiceTest: XCTestCase { stub(condition: isHost("localhost") && isPath("/api/archives/id/metadata") && isMethodPUT() - && hasBody("tags=tags&title=name".data(using: .utf8)!) + && hasBody(Data("tags=tags&title=name".utf8)) && hasHeaderNamed("Authorization", value: "Bearer YXBpS2V5")) { _ in HTTPStubsResponse( fileAtPath: OHPathForFile("UnauthorizedResponse.json", type(of: self))!,