Show classifier download progress
This commit is contained in:
parent
f55f4e65dd
commit
d5edf360fb
@ -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;
|
||||||
|
Binary file not shown.
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
@ -8,11 +8,28 @@ 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
|
||||||
|
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
@ -147,6 +164,9 @@ final class Database: ObservableObject {
|
|||||||
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 {
|
||||||
|
23
Caps/Views/ClassifierDownloadView.swift
Normal file
23
Caps/Views/ClassifierDownloadView.swift
Normal 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))
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user