Improve progress updating, fix warnings
This commit is contained in:
parent
3dc5674a3a
commit
92e1eacf57
@ -29,6 +29,7 @@
|
||||
E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC93283D88A4006E9E7F /* Cap.swift */; };
|
||||
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC95283E14DF006E9E7F /* Database.swift */; };
|
||||
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC9A283E3395006E9E7F /* CapRowView.swift */; };
|
||||
E29E17C92D4D0ABF00E0EE54 /* Database+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */; };
|
||||
E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C2283E672A00F7B269 /* SFSafeSymbols */; };
|
||||
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C4283EA72000F7B269 /* SortCriteria.swift */; };
|
||||
E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */; };
|
||||
@ -78,6 +79,7 @@
|
||||
E25AAC95283E14DF006E9E7F /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||
E25AAC9A283E3395006E9E7F /* CapRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapRowView.swift; sourceTree = "<group>"; };
|
||||
E268C72D29BF2D8400D813A0 /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; };
|
||||
E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Mock.swift"; sourceTree = "<group>"; };
|
||||
E2EA00C4283EA72000F7B269 /* SortCriteria.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCriteria.swift; sourceTree = "<group>"; };
|
||||
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortSelectionView.swift; sourceTree = "<group>"; };
|
||||
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCaseRowView.swift; sourceTree = "<group>"; };
|
||||
@ -100,6 +102,10 @@
|
||||
E2ED70992A73D86F00067808 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
E29E17C32D4D0A9300E0EE54 /* Download */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Download; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
E25AAC75283D855D006E9E7F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
@ -133,6 +139,7 @@
|
||||
E25AAC7A283D855D006E9E7F /* Caps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29E17C32D4D0A9300E0EE54 /* Download */,
|
||||
E25AAC7B283D855D006E9E7F /* CapsApp.swift */,
|
||||
E25AAC7D283D855D006E9E7F /* ContentView.swift */,
|
||||
E2EA00CF283EDD2C00F7B269 /* Camera */,
|
||||
@ -149,6 +156,7 @@
|
||||
E25AAC81283D855F006E9E7F /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29E17C82D4D0ABF00E0EE54 /* Database+Mock.swift */,
|
||||
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
@ -234,6 +242,9 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
E29E17C32D4D0A9300E0EE54 /* Download */,
|
||||
);
|
||||
name = Caps;
|
||||
packageProductDependencies = (
|
||||
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */,
|
||||
@ -332,6 +343,7 @@
|
||||
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */,
|
||||
E2ED709A2A73D86F00067808 /* String+Extensions.swift in Sources */,
|
||||
E20D105028574E190019BD91 /* CapImage.swift in Sources */,
|
||||
E29E17C92D4D0ABF00E0EE54 /* Database+Mock.swift in Sources */,
|
||||
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */,
|
||||
E20D105228589AAC0019BD91 /* FileManager+Extensions.swift in Sources */,
|
||||
882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */,
|
||||
|
@ -1,12 +1,13 @@
|
||||
{
|
||||
"originHash" : "c78617470808606280f24ff566c34cb1b032779babfb39f1ddbcda602b363bd4",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "bottom-sheet",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/weitieda/bottom-sheet",
|
||||
"state" : {
|
||||
"revision" : "4e074d49f3148577ac66cf47b85a99d016480d01",
|
||||
"version" : "1.0.10"
|
||||
"revision" : "6b21007153365235418f3943a960a1f9546592e0",
|
||||
"version" : "1.0.12"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -19,5 +20,5 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
"version" : 3
|
||||
}
|
||||
|
Binary file not shown.
@ -3,6 +3,7 @@ import SwiftUI
|
||||
import Vision
|
||||
import CryptoKit
|
||||
|
||||
@MainActor
|
||||
final class Database: ObservableObject {
|
||||
|
||||
@AppStorage("classifier")
|
||||
@ -33,6 +34,8 @@ final class Database: ObservableObject {
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
private let progressObserver = ClassifierProgressObserver()
|
||||
|
||||
@AppStorage("serverUrl")
|
||||
var serverPath: String = "" {
|
||||
didSet {
|
||||
@ -150,9 +153,7 @@ final class Database: ObservableObject {
|
||||
}
|
||||
set {
|
||||
_classifierClassesCache = newValue
|
||||
DispatchQueue.main.async {
|
||||
self._classifierClassesString = newValue.map { "\($0)" }.joined(separator: ",")
|
||||
}
|
||||
self._classifierClassesString = newValue.map { "\($0)" }.joined(separator: ",")
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +188,11 @@ final class Database: ObservableObject {
|
||||
updatePendingImageUploadCount(imageUploads: imageUploads)
|
||||
}
|
||||
|
||||
convenience init(caps: [Int : Cap]) {
|
||||
self.init()
|
||||
self.caps = caps
|
||||
}
|
||||
|
||||
func mainImage(for cap: Int) -> Int {
|
||||
caps[cap]?.mainImage ?? 0
|
||||
}
|
||||
@ -358,9 +364,7 @@ final class Database: ObservableObject {
|
||||
} else {
|
||||
oldCap.update(with: cap)
|
||||
let save = oldCap
|
||||
DispatchQueue.main.async {
|
||||
self.caps[cap.id] = save
|
||||
}
|
||||
self.caps[cap.id] = save
|
||||
updates += 1
|
||||
}
|
||||
}
|
||||
@ -394,9 +398,7 @@ final class Database: ObservableObject {
|
||||
log("Classifier version has an invalid value '\(string)'")
|
||||
return false
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.storedServerClassifierVersion = serverVersion
|
||||
}
|
||||
self.storedServerClassifierVersion = serverVersion
|
||||
return true
|
||||
}
|
||||
|
||||
@ -409,19 +411,16 @@ final class Database: ObservableObject {
|
||||
log("Downloading classifier")
|
||||
|
||||
let progress = ClassifierProgress()
|
||||
DispatchQueue.main.async {
|
||||
self.classifierDownloadProgress = progress
|
||||
}
|
||||
self.classifierDownloadProgress = progress
|
||||
defer {
|
||||
DispatchQueue.main.async {
|
||||
self.classifierDownloadProgress = nil
|
||||
}
|
||||
self.classifierDownloadProgress = nil
|
||||
}
|
||||
|
||||
let tempUrl: URL
|
||||
let response: URLResponse
|
||||
do {
|
||||
(tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl, delegate: progress)
|
||||
progressObserver.delegate = self
|
||||
(tempUrl, response) = try await URLSession.shared.download(from: serverClassifierUrl, delegate: progressObserver)
|
||||
} catch {
|
||||
log("Failed to download classifier version: \(error)")
|
||||
return false
|
||||
@ -439,14 +438,14 @@ final class Database: ObservableObject {
|
||||
log("Failed to replace classifier: \(error)")
|
||||
return false
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.storedLocalClassifierVersion = self.serverClassifierVersion
|
||||
log("Downloaded classifier \(self.localClassifierVersion)")
|
||||
self.classifier = nil
|
||||
}
|
||||
|
||||
self.storedLocalClassifierVersion = self.serverClassifierVersion
|
||||
log("Downloaded classifier \(self.localClassifierVersion)")
|
||||
self.classifier = nil
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
func downloadClassifierClasses() async -> Bool {
|
||||
guard let serverClassifierClassesUrl else {
|
||||
@ -509,9 +508,7 @@ final class Database: ObservableObject {
|
||||
func save(newCap name: String) -> Cap {
|
||||
let cap = Cap(id: nextCapId, name: name)
|
||||
caps[cap.id] = cap
|
||||
DispatchQueue.main.async {
|
||||
self.changedCaps.insert(cap.id)
|
||||
}
|
||||
self.changedCaps.insert(cap.id)
|
||||
return cap
|
||||
}
|
||||
|
||||
@ -526,17 +523,11 @@ final class Database: ObservableObject {
|
||||
}
|
||||
log("Saved image \(cap.imageCount) for cap \(capId)")
|
||||
if imageUploads[capId] != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.imageUploads[capId]!.append(cap.imageCount)
|
||||
}
|
||||
self.imageUploads[capId]!.append(cap.imageCount)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.imageUploads[capId] = [cap.imageCount]
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.caps[capId]!.imageCount += 1
|
||||
self.imageUploads[capId] = [cap.imageCount]
|
||||
}
|
||||
self.caps[capId]!.imageCount += 1
|
||||
return true
|
||||
}
|
||||
|
||||
@ -558,14 +549,10 @@ final class Database: ObservableObject {
|
||||
return
|
||||
}
|
||||
log("Starting upload timer")
|
||||
DispatchQueue.main.async {
|
||||
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: self.uploadTimerElapsed)
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadTimerElapsed(timer: Timer) {
|
||||
Task {
|
||||
await uploadAll()
|
||||
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
|
||||
Task {
|
||||
await self.uploadAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,22 +561,16 @@ final class Database: ObservableObject {
|
||||
log("Already uploading")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isUploading = true
|
||||
}
|
||||
self.isUploading = true
|
||||
defer {
|
||||
DispatchQueue.main.async {
|
||||
self.isUploading = false
|
||||
}
|
||||
self.isUploading = false
|
||||
}
|
||||
guard !changedCaps.isEmpty || pendingImageUploadCount > 0 else {
|
||||
return
|
||||
}
|
||||
log("Starting uploads")
|
||||
let uploaded = await uploadAllChangedCaps()
|
||||
DispatchQueue.main.async {
|
||||
self.changedCaps.subtract(uploaded)
|
||||
}
|
||||
self.changedCaps.subtract(uploaded)
|
||||
await uploadAllImages()
|
||||
log("Uploads finished")
|
||||
}
|
||||
@ -622,14 +603,10 @@ final class Database: ObservableObject {
|
||||
}
|
||||
log("Uploaded image \(image) for cap \(cap)")
|
||||
let remaining = imageUploads[cap]?.filter { $0 != image }
|
||||
if let r = remaining, !r.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
self.imageUploads[cap] = r
|
||||
}
|
||||
if let remaining, !remaining.isEmpty {
|
||||
self.imageUploads[cap] = remaining
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.imageUploads[cap] = nil
|
||||
}
|
||||
self.imageUploads[cap] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -664,9 +641,7 @@ final class Database: ObservableObject {
|
||||
if httpResponse.statusCode == 410 {
|
||||
log("Missing cap for image \(url.lastPathComponent), reupload cap")
|
||||
// Missing cap, force upload
|
||||
DispatchQueue.main.async {
|
||||
self.changedCaps.insert(cap)
|
||||
}
|
||||
self.changedCaps.insert(cap)
|
||||
} else {
|
||||
log("Failed to upload image \(url.lastPathComponent): Response \(httpResponse.statusCode)")
|
||||
}
|
||||
@ -736,9 +711,7 @@ final class Database: ObservableObject {
|
||||
log("Failed to upload cap \(cap.id): Response \(httpResponse.statusCode)")
|
||||
return false
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.changedCaps.remove(cap.id)
|
||||
}
|
||||
self.changedCaps.remove(cap.id)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to upload cap \(cap.id): \(error)")
|
||||
@ -761,10 +734,8 @@ final class Database: ObservableObject {
|
||||
}
|
||||
cap.mainImage = version
|
||||
let finalCap = cap
|
||||
DispatchQueue.main.async {
|
||||
self.caps[capId] = finalCap
|
||||
log("Set main image \(version) for \(capId)")
|
||||
}
|
||||
self.caps[capId] = finalCap
|
||||
log("Set main image \(version) for \(capId)")
|
||||
return finalCap
|
||||
}
|
||||
|
||||
@ -852,9 +823,7 @@ final class Database: ObservableObject {
|
||||
// Delete cached images
|
||||
images.removeCachedImages(for: cap)
|
||||
// Delete cap
|
||||
DispatchQueue.main.async {
|
||||
self.caps[cap] = nil
|
||||
}
|
||||
self.caps[cap] = nil
|
||||
log("Deleted cap \(cap)")
|
||||
return true
|
||||
} catch {
|
||||
@ -888,19 +857,24 @@ final class Database: ObservableObject {
|
||||
log("Image removed")
|
||||
return
|
||||
}
|
||||
DispatchQueue.global().async {
|
||||
Task {
|
||||
guard let classifier = self.getClassifier() else {
|
||||
return
|
||||
}
|
||||
log("Image classification started")
|
||||
classifier.recognize(image: image) { matches in
|
||||
DispatchQueue.main.async {
|
||||
self.matches = matches ?? [:]
|
||||
let matches = await withCheckedContinuation { continuation in
|
||||
classifier.recognize(image: image) { matches in
|
||||
continuation.resume(returning: matches)
|
||||
}
|
||||
}
|
||||
self.update(matches: matches ?? [:])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func update(matches: [Int : Float]) {
|
||||
self.matches = matches
|
||||
}
|
||||
|
||||
func canClassify(cap id: Int) -> Bool {
|
||||
classifierClasses.contains(id)
|
||||
}
|
||||
@ -1027,68 +1001,13 @@ 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
|
||||
extension Database: ClassifierProgressDelegate {
|
||||
|
||||
nonisolated func classifierProgress(_ progress: ClassifierProgress) {
|
||||
Task {
|
||||
await MainActor.run {
|
||||
self.classifierDownloadProgress = progress
|
||||
}
|
||||
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 {
|
||||
let db = Database()
|
||||
db.serverPath = "https://caps.christophhagen.de"
|
||||
db.caps = [
|
||||
Cap(id: 123, name: "My new cap"),
|
||||
Cap(id: 234, name: "My favorite cap"),
|
||||
Cap(id: 345, name: "My oldest cap"),
|
||||
Cap(id: 456, name: "My new cap"),
|
||||
Cap(id: 567, name: "My favorite cap"),
|
||||
Cap(id: 678, name: "My oldest cap"),
|
||||
].reduce(into: [:]) { $0[$1.id] = $1 }
|
||||
db.image = UIImage(systemSymbol: .photo)
|
||||
return db
|
||||
}
|
||||
|
||||
static var largeMock: Database {
|
||||
let db = Database()
|
||||
db.serverPath = "https://caps.christophhagen.de"
|
||||
db.caps = (1..<500)
|
||||
.map { Cap(id: $0, name: "Cap \($0)") }
|
||||
.reduce(into: [:]) { $0[$1.id] = $1 }
|
||||
db.image = UIImage(systemSymbol: .photo)
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
19
Caps/Download/ClassifierProgress.swift
Normal file
19
Caps/Download/ClassifierProgress.swift
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
struct ClassifierProgress {
|
||||
|
||||
let bytesLoaded: Double
|
||||
|
||||
let 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
|
||||
}
|
||||
}
|
5
Caps/Download/ClassifierProgressDelegate.swift
Normal file
5
Caps/Download/ClassifierProgressDelegate.swift
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
protocol ClassifierProgressDelegate: AnyObject {
|
||||
|
||||
func classifierProgress(_ progress: ClassifierProgress)
|
||||
}
|
20
Caps/Download/ClassifierProgressObserver.swift
Normal file
20
Caps/Download/ClassifierProgressObserver.swift
Normal file
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
final class ClassifierProgressObserver: NSObject {
|
||||
|
||||
weak var delegate: ClassifierProgressDelegate?
|
||||
}
|
||||
|
||||
|
||||
extension ClassifierProgressObserver: URLSessionDownloadDelegate {
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
delegate?.classifierProgress(.init(
|
||||
bytesLoaded: Double(totalBytesWritten),
|
||||
total: Double(totalBytesExpectedToWrite)))
|
||||
}
|
||||
}
|
32
Caps/Preview Content/Database+Mock.swift
Normal file
32
Caps/Preview Content/Database+Mock.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import UIKit
|
||||
|
||||
extension Database {
|
||||
|
||||
static var mock: Database {
|
||||
let caps = [
|
||||
Cap(id: 123, name: "My new cap"),
|
||||
Cap(id: 234, name: "My favorite cap"),
|
||||
Cap(id: 345, name: "My oldest cap"),
|
||||
Cap(id: 456, name: "My new cap"),
|
||||
Cap(id: 567, name: "My favorite cap"),
|
||||
Cap(id: 678, name: "My oldest cap"),
|
||||
].reduce(into: [:]) { $0[$1.id] = $1 }
|
||||
|
||||
let db = Database(caps: caps)
|
||||
db.serverPath = "https://caps.christophhagen.de"
|
||||
|
||||
db.image = UIImage(systemSymbol: .photo)
|
||||
return db
|
||||
}
|
||||
|
||||
static var largeMock: Database {
|
||||
let caps = (1..<500)
|
||||
.map { Cap(id: $0, name: "Cap \($0)") }
|
||||
.reduce(into: [:]) { $0[$1.id] = $1 }
|
||||
|
||||
let db = Database(caps: caps)
|
||||
db.serverPath = "https://caps.christophhagen.de"
|
||||
db.image = UIImage(systemSymbol: .photo)
|
||||
return db
|
||||
}
|
||||
}
|
@ -2,9 +2,8 @@ import SwiftUI
|
||||
|
||||
struct ClassifierDownloadView: View {
|
||||
|
||||
@ObservedObject
|
||||
var progress: Database.ClassifierProgress
|
||||
|
||||
let progress: ClassifierProgress
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ProgressView("Downloading classifier...", value: progress.bytesLoaded, total: Double(progress.total))
|
||||
@ -19,5 +18,8 @@ struct ClassifierDownloadView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ClassifierDownloadView(progress: .init(bytesLoaded: 12_300_000, total: 24_500_000))
|
||||
ClassifierDownloadView(progress: .init(
|
||||
bytesLoaded: 12_300_000,
|
||||
total: 24_500_000)
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user