Allow setting server url in settings

This commit is contained in:
Christoph Hagen 2024-03-01 17:37:10 +01:00
parent 1d992b0bd2
commit 3dc5674a3a
5 changed files with 82 additions and 28 deletions

View File

@ -5,7 +5,7 @@ struct CapsApp: App {
static let thumbnailImageSize: CGFloat = 60 static let thumbnailImageSize: CGFloat = 60
let database = Database(server: URL(string: "https://christophhagen.de/caps")!) let database = Database()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {

View File

@ -33,7 +33,16 @@ final class Database: ObservableObject {
private let encoder = JSONEncoder() private let encoder = JSONEncoder()
private let decoder = JSONDecoder() 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 let folderUrl: URL
@ -159,20 +168,20 @@ final class Database: ObservableObject {
return _classifierClassesCache! return _classifierClassesCache!
} }
init(server: URL, folder: URL = FileManager.default.documentDirectory) { init(folder: URL = FileManager.default.documentDirectory) {
self.serverUrl = server
self.folderUrl = folder self.folderUrl = folder
self.caps = [:] self.caps = [:]
let imageFolder = folder.appendingPathComponent("images") let imageFolder = folder.appendingPathComponent("images")
self.images = try! ImageCache( self.images = try! ImageCache(
folder: imageFolder, folder: imageFolder,
server: server, server: nil,
thumbnailSize: CapsApp.thumbnailImageSize) thumbnailSize: CapsApp.thumbnailImageSize)
self.localClassifierVersion = storedLocalClassifierVersion self.localClassifierVersion = storedLocalClassifierVersion
self.serverClassifierVersion = storedServerClassifierVersion self.serverClassifierVersion = storedServerClassifierVersion
images.server = serverUrl
ensureFolderExistence(gridStorageFolder) ensureFolderExistence(gridStorageFolder)
loadCaps() loadCaps()
updatePendingImageUploadCount(imageUploads: imageUploads) updatePendingImageUploadCount(imageUploads: imageUploads)
@ -200,20 +209,20 @@ final class Database: ObservableObject {
folderUrl.appendingPathComponent("uploads") folderUrl.appendingPathComponent("uploads")
} }
private var serverDbUrl: URL { private var serverDbUrl: URL? {
serverUrl.appendingPathComponent("caps.json") serverUrl?.appendingPathComponent("caps.json")
} }
private var serverClassifierUrl: URL { private var serverClassifierUrl: URL? {
serverUrl.appendingPathComponent("classifier.mlmodel") serverUrl?.appendingPathComponent("classifier.mlmodel")
} }
private var serverClassifierClassesUrl: URL { private var serverClassifierClassesUrl: URL? {
serverUrl.appendingPathComponent("classifier.classes") serverUrl?.appendingPathComponent("classifier.classes")
} }
private var serverClassifierVersionUrl: URL { private var serverClassifierVersionUrl: URL? {
serverUrl.appendingPathComponent("version") serverUrl?.appendingPathComponent("version")
} }
private var gridStorageFolder: URL { private var gridStorageFolder: URL {
@ -224,7 +233,7 @@ final class Database: ObservableObject {
guard let path = caps[cap]?.mainImagePath else { guard let path = caps[cap]?.mainImagePath else {
return nil return nil
} }
return serverUrl.appendingPathComponent(path) return serverUrl?.appendingPathComponent(path)
} }
// MARK: Disk storage // MARK: Disk storage
@ -305,6 +314,10 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
func downloadCaps() async -> Bool { 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)") log("Downloading cap data from \(serverDbUrl)")
let data: Data let data: Data
let response: URLResponse let response: URLResponse
@ -357,6 +370,10 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
func updateServerClassifierVersion() async -> Bool { func updateServerClassifierVersion() async -> Bool {
guard let serverClassifierVersionUrl else {
log("No server url to download classifier version")
return false
}
let data: Data let data: Data
let response: URLResponse let response: URLResponse
do { do {
@ -385,8 +402,12 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
func downloadClassifier() async -> Bool { func downloadClassifier() async -> Bool {
guard let serverClassifierUrl else {
log("No server url to download classifier")
return false
}
log("Downloading classifier") log("Downloading classifier")
let progress = ClassifierProgress() let progress = ClassifierProgress()
DispatchQueue.main.async { DispatchQueue.main.async {
self.classifierDownloadProgress = progress self.classifierDownloadProgress = progress
@ -428,6 +449,10 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
func downloadClassifierClasses() async -> Bool { func downloadClassifierClasses() async -> Bool {
guard let serverClassifierClassesUrl else {
log("No server url to download classifier classes")
return false
}
log("Downloading classifier classes") log("Downloading classifier classes")
let data: Data let data: Data
let response: URLResponse let response: URLResponse
@ -612,6 +637,10 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
private func upload(imageAt url: URL, for cap: Int) async -> Bool { 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 { guard hasServerAuthentication else {
return false return false
} }
@ -619,10 +648,10 @@ final class Database: ObservableObject {
log("No image data found for image \(url.lastPathComponent) (Cap \(cap))") log("No image data found for image \(url.lastPathComponent) (Cap \(cap))")
return false return false
} }
let serverUrl = serverUrl let serverImageUrl = serverUrl
.appendingPathComponent("image") .appendingPathComponent("image")
.appendingPathComponent("\(cap)") .appendingPathComponent("\(cap)")
var request = URLRequest(url: serverUrl) var request = URLRequest(url: serverImageUrl)
request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key") request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key")
request.httpMethod = "POST" request.httpMethod = "POST"
do { do {
@ -677,6 +706,10 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
private func upload(cap: Cap) async -> Bool { private func upload(cap: Cap) async -> Bool {
guard let serverUrl else {
log("No server url to upload cap")
return false
}
guard hasServerAuthentication else { guard hasServerAuthentication else {
return false return false
} }
@ -736,6 +769,10 @@ final class Database: ObservableObject {
} }
func delete(image: CapImage) async -> Bool { func delete(image: CapImage) async -> Bool {
guard let serverUrl else {
log("No server url to delete image")
return false
}
guard hasServerAuthentication else { guard hasServerAuthentication else {
log("No authorization to delete cap image") log("No authorization to delete cap image")
return false return false
@ -784,6 +821,10 @@ final class Database: ObservableObject {
} }
func delete(cap: Int) async -> Bool { func delete(cap: Int) async -> Bool {
guard let serverUrl else {
log("No server url to delete cap")
return false
}
guard hasServerAuthentication else { guard hasServerAuthentication else {
log("No authorization to delete cap") log("No authorization to delete cap")
return false return false
@ -1027,7 +1068,8 @@ extension Database.ClassifierProgress: URLSessionDownloadDelegate {
extension Database { extension Database {
static var mock: 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 = [ db.caps = [
Cap(id: 123, name: "My new cap"), Cap(id: 123, name: "My new cap"),
Cap(id: 234, name: "My favorite cap"), Cap(id: 234, name: "My favorite cap"),
@ -1041,7 +1083,8 @@ extension Database {
} }
static var largeMock: 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) db.caps = (1..<500)
.map { Cap(id: $0, name: "Cap \($0)") } .map { Cap(id: $0, name: "Cap \($0)") }
.reduce(into: [:]) { $0[$1.id] = $1 } .reduce(into: [:]) { $0[$1.id] = $1 }

View File

@ -5,7 +5,7 @@ final class ImageCache: ObservableObject {
let folder: URL let folder: URL
let server: URL var server: URL?
let thumbnailSize: CGFloat let thumbnailSize: CGFloat
@ -22,7 +22,7 @@ final class ImageCache: ObservableObject {
@Published @Published
private(set) var imageCount = 0 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.folder = folder
self.server = server self.server = server
self.thumbnailSize = thumbnailSize * UIScreen.main.scale 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)) folder.appendingPathComponent(String(format: "%04d-%02d.jpg", image.cap, image.version))
} }
private func remoteImageUrl(_ image: CapImage) -> URL { private func remoteImageUrl(_ image: CapImage) -> URL? {
server.appendingPathComponent(String(format: "images/%04d/%04d-%02d.jpg", image.cap, image.cap, image.version)) server?.appendingPathComponent(String(format: "images/%04d/%04d-%02d.jpg", image.cap, image.cap, image.version))
} }
@discardableResult @discardableResult
@ -134,7 +134,9 @@ final class ImageCache: ObservableObject {
} }
private func loadRemoteImage(_ image: CapImage) async -> URL? { private func loadRemoteImage(_ image: CapImage) async -> URL? {
let remoteURL = remoteImageUrl(image) guard let remoteURL = remoteImageUrl(image) else {
return nil
}
return await loadRemoteImage(at: remoteURL) return await loadRemoteImage(at: remoteURL)
} }

View File

@ -14,10 +14,6 @@ struct CapRowView: View {
@EnvironmentObject @EnvironmentObject
var database: Database var database: Database
var imageUrl: URL {
database.serverUrl.appendingPathComponent(cap.mainImagePath)
}
var body: some View { var body: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {

View File

@ -18,6 +18,18 @@ struct SettingsView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack(alignment: .leading, spacing: 3) { 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") Text("Authentication")
.font(.footnote) .font(.footnote)
.textCase(.uppercase) .textCase(.uppercase)
@ -25,6 +37,7 @@ struct SettingsView: View {
.padding(.top) .padding(.top)
Group { Group {
FancyTextField(text: $serverAuthenticationKey, icon: .key, placeholder: "Server key") FancyTextField(text: $serverAuthenticationKey, icon: .key, placeholder: "Server key")
.keyboardType(.asciiCapable)
}.padding(.horizontal) }.padding(.horizontal)
Text("Statistics") Text("Statistics")
.font(.footnote) .font(.footnote)