Allow setting server url in settings
This commit is contained in:
parent
1d992b0bd2
commit
3dc5674a3a
@ -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 {
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user