diff --git a/Caps/ContentView.swift b/Caps/ContentView.swift index 2de00e4..6cd759f 100644 --- a/Caps/ContentView.swift +++ b/Caps/ContentView.swift @@ -357,6 +357,7 @@ struct ContentView: View { private func downloadClassifier() { Task { await database.downloadClassifier() + await database.downloadClassifierClasses() } } diff --git a/Caps/Data/Cap.swift b/Caps/Data/Cap.swift index 32c88fc..cdc514a 100644 --- a/Caps/Data/Cap.swift +++ b/Caps/Data/Cap.swift @@ -17,9 +17,6 @@ struct Cap { /// The index of the main image for the cap var mainImage: Int - /// The version of the first classifier capable of recognizing the cap - var classifierVersion: Int? - var color: Color? /// The subpath to the main image on the server @@ -44,13 +41,12 @@ struct Cap { - Parameter id: The unique id of the cap - Parameter name: The name associated with the cap */ - init(id: Int, name: String, classifier: Int? = nil) { + init(id: Int, name: String) { self.id = id self.name = name self.cleanName = name.clean self.imageCount = 0 self.mainImage = 0 - self.classifierVersion = classifier } init(data: CapData) { @@ -59,7 +55,6 @@ struct Cap { self.cleanName = data.name.clean self.imageCount = data.count self.mainImage = data.mainImage - self.classifierVersion = data.classifierVersion } var data: CapData { @@ -67,7 +62,6 @@ struct Cap { name: name, count: imageCount, mainImage: mainImage, - classifierVersion: classifierVersion, color: color) } @@ -76,30 +70,18 @@ struct Cap { self.cleanName = data.name.clean self.imageCount = data.count self.mainImage = data.mainImage - self.classifierVersion = data.classifierVersion } static func ==(lhs: Cap, rhs: CapData) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name && lhs.imageCount == rhs.count && - lhs.mainImage == rhs.mainImage && - lhs.classifierVersion == rhs.classifierVersion + lhs.mainImage == rhs.mainImage } static func !=(lhs: Cap, rhs: CapData) -> Bool { !(lhs == rhs) } - - func classifiable(by classifierVersion: Int?) -> Bool { - guard let version = classifierVersion else { - return false - } - guard let own = self.classifierVersion else { - return false - } - return version >= own - } } extension Cap { @@ -130,7 +112,6 @@ extension Cap: Codable { case cleanName = "c" case imageCount = "i" case mainImage = "m" - case classifierVersion = "v" case color = "f" } } diff --git a/Caps/Data/CapData.swift b/Caps/Data/CapData.swift index 7413694..69c33a8 100644 --- a/Caps/Data/CapData.swift +++ b/Caps/Data/CapData.swift @@ -10,8 +10,6 @@ struct CapData: Codable { var mainImage: Int - var classifierVersion: Int? - var color: Cap.Color? enum CodingKeys: String, CodingKey { @@ -19,7 +17,6 @@ struct CapData: Codable { case name = "n" case count = "c" case mainImage = "m" - case classifierVersion = "v" case color = "f" } } diff --git a/Caps/Data/Database.swift b/Caps/Data/Database.swift index 560b718..3a8e1b7 100644 --- a/Caps/Data/Database.swift +++ b/Caps/Data/Database.swift @@ -106,6 +106,35 @@ final class Database: ObservableObject { @Published var isUploading = false + + @AppStorage("classifierClasses") + private var _classifierClassesString: String = "" + + private var _classifierClassesCache: Set? + + private var classifierClasses: Set { + get { + _classifierClassesCache ?? loadClassifierClasses() + } + set { + _classifierClassesCache = newValue + DispatchQueue.main.async { + self._classifierClassesString = newValue.map { "\($0)" }.joined(separator: ",") + } + } + } + + private func loadClassifierClasses() -> Set { + let elements: [Int] = _classifierClassesString.components(separatedBy: ",").compactMap { + guard let id = Int($0) else { + log("Failed to load classifier class from '\($0)'") + return nil + } + return id + } + _classifierClassesCache = Set(elements) + return _classifierClassesCache! + } init(server: URL, folder: URL = FileManager.default.documentDirectory) { self.serverUrl = server @@ -151,6 +180,10 @@ final class Database: ObservableObject { private var serverClassifierUrl: URL { serverUrl.appendingPathComponent("classifier.mlmodel") } + + private var serverClassifierClassesUrl: URL { + serverUrl.appendingPathComponent("classifier.classes") + } private var serverClassifierVersionUrl: URL { serverUrl.appendingPathComponent("version") @@ -359,6 +392,48 @@ final class Database: ObservableObject { log("Downloaded classifier \(classifierVersion)") return true } + + @discardableResult + func downloadClassifierClasses() async -> Bool { + log("Downloading classifier classes") + let data: Data + let response: URLResponse + do { + (data, response) = try await URLSession.shared.data(from: serverClassifierClassesUrl) + } catch { + log("Failed to download classifier classes: \(error)") + return false + } + guard (response as? HTTPURLResponse)?.statusCode == 200 else { + return false + } + + guard let string = String(data: data, encoding: .utf8) else { + log("Classifier classes is invalid data (not a string)") + return false + } + let classes = string.components(separatedBy: ",") + + // Validate input + var isValid = true + let ids: [Int] = classes.compactMap { s in + guard let id = Int(s) else { + log("Invalid id '\(s)' in downloaded classes list") + isValid = false + return nil + } + if caps[id] == nil { + // Caps which are deleted may still be recognized + return nil + } + return id + } + guard isValid else { + return false + } + self.classifierClasses = Set(ids) + return true + } /** Indicate that the cap has pending operations, such as determining the color or a thumbnail @@ -374,7 +449,7 @@ final class Database: ObservableObject { // MARK: Adding new data func save(newCap name: String) -> Cap { - let cap = Cap(id: nextCapId, name: name, classifier: nil) + let cap = Cap(id: nextCapId, name: name) caps[cap.id] = cap DispatchQueue.main.async { self.changedCaps.insert(cap.id) @@ -745,6 +820,10 @@ final class Database: ObservableObject { } } } + + func canClassify(cap id: Int) -> Bool { + classifierClasses.contains(id) + } private func getClassifier() -> Classifier? { if let classifier = classifier { @@ -847,8 +926,7 @@ final class Database: ObservableObject { } var classifierClassCount: Int { - let version = classifierVersion - return caps.values.filter { $0.classifiable(by: version) }.count + classifierClasses.count } func imageCacheSize() async -> Int { @@ -883,7 +961,7 @@ extension Database { static var largeMock: Database { let db = Database(server: URL(string: "https://christophhagen.de/caps")!) db.caps = (1..<500) - .map { Cap(id: $0, name: "Cap \($0)", classifier: nil)} + .map { Cap(id: $0, name: "Cap \($0)") } .reduce(into: [:]) { $0[$1.id] = $1 } db.image = UIImage(systemSymbol: .photo) return db