From 3dc5674a3a13fc3f487edef4df54d84acbfe3e4d Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Fri, 1 Mar 2024 17:37:10 +0100 Subject: [PATCH] Allow setting server url in settings --- Caps/CapsApp.swift | 2 +- Caps/Data/Database.swift | 79 +++++++++++++++++++++++++++-------- Caps/Data/ImageCache.swift | 12 +++--- Caps/Views/CapRowView.swift | 4 -- Caps/Views/SettingsView.swift | 13 ++++++ 5 files changed, 82 insertions(+), 28 deletions(-) diff --git a/Caps/CapsApp.swift b/Caps/CapsApp.swift index bc5a5c0..0a8e27b 100644 --- a/Caps/CapsApp.swift +++ b/Caps/CapsApp.swift @@ -5,7 +5,7 @@ struct CapsApp: App { static let thumbnailImageSize: CGFloat = 60 - let database = Database(server: URL(string: "https://christophhagen.de/caps")!) + let database = Database() var body: some Scene { WindowGroup { diff --git a/Caps/Data/Database.swift b/Caps/Data/Database.swift index 3ef6acb..ff05138 100644 --- a/Caps/Data/Database.swift +++ b/Caps/Data/Database.swift @@ -33,7 +33,16 @@ final class Database: ObservableObject { private let encoder = JSONEncoder() private let decoder = JSONDecoder() - let serverUrl: URL + @AppStorage("serverUrl") + var serverPath: String = "" { + didSet { + images.server = serverUrl + } + } + + var serverUrl: URL? { + .init(string: serverPath) + } let folderUrl: URL @@ -159,20 +168,20 @@ final class Database: ObservableObject { return _classifierClassesCache! } - init(server: URL, folder: URL = FileManager.default.documentDirectory) { - self.serverUrl = server + init(folder: URL = FileManager.default.documentDirectory) { self.folderUrl = folder self.caps = [:] let imageFolder = folder.appendingPathComponent("images") self.images = try! ImageCache( folder: imageFolder, - server: server, + server: nil, thumbnailSize: CapsApp.thumbnailImageSize) self.localClassifierVersion = storedLocalClassifierVersion self.serverClassifierVersion = storedServerClassifierVersion + images.server = serverUrl ensureFolderExistence(gridStorageFolder) loadCaps() updatePendingImageUploadCount(imageUploads: imageUploads) @@ -200,20 +209,20 @@ final class Database: ObservableObject { folderUrl.appendingPathComponent("uploads") } - private var serverDbUrl: URL { - serverUrl.appendingPathComponent("caps.json") + private var serverDbUrl: URL? { + serverUrl?.appendingPathComponent("caps.json") } - private var serverClassifierUrl: URL { - serverUrl.appendingPathComponent("classifier.mlmodel") + private var serverClassifierUrl: URL? { + serverUrl?.appendingPathComponent("classifier.mlmodel") } - private var serverClassifierClassesUrl: URL { - serverUrl.appendingPathComponent("classifier.classes") + private var serverClassifierClassesUrl: URL? { + serverUrl?.appendingPathComponent("classifier.classes") } - private var serverClassifierVersionUrl: URL { - serverUrl.appendingPathComponent("version") + private var serverClassifierVersionUrl: URL? { + serverUrl?.appendingPathComponent("version") } private var gridStorageFolder: URL { @@ -224,7 +233,7 @@ final class Database: ObservableObject { guard let path = caps[cap]?.mainImagePath else { return nil } - return serverUrl.appendingPathComponent(path) + return serverUrl?.appendingPathComponent(path) } // MARK: Disk storage @@ -305,6 +314,10 @@ final class Database: ObservableObject { @discardableResult func downloadCaps() async -> Bool { + guard let serverDbUrl else { + log("No server url set to download cap data") + return false + } log("Downloading cap data from \(serverDbUrl)") let data: Data let response: URLResponse @@ -357,6 +370,10 @@ final class Database: ObservableObject { @discardableResult func updateServerClassifierVersion() async -> Bool { + guard let serverClassifierVersionUrl else { + log("No server url to download classifier version") + return false + } let data: Data let response: URLResponse do { @@ -385,8 +402,12 @@ final class Database: ObservableObject { @discardableResult func downloadClassifier() async -> Bool { + guard let serverClassifierUrl else { + log("No server url to download classifier") + return false + } log("Downloading classifier") - + let progress = ClassifierProgress() DispatchQueue.main.async { self.classifierDownloadProgress = progress @@ -428,6 +449,10 @@ final class Database: ObservableObject { @discardableResult func downloadClassifierClasses() async -> Bool { + guard let serverClassifierClassesUrl else { + log("No server url to download classifier classes") + return false + } log("Downloading classifier classes") let data: Data let response: URLResponse @@ -612,6 +637,10 @@ final class Database: ObservableObject { @discardableResult private func upload(imageAt url: URL, for cap: Int) async -> Bool { + guard let serverUrl else { + log("No server url to upload image") + return false + } guard hasServerAuthentication else { return false } @@ -619,10 +648,10 @@ final class Database: ObservableObject { log("No image data found for image \(url.lastPathComponent) (Cap \(cap))") return false } - let serverUrl = serverUrl + let serverImageUrl = serverUrl .appendingPathComponent("image") .appendingPathComponent("\(cap)") - var request = URLRequest(url: serverUrl) + var request = URLRequest(url: serverImageUrl) request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key") request.httpMethod = "POST" do { @@ -677,6 +706,10 @@ final class Database: ObservableObject { @discardableResult private func upload(cap: Cap) async -> Bool { + guard let serverUrl else { + log("No server url to upload cap") + return false + } guard hasServerAuthentication else { return false } @@ -736,6 +769,10 @@ final class Database: ObservableObject { } func delete(image: CapImage) async -> Bool { + guard let serverUrl else { + log("No server url to delete image") + return false + } guard hasServerAuthentication else { log("No authorization to delete cap image") return false @@ -784,6 +821,10 @@ final class Database: ObservableObject { } func delete(cap: Int) async -> Bool { + guard let serverUrl else { + log("No server url to delete cap") + return false + } guard hasServerAuthentication else { log("No authorization to delete cap") return false @@ -1027,7 +1068,8 @@ extension Database.ClassifierProgress: URLSessionDownloadDelegate { extension Database { static var mock: Database { - let db = Database(server: URL(string: "https://christophhagen.de/caps")!) + 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"), @@ -1041,7 +1083,8 @@ extension Database { } static var largeMock: Database { - let db = Database(server: URL(string: "https://christophhagen.de/caps")!) + 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 } diff --git a/Caps/Data/ImageCache.swift b/Caps/Data/ImageCache.swift index 7a8f833..d1e22e1 100644 --- a/Caps/Data/ImageCache.swift +++ b/Caps/Data/ImageCache.swift @@ -5,7 +5,7 @@ final class ImageCache: ObservableObject { let folder: URL - let server: URL + var server: URL? let thumbnailSize: CGFloat @@ -22,7 +22,7 @@ final class ImageCache: ObservableObject { @Published private(set) var imageCount = 0 - init(folder: URL, server: URL, thumbnailSize: CGFloat) throws { + init(folder: URL, server: URL?, thumbnailSize: CGFloat) throws { self.folder = folder self.server = server self.thumbnailSize = thumbnailSize * UIScreen.main.scale @@ -37,8 +37,8 @@ final class ImageCache: ObservableObject { folder.appendingPathComponent(String(format: "%04d-%02d.jpg", image.cap, image.version)) } - private func remoteImageUrl(_ image: CapImage) -> URL { - server.appendingPathComponent(String(format: "images/%04d/%04d-%02d.jpg", image.cap, image.cap, image.version)) + private func remoteImageUrl(_ image: CapImage) -> URL? { + server?.appendingPathComponent(String(format: "images/%04d/%04d-%02d.jpg", image.cap, image.cap, image.version)) } @discardableResult @@ -134,7 +134,9 @@ final class ImageCache: ObservableObject { } private func loadRemoteImage(_ image: CapImage) async -> URL? { - let remoteURL = remoteImageUrl(image) + guard let remoteURL = remoteImageUrl(image) else { + return nil + } return await loadRemoteImage(at: remoteURL) } diff --git a/Caps/Views/CapRowView.swift b/Caps/Views/CapRowView.swift index 6079432..de0d7f9 100644 --- a/Caps/Views/CapRowView.swift +++ b/Caps/Views/CapRowView.swift @@ -14,10 +14,6 @@ struct CapRowView: View { @EnvironmentObject var database: Database - var imageUrl: URL { - database.serverUrl.appendingPathComponent(cap.mainImagePath) - } - var body: some View { HStack(alignment: .center) { VStack(alignment: .leading, spacing: 0) { diff --git a/Caps/Views/SettingsView.swift b/Caps/Views/SettingsView.swift index 72652ba..0595f7f 100644 --- a/Caps/Views/SettingsView.swift +++ b/Caps/Views/SettingsView.swift @@ -18,6 +18,18 @@ struct SettingsView: View { var body: some View { NavigationView { VStack(alignment: .leading, spacing: 3) { + Text("Server") + .font(.footnote) + .textCase(.uppercase) + .foregroundColor(.secondary) + .padding(.top) + Group { + FancyTextField( + text: $database.serverPath, + icon: .globe, + placeholder: "Server url") + .keyboardType(.URL) + }.padding(.horizontal) Text("Authentication") .font(.footnote) .textCase(.uppercase) @@ -25,6 +37,7 @@ struct SettingsView: View { .padding(.top) Group { FancyTextField(text: $serverAuthenticationKey, icon: .key, placeholder: "Server key") + .keyboardType(.asciiCapable) }.padding(.horizontal) Text("Statistics") .font(.footnote)