From 2b3ab859fc688607112eaf475a596d56ebf7a2e3 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sat, 11 Jun 2022 11:27:56 +0200 Subject: [PATCH] Fix uploads, add server key entry --- Caps.xcodeproj/project.pbxproj | 4 + .../UserInterfaceState.xcuserstate | Bin 0 -> 33628 bytes Caps/Camera/FrameManager.swift | 3 - Caps/CapsApp.swift | 1 - Caps/ContentView.swift | 4 +- Caps/Data/Cap.swift | 9 ++ Caps/Data/Database.swift | 65 +++++++----- Caps/Views/FancyTextField.swift | 47 +++++++++ Caps/Views/SearchField.swift | 23 +--- Caps/Views/SettingsView.swift | 99 +++++++++--------- 10 files changed, 155 insertions(+), 100 deletions(-) create mode 100644 Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Caps/Views/FancyTextField.swift diff --git a/Caps.xcodeproj/project.pbxproj b/Caps.xcodeproj/project.pbxproj index 1f8e577..8b463db 100644 --- a/Caps.xcodeproj/project.pbxproj +++ b/Caps.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; }; E25AAC7C283D855D006E9E7F /* CapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7B283D855D006E9E7F /* CapsApp.swift */; }; E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7D283D855D006E9E7F /* ContentView.swift */; }; E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC7F283D855F006E9E7F /* Assets.xcassets */; }; @@ -43,6 +44,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 88DBE72D285495B100D1573B /* FancyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTextField.swift; sourceTree = ""; }; E25AAC78283D855D006E9E7F /* Caps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Caps.app; sourceTree = BUILT_PRODUCTS_DIR; }; E25AAC7B283D855D006E9E7F /* CapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsApp.swift; sourceTree = ""; }; E25AAC7D283D855D006E9E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -162,6 +164,7 @@ E2EA00E0283F658E00F7B269 /* SettingsView.swift */, E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */, E2EA00CD283EBEB600F7B269 /* SearchField.swift */, + 88DBE72D285495B100D1573B /* FancyTextField.swift */, E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */, E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */, E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */, @@ -282,6 +285,7 @@ E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */, E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */, E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */, + 88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */, E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */, E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */, E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */, diff --git a/Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate b/Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..a4f690827dd89013a3cbca5b9fed396975a081de GIT binary patch literal 33628 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 l9eEuE9fchw9V Cap { let cap = Cap(id: nextCapId, name: name, classifier: serverClassifierVersion) caps[cap.id] = cap - #warning("Upload new cap") + DispatchQueue.main.async { + self.changedCaps.insert(cap.id) + } return cap } @@ -350,14 +351,9 @@ final class Database: ObservableObject { } log("Saved \(url.lastPathComponent) for upload") caps[capId]?.imageCount += 1 - updateImageUploadCounts() return true } - private func updateImageUploadCounts() { - - } - private func loadImageUploadCounts() -> [Int : Int] { var result = [Int : Int]() pendingImageUploads.forEach { url in @@ -376,9 +372,10 @@ final class Database: ObservableObject { // MARK: Uploads func startRegularUploads() { - guard uploadTimer != nil else { + guard uploadTimer == nil else { return } + log("Starting upload timer") DispatchQueue.main.async { self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: self.uploadTimerElapsed) } @@ -392,13 +389,19 @@ final class Database: ObservableObject { private func uploadAll() async { guard !isUploading else { + log("Already uploading") return } DispatchQueue.main.async { self.isUploading = true } - await uploadAllChangedCaps() + log("Starting uploads") + let uploaded = await uploadAllChangedCaps() + DispatchQueue.main.async { + self.changedCaps.subtract(uploaded) + } await uploadAllImages() + log("Uploads finished") DispatchQueue.main.async { self.isUploading = false } @@ -428,18 +431,22 @@ final class Database: ObservableObject { log("No server authentication to upload to server") return } - updateImageUploadCounts() for url in pendingImageUploads { guard let capId = capId(from: url) else { log("Unexpected image \(url.lastPathComponent) in upload folder") continue } - guard await upload(imageAt: url, for: capId) else { + guard fm.fileExists(atPath: url.path) else { + log("Missing image \(url.lastPathComponent) in upload folder") continue } + guard await upload(imageAt: url, for: capId) else { + log("Failed to upload image \(url.lastPathComponent)") + continue + } + log("Uploaded image \(url.lastPathComponent)") do { try fm.removeItem(at: url) - updateImageUploadCounts() } catch { log("Failed to remove uploaded image \(url.lastPathComponent): \(error)") } @@ -448,16 +455,20 @@ final class Database: ObservableObject { @discardableResult private func upload(imageAt url: URL, for cap: Int) async -> Bool { - guard let key = serverAuthenticationKey else { + guard hasServerAuthentication else { + return false + } + guard let data = try? Data(contentsOf: url) else { return false } let url = serverUrl .appendingPathComponent("images") - .appendingPathComponent("\(cap)?key=\(key)") + .appendingPathComponent("\(cap)") var request = URLRequest(url: url) + request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key") request.httpMethod = "POST" do { - let (_, response) = try await URLSession.shared.upload(for: request, fromFile: url) + let (_, response) = try await URLSession.shared.upload(for: request, from: data) guard let httpResponse = response as? HTTPURLResponse else { log("Unexpected response for upload of image \(url.lastPathComponent): \(response)") return false @@ -477,43 +488,45 @@ final class Database: ObservableObject { changedCaps.count } - private func uploadAllChangedCaps() async { + private func uploadAllChangedCaps() async -> Set { guard hasServerAuthentication else { log("No server authentication to upload to server") - return + return .init() } var uploaded = Set() for capId in changedCaps { guard let cap = caps[capId] else { + log("Missing cap \(capId) to upload") uploaded.insert(capId) continue } guard await upload(cap: cap) else { continue } + log("Uploaded cap \(capId)") uploaded.insert(capId) } - changedCaps.subtract(uploaded) + return uploaded } @discardableResult private func upload(cap: Cap) async -> Bool { - guard let key = serverAuthenticationKey else { + guard hasServerAuthentication else { return false } let data: Data do { /// `Cap` and `CapData` have equivalent JSON layout - data = try encoder.encode(cap) + data = try encoder.encode(cap.data) } catch { log("Failed to encode cap \(cap.id) for upload: \(error)") return false } let url = serverUrl - .appendingPathComponent("images") - .appendingPathComponent("\(cap)?key=\(key)") + .appendingPathComponent("cap") var request = URLRequest(url: url) request.httpMethod = "POST" + request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key") do { let (_, response) = try await URLSession.shared.upload(for: request, from: data) guard let httpResponse = response as? HTTPURLResponse else { @@ -524,7 +537,9 @@ final class Database: ObservableObject { log("Failed to upload cap \(cap.id): Response \(httpResponse.statusCode)") return false } - changedCaps.remove(cap.id) + DispatchQueue.main.async { + self.changedCaps.remove(cap.id) + } return true } catch { log("Failed to upload cap \(cap.id): \(error)") diff --git a/Caps/Views/FancyTextField.swift b/Caps/Views/FancyTextField.swift new file mode 100644 index 0000000..45d9a3c --- /dev/null +++ b/Caps/Views/FancyTextField.swift @@ -0,0 +1,47 @@ +import SwiftUI +import SFSafeSymbols + +struct FancyTextField: View { + + @Binding + var text: String + + let icon: SFSymbol + + let placeholder: String + + let showClearButton: Bool = true + + var body: some View { + TextField("Search", text: $text, prompt: Text(placeholder)) + .padding(7) + .padding(.horizontal, 25) + .background(Color(.systemGray5)) + .cornerRadius(8) + .overlay( + HStack { + Image(systemSymbol: icon) + .foregroundColor(.gray) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .padding(.leading, 8) + if showClearButton && text != "" { + Button(action: { + self.text = "" + }) { + Image(systemSymbol: .multiplyCircleFill) + .foregroundColor(.gray) + .padding(.trailing, 8) + } + } + } + ) + } +} + +struct FancyTextField_Previews: PreviewProvider { + static var previews: some View { + FancyTextField(text: .constant("Text"), + icon: .magnifyingglass, + placeholder: "Enter text") + } +} diff --git a/Caps/Views/SearchField.swift b/Caps/Views/SearchField.swift index 03e716c..7bf78a9 100644 --- a/Caps/Views/SearchField.swift +++ b/Caps/Views/SearchField.swift @@ -7,28 +7,7 @@ struct SearchField: View { var searchString: String var body: some View { - TextField("Search", text: $searchString, prompt: Text("Search...")) - .padding(7) - .padding(.horizontal, 25) - .background(Color(.systemGray5)) - .cornerRadius(8) - .overlay( - HStack { - Image(systemSymbol: .magnifyingglass) - .foregroundColor(.gray) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - .padding(.leading, 8) - if searchString != "" { - Button(action: { - self.searchString = "" - }) { - Image(systemSymbol: .multiplyCircleFill) - .foregroundColor(.gray) - .padding(.trailing, 8) - } - } - } - ) + FancyTextField(text: $searchString, icon: .magnifyingglass, placeholder: "Search...") } } diff --git a/Caps/Views/SettingsView.swift b/Caps/Views/SettingsView.swift index f0aec12..b7b4df8 100644 --- a/Caps/Views/SettingsView.swift +++ b/Caps/Views/SettingsView.swift @@ -1,65 +1,68 @@ import SwiftUI +import SFSafeSymbols struct SettingsView: View { - + @EnvironmentObject var database: Database - + @Binding var isPresented: Bool - + + @AppStorage("authKey") + private var serverAuthenticationKey: String = "" + var body: some View { - VStack(alignment: .leading, spacing: 3) { - HStack { - Text("Settings") - .font(.title2) - .bold() + NavigationView { + VStack(alignment: .leading, spacing: 3) { + Text("Authentication") + .font(.footnote) + .textCase(.uppercase) + .foregroundColor(.secondary) + .padding(.top) + Group { + FancyTextField(text: $serverAuthenticationKey, icon: .key, placeholder: "Server key") + }.padding(.horizontal) + Text("Statistics") + .font(.footnote) + .textCase(.uppercase) + .foregroundColor(.secondary) + .padding(.top) + Group { + SettingsStatisticRow(label: "Caps", value: "\(database.numberOfCaps)") + SettingsStatisticRow(label: "Total images", value: "\(database.numberOfImages)") + SettingsStatisticRow(label: "Images per cap", value: String(format: "%.1f", database.averageImageCount)) + }.padding(.horizontal) + Text("Classifier") + .font(.footnote) + .textCase(.uppercase) + .foregroundColor(.secondary) + .padding(.top) + Group { + SettingsStatisticRow(label: "Version", value: "\(database.classifierVersion)") + SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)") + }.padding(.horizontal) + Text("Storage") + .font(.footnote) + .textCase(.uppercase) + .foregroundColor(.secondary) + .padding(.top) + Group { + SettingsStatisticRow(label: "Image cache", value: byteString(database.imageCacheSize)) + SettingsStatisticRow(label: "Database", value: byteString(database.databaseSize)) + SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize)) + }.padding(.horizontal) Spacer() - Button(action: hide) { - Image(systemSymbol: .xmarkCircleFill) - .foregroundColor(.gray) - .font(.system(size: 26)) - } } - Text("Statistics") - .font(.footnote) - .textCase(.uppercase) - .foregroundColor(.secondary) - .padding(.top) - Group { - SettingsStatisticRow(label: "Caps", value: "\(database.numberOfCaps)") - SettingsStatisticRow(label: "Total images", value: "\(database.numberOfImages)") - SettingsStatisticRow(label: "Images per cap", value: String(format: "%.1f", database.averageImageCount)) - }.padding(.horizontal) - Text("Classifier") - .font(.footnote) - .textCase(.uppercase) - .foregroundColor(.secondary) - .padding(.top) - Group { - SettingsStatisticRow(label: "Version", value: "\(database.classifierVersion)") - SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)") - }.padding(.horizontal) - Text("Storage") - .font(.footnote) - .textCase(.uppercase) - .foregroundColor(.secondary) - .padding(.top) - Group { - SettingsStatisticRow(label: "Image cache", value: byteString(database.imageCacheSize)) - SettingsStatisticRow(label: "Database", value: byteString(database.databaseSize)) - SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize)) - }.padding(.horizontal) - - Spacer() + .padding(.horizontal) + .navigationTitle("Settings") } - .padding(.horizontal) } - + private func hide() { isPresented = false } - + private func byteString(_ count: Int) -> String { ByteCountFormatter.string(fromByteCount: Int64(count), countStyle: .file) } @@ -69,6 +72,6 @@ struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView(isPresented: .constant(true)) .environmentObject(Database.mock) - .previewLayout(.fixed(width: 375, height: 330)) + //.previewLayout(.fixed(width: 375, height: 410)) } }