Show classifier download progress

This commit is contained in:
Christoph Hagen 2023-10-24 14:51:08 +02:00
parent f55f4e65dd
commit d5edf360fb
7 changed files with 147 additions and 18 deletions

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 88C1511C29A11ADF0080EF4F /* CapImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */; };
88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; }; 88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; };
E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20D1049285612AF0019BD91 /* ImageGrid.swift */; }; E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20D1049285612AF0019BD91 /* ImageGrid.swift */; };
@ -52,6 +53,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassifierDownloadView.swift; sourceTree = "<group>"; };
88C1511B29A11ADF0080EF4F /* CapImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapImagesView.swift; sourceTree = "<group>"; }; 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapImagesView.swift; sourceTree = "<group>"; };
88DBE72D285495B100D1573B /* FancyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTextField.swift; sourceTree = "<group>"; }; 88DBE72D285495B100D1573B /* FancyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTextField.swift; sourceTree = "<group>"; };
E20D1049285612AF0019BD91 /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; }; E20D1049285612AF0019BD91 /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; };
@ -192,6 +194,7 @@
E2EA00CD283EBEB600F7B269 /* SearchField.swift */, E2EA00CD283EBEB600F7B269 /* SearchField.swift */,
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */, E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */,
E2EA00E0283F658E00F7B269 /* SettingsView.swift */, E2EA00E0283F658E00F7B269 /* SettingsView.swift */,
882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */,
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */, E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */,
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */, E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */,
88C1511B29A11ADF0080EF4F /* CapImagesView.swift */, 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */,
@ -245,7 +248,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1340; LastSwiftUpdateCheck = 1340;
LastUpgradeCheck = 1340; LastUpgradeCheck = 1500;
TargetAttributes = { TargetAttributes = {
E25AAC77283D855D006E9E7F = { E25AAC77283D855D006E9E7F = {
CreatedOnToolsVersion = 13.4; CreatedOnToolsVersion = 13.4;
@ -327,6 +330,7 @@
E20D105028574E190019BD91 /* CapImage.swift in Sources */, E20D105028574E190019BD91 /* CapImage.swift in Sources */,
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */, E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */,
E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */, E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */,
882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */,
E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */, E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */,
E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */, E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */,
); );
@ -371,6 +375,7 @@
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -431,6 +436,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1340" LastUpgradeVersion = "1500"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -288,7 +288,7 @@ struct ContentView: View {
} }
.alert(isPresented: $showNewClassifierAlert) { .alert(isPresented: $showNewClassifierAlert) {
Alert(title: Text("New classifier available"), Alert(title: Text("New classifier available"),
message: Text("Classifier \(database.serverClassifierVersion) is available. You have version \(database.classifierVersion). Do you want to download it now?"), message: Text("Classifier \(database.serverClassifierVersion) is available. You have version \(database.localClassifierVersion). Do you want to download it now?"),
primaryButton: .default(Text("Download"), action: downloadClassifier), primaryButton: .default(Text("Download"), action: downloadClassifier),
secondaryButton: .cancel()) secondaryButton: .cancel())
} }
@ -329,8 +329,8 @@ struct ContentView: View {
private func refresh() { private func refresh() {
Task { Task {
await database.downloadCaps() await database.downloadCaps()
let hasNewClassifier = await database.serverHasNewClassifier() await database.updateServerClassifierVersion()
guard hasNewClassifier else { guard database.hasNewClassifier else {
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -8,10 +8,27 @@ final class Database: ObservableObject {
private let imageCompressionQuality: CGFloat = 0.3 private let imageCompressionQuality: CGFloat = 0.3
@AppStorage("classifier") @AppStorage("classifier")
private(set) var classifierVersion = 0 private var storedLocalClassifierVersion = 0 {
didSet { localClassifierVersion = storedLocalClassifierVersion }
}
@AppStorage("serverClassifier") @AppStorage("serverClassifier")
private var storedServerClassifierVersion = 0 {
didSet { serverClassifierVersion = storedServerClassifierVersion }
}
@Published
private(set) var localClassifierVersion = 0
@Published
private(set) var serverClassifierVersion = 0 private(set) var serverClassifierVersion = 0
@Published
private(set) var classifierDownloadProgress: ClassifierProgress?
var hasNewClassifier: Bool {
serverClassifierVersion > localClassifierVersion
}
let images: ImageCache let images: ImageCache
@ -146,6 +163,9 @@ final class Database: ObservableObject {
folder: imageFolder, folder: imageFolder,
server: server, server: server,
thumbnailSize: CapsApp.thumbnailImageSize) thumbnailSize: CapsApp.thumbnailImageSize)
self.localClassifierVersion = storedLocalClassifierVersion
self.serverClassifierVersion = storedServerClassifierVersion
ensureFolderExistence(gridStorageFolder) ensureFolderExistence(gridStorageFolder)
loadCaps() loadCaps()
@ -329,7 +349,7 @@ final class Database: ObservableObject {
} }
@discardableResult @discardableResult
func serverHasNewClassifier() async -> Bool { func updateServerClassifierVersion() async -> Bool {
let data: Data let data: Data
let response: URLResponse let response: URLResponse
do { do {
@ -351,23 +371,29 @@ final class Database: ObservableObject {
return false return false
} }
DispatchQueue.main.async { 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 return true
} }
@discardableResult @discardableResult
func downloadClassifier() async -> Bool { func downloadClassifier() async -> Bool {
log("Downloading classifier") log("Downloading classifier")
let progress = ClassifierProgress()
DispatchQueue.main.async {
self.classifierDownloadProgress = progress
}
defer {
DispatchQueue.main.async {
self.classifierDownloadProgress = nil
}
}
let tempUrl: URL let tempUrl: URL
let response: URLResponse let response: URLResponse
do { do {
(tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl) (tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl, delegate: progress)
} catch { } catch {
log("Failed to download classifier version: \(error)") log("Failed to download classifier version: \(error)")
return false return false
@ -386,10 +412,10 @@ final class Database: ObservableObject {
return false return false
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.classifierVersion = self.serverClassifierVersion self.storedLocalClassifierVersion = self.serverClassifierVersion
log("Downloaded classifier \(self.localClassifierVersion)")
self.classifier = nil self.classifier = nil
} }
log("Downloaded classifier \(classifierVersion)")
return true 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 { extension Database {
static var mock: Database { static var mock: Database {

View File

@ -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))
}

View File

@ -42,8 +42,28 @@ struct SettingsView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.top) .padding(.top)
Group { 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)") 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) }.padding(.horizontal)
Text("Storage") Text("Storage")
.font(.footnote) .font(.footnote)
@ -56,7 +76,9 @@ struct SettingsView: View {
SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize)) SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize))
HStack { HStack {
Spacer() Spacer()
Button("Clear image cache", action: clearImageCache) Button(action: clearImageCache) {
Label("Clear image cache", systemSymbol: .trash)
}
.padding() .padding()
Spacer() 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() { private func hide() {
isPresented = false isPresented = false
} }