diff --git a/Caps.xcodeproj/project.pbxproj b/Caps.xcodeproj/project.pbxproj index 938af63..b4f58df 100644 --- a/Caps.xcodeproj/project.pbxproj +++ b/Caps.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */; }; 88C1511C29A11ADF0080EF4F /* CapImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */; }; 88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; }; E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20D1049285612AF0019BD91 /* ImageGrid.swift */; }; @@ -52,6 +53,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassifierDownloadView.swift; sourceTree = ""; }; 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapImagesView.swift; sourceTree = ""; }; 88DBE72D285495B100D1573B /* FancyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTextField.swift; sourceTree = ""; }; E20D1049285612AF0019BD91 /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = ""; }; @@ -192,6 +194,7 @@ E2EA00CD283EBEB600F7B269 /* SearchField.swift */, E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */, E2EA00E0283F658E00F7B269 /* SettingsView.swift */, + 882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */, E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */, E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */, 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */, @@ -245,7 +248,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1340; - LastUpgradeCheck = 1340; + LastUpgradeCheck = 1500; TargetAttributes = { E25AAC77283D855D006E9E7F = { CreatedOnToolsVersion = 13.4; @@ -327,6 +330,7 @@ E20D105028574E190019BD91 /* CapImage.swift in Sources */, E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */, E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */, + 882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */, E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */, E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */, ); @@ -371,6 +375,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -431,6 +436,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate b/Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate index fd9aa49..a938d06 100644 Binary files a/Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate and b/Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Caps.xcodeproj/xcshareddata/xcschemes/Caps.xcscheme b/Caps.xcodeproj/xcshareddata/xcschemes/Caps.xcscheme index 794df71..7e22430 100644 --- a/Caps.xcodeproj/xcshareddata/xcschemes/Caps.xcscheme +++ b/Caps.xcodeproj/xcshareddata/xcschemes/Caps.xcscheme @@ -1,6 +1,6 @@ localClassifierVersion + } let images: ImageCache @@ -146,6 +163,9 @@ final class Database: ObservableObject { folder: imageFolder, server: server, thumbnailSize: CapsApp.thumbnailImageSize) + + self.localClassifierVersion = storedLocalClassifierVersion + self.serverClassifierVersion = storedServerClassifierVersion ensureFolderExistence(gridStorageFolder) loadCaps() @@ -329,7 +349,7 @@ final class Database: ObservableObject { } @discardableResult - func serverHasNewClassifier() async -> Bool { + func updateServerClassifierVersion() async -> Bool { let data: Data let response: URLResponse do { @@ -351,23 +371,29 @@ final class Database: ObservableObject { return false } DispatchQueue.main.async { - self.serverClassifierVersion = serverVersion + self.storedServerClassifierVersion = serverVersion } - guard serverVersion > self.classifierVersion else { - log("No new classifier available (Local: \(classifierVersion) Server: \(serverVersion))") - return false - } - log("New classifier available (Local: \(classifierVersion) Server: \(serverVersion))") return true } @discardableResult func downloadClassifier() async -> Bool { log("Downloading classifier") + + let progress = ClassifierProgress() + DispatchQueue.main.async { + self.classifierDownloadProgress = progress + } + defer { + DispatchQueue.main.async { + self.classifierDownloadProgress = nil + } + } + let tempUrl: URL let response: URLResponse do { - (tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl) + (tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl, delegate: progress) } catch { log("Failed to download classifier version: \(error)") return false @@ -386,10 +412,10 @@ final class Database: ObservableObject { return false } DispatchQueue.main.async { - self.classifierVersion = self.serverClassifierVersion + self.storedLocalClassifierVersion = self.serverClassifierVersion + log("Downloaded classifier \(self.localClassifierVersion)") self.classifier = nil } - log("Downloaded classifier \(classifierVersion)") return true } @@ -987,6 +1013,44 @@ final class Database: ObservableObject { } } +extension Database { + + final class ClassifierProgress: NSObject, ObservableObject { + + @Published + var bytesLoaded: Double + + @Published + var total: Double + + var percentage: Double { + guard total > 0 else { + return 0.0 + } + return bytesLoaded * 100 / total + } + + init(bytesLoaded: Double = 0, total: Double = 0) { + self.bytesLoaded = bytesLoaded + self.total = total + } + } +} + +extension Database.ClassifierProgress: URLSessionDownloadDelegate { + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + DispatchQueue.main.async { + self.bytesLoaded = Double(totalBytesWritten) + self.total = Double(totalBytesExpectedToWrite) + } + } +} + extension Database { static var mock: Database { diff --git a/Caps/Views/ClassifierDownloadView.swift b/Caps/Views/ClassifierDownloadView.swift new file mode 100644 index 0000000..755d003 --- /dev/null +++ b/Caps/Views/ClassifierDownloadView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct ClassifierDownloadView: View { + + @ObservedObject + var progress: Database.ClassifierProgress + + var body: some View { + VStack { + ProgressView("Downloading classifier...", value: progress.bytesLoaded, total: Double(progress.total)) + .progressViewStyle(.linear) + HStack { + Text(String(format: "%.0f %%", progress.percentage)) + Spacer() + Text(String(format: "%.1f / %.1f MB", progress.bytesLoaded / 1_000_000, progress.total / 1_000_000 )) + } + } + } +} + +#Preview { + ClassifierDownloadView(progress: .init(bytesLoaded: 12_300_000, total: 24_500_000)) +} diff --git a/Caps/Views/SettingsView.swift b/Caps/Views/SettingsView.swift index eb43920..c8f5414 100644 --- a/Caps/Views/SettingsView.swift +++ b/Caps/Views/SettingsView.swift @@ -42,8 +42,28 @@ struct SettingsView: View { .foregroundColor(.secondary) .padding(.top) Group { - SettingsStatisticRow(label: "Version", value: "\(database.classifierVersion)") + SettingsStatisticRow(label: "Server Version", value: "\(database.serverClassifierVersion)") + SettingsStatisticRow(label: "Local Version", value: "\(database.localClassifierVersion)") SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)") + HStack { + Spacer() + Button(action: updateClassifierVersion) { + Label("Refresh", systemSymbol: .arrowCounterclockwise) + } + .disabled(database.classifierDownloadProgress != nil) + .padding() + if database.localClassifierVersion != database.serverClassifierVersion { + Button(action: downloadNewClassifier) { + Label("Download", systemSymbol: .squareAndArrowDown) + } + .disabled(database.classifierDownloadProgress != nil) + .padding() + } + Spacer() + } + if let progress = database.classifierDownloadProgress { + ClassifierDownloadView(progress: progress) + } }.padding(.horizontal) Text("Storage") .font(.footnote) @@ -56,7 +76,9 @@ struct SettingsView: View { SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize)) HStack { Spacer() - Button("Clear image cache", action: clearImageCache) + Button(action: clearImageCache) { + Label("Clear image cache", systemSymbol: .trash) + } .padding() Spacer() } @@ -82,6 +104,20 @@ struct SettingsView: View { } } + private func downloadNewClassifier() { + Task { + // Ensure that correct version is saved + await database.updateServerClassifierVersion() + await database.downloadClassifier() + } + } + + private func updateClassifierVersion() { + Task { + await database.updateServerClassifierVersion() + } + } + private func hide() { isPresented = false }