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
|
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 {
|
||||||
|
@ -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 }
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user