Fix uploads, add server key entry
This commit is contained in:
parent
093d82893b
commit
2b3ab859fc
@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
E25AAC7C283D855D006E9E7F /* CapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7B283D855D006E9E7F /* CapsApp.swift */; };
|
||||||
E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7D283D855D006E9E7F /* ContentView.swift */; };
|
E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7D283D855D006E9E7F /* ContentView.swift */; };
|
||||||
E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC7F283D855F006E9E7F /* Assets.xcassets */; };
|
E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC7F283D855F006E9E7F /* Assets.xcassets */; };
|
||||||
@ -43,6 +44,7 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
88DBE72D285495B100D1573B /* FancyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTextField.swift; sourceTree = "<group>"; };
|
||||||
E25AAC78283D855D006E9E7F /* Caps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Caps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
E25AAC7B283D855D006E9E7F /* CapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsApp.swift; sourceTree = "<group>"; };
|
||||||
E25AAC7D283D855D006E9E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
E25AAC7D283D855D006E9E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
@ -162,6 +164,7 @@
|
|||||||
E2EA00E0283F658E00F7B269 /* SettingsView.swift */,
|
E2EA00E0283F658E00F7B269 /* SettingsView.swift */,
|
||||||
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */,
|
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */,
|
||||||
E2EA00CD283EBEB600F7B269 /* SearchField.swift */,
|
E2EA00CD283EBEB600F7B269 /* SearchField.swift */,
|
||||||
|
88DBE72D285495B100D1573B /* FancyTextField.swift */,
|
||||||
E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */,
|
E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */,
|
||||||
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */,
|
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */,
|
||||||
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */,
|
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */,
|
||||||
@ -282,6 +285,7 @@
|
|||||||
E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */,
|
E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */,
|
||||||
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */,
|
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */,
|
||||||
E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */,
|
E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */,
|
||||||
|
88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */,
|
||||||
E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */,
|
E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */,
|
||||||
E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */,
|
E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */,
|
||||||
E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */,
|
E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */,
|
||||||
|
BIN
Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
BIN
Caps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
Binary file not shown.
@ -62,9 +62,6 @@ extension FrameManager: AVCapturePhotoCaptureDelegate {
|
|||||||
log("Could not mask image")
|
log("Could not mask image")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
print(image.size)
|
|
||||||
print(masked.size)
|
|
||||||
print(masked.scale)
|
|
||||||
return masked
|
return masked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
#warning("TODO: Create new caps")
|
|
||||||
#warning("TODO: Add colors")
|
#warning("TODO: Add colors")
|
||||||
#warning("TODO: Grid view")
|
#warning("TODO: Grid view")
|
||||||
|
|
||||||
|
@ -241,7 +241,7 @@ struct ContentView: View {
|
|||||||
image: $database.image,
|
image: $database.image,
|
||||||
capId: $capIdOfNextPhoto)
|
capId: $capIdOfNextPhoto)
|
||||||
}
|
}
|
||||||
.bottomSheet(isPresented: $showSettingsSheet, height: 360) {
|
.sheet(isPresented: $showSettingsSheet) {
|
||||||
SettingsView(isPresented: $showSettingsSheet)
|
SettingsView(isPresented: $showSettingsSheet)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showGridView) {
|
.sheet(isPresented: $showGridView) {
|
||||||
@ -266,6 +266,8 @@ struct ContentView: View {
|
|||||||
sortType = .match
|
sortType = .match
|
||||||
sortAscending = false
|
sortAscending = false
|
||||||
}
|
}
|
||||||
|
}.onAppear {
|
||||||
|
database.startRegularUploads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,15 @@ struct Cap {
|
|||||||
self.mainImage = data.mainImage
|
self.mainImage = data.mainImage
|
||||||
self.classifierVersion = data.classifierVersion
|
self.classifierVersion = data.classifierVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var data: CapData {
|
||||||
|
.init(id: id,
|
||||||
|
name: name,
|
||||||
|
count: imageCount,
|
||||||
|
mainImage: mainImage,
|
||||||
|
classifierVersion: classifierVersion,
|
||||||
|
color: color)
|
||||||
|
}
|
||||||
|
|
||||||
mutating func update(with data: CapData) {
|
mutating func update(with data: CapData) {
|
||||||
self.name = data.name
|
self.name = data.name
|
||||||
|
@ -52,10 +52,10 @@ final class Database: ObservableObject {
|
|||||||
let serverUrl: URL
|
let serverUrl: URL
|
||||||
|
|
||||||
@AppStorage("authKey")
|
@AppStorage("authKey")
|
||||||
private var serverAuthenticationKey: String?
|
private var serverAuthenticationKey: String = ""
|
||||||
|
|
||||||
var hasServerAuthentication: Bool {
|
var hasServerAuthentication: Bool {
|
||||||
serverAuthenticationKey != nil
|
serverAuthenticationKey != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
@ -120,7 +120,6 @@ final class Database: ObservableObject {
|
|||||||
diskCapacity: Database.imageCacheStorage,
|
diskCapacity: Database.imageCacheStorage,
|
||||||
directory: cacheDirectory)
|
directory: cacheDirectory)
|
||||||
loadCaps()
|
loadCaps()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
@ -323,7 +322,9 @@ final class Database: ObservableObject {
|
|||||||
func save(newCap name: String) -> Cap {
|
func save(newCap name: String) -> Cap {
|
||||||
let cap = Cap(id: nextCapId, name: name, classifier: serverClassifierVersion)
|
let cap = Cap(id: nextCapId, name: name, classifier: serverClassifierVersion)
|
||||||
caps[cap.id] = cap
|
caps[cap.id] = cap
|
||||||
#warning("Upload new cap")
|
DispatchQueue.main.async {
|
||||||
|
self.changedCaps.insert(cap.id)
|
||||||
|
}
|
||||||
return cap
|
return cap
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,14 +351,9 @@ final class Database: ObservableObject {
|
|||||||
}
|
}
|
||||||
log("Saved \(url.lastPathComponent) for upload")
|
log("Saved \(url.lastPathComponent) for upload")
|
||||||
caps[capId]?.imageCount += 1
|
caps[capId]?.imageCount += 1
|
||||||
updateImageUploadCounts()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateImageUploadCounts() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadImageUploadCounts() -> [Int : Int] {
|
private func loadImageUploadCounts() -> [Int : Int] {
|
||||||
var result = [Int : Int]()
|
var result = [Int : Int]()
|
||||||
pendingImageUploads.forEach { url in
|
pendingImageUploads.forEach { url in
|
||||||
@ -376,9 +372,10 @@ final class Database: ObservableObject {
|
|||||||
// MARK: Uploads
|
// MARK: Uploads
|
||||||
|
|
||||||
func startRegularUploads() {
|
func startRegularUploads() {
|
||||||
guard uploadTimer != nil else {
|
guard uploadTimer == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log("Starting upload timer")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: self.uploadTimerElapsed)
|
self.uploadTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: self.uploadTimerElapsed)
|
||||||
}
|
}
|
||||||
@ -392,13 +389,19 @@ final class Database: ObservableObject {
|
|||||||
|
|
||||||
private func uploadAll() async {
|
private func uploadAll() async {
|
||||||
guard !isUploading else {
|
guard !isUploading else {
|
||||||
|
log("Already uploading")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isUploading = true
|
self.isUploading = true
|
||||||
}
|
}
|
||||||
await uploadAllChangedCaps()
|
log("Starting uploads")
|
||||||
|
let uploaded = await uploadAllChangedCaps()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.changedCaps.subtract(uploaded)
|
||||||
|
}
|
||||||
await uploadAllImages()
|
await uploadAllImages()
|
||||||
|
log("Uploads finished")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isUploading = false
|
self.isUploading = false
|
||||||
}
|
}
|
||||||
@ -428,18 +431,22 @@ final class Database: ObservableObject {
|
|||||||
log("No server authentication to upload to server")
|
log("No server authentication to upload to server")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateImageUploadCounts()
|
|
||||||
for url in pendingImageUploads {
|
for url in pendingImageUploads {
|
||||||
guard let capId = capId(from: url) else {
|
guard let capId = capId(from: url) else {
|
||||||
log("Unexpected image \(url.lastPathComponent) in upload folder")
|
log("Unexpected image \(url.lastPathComponent) in upload folder")
|
||||||
continue
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
guard await upload(imageAt: url, for: capId) else {
|
||||||
|
log("Failed to upload image \(url.lastPathComponent)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log("Uploaded image \(url.lastPathComponent)")
|
||||||
do {
|
do {
|
||||||
try fm.removeItem(at: url)
|
try fm.removeItem(at: url)
|
||||||
updateImageUploadCounts()
|
|
||||||
} catch {
|
} catch {
|
||||||
log("Failed to remove uploaded image \(url.lastPathComponent): \(error)")
|
log("Failed to remove uploaded image \(url.lastPathComponent): \(error)")
|
||||||
}
|
}
|
||||||
@ -448,16 +455,20 @@ 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 key = serverAuthenticationKey else {
|
guard hasServerAuthentication else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard let data = try? Data(contentsOf: url) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let url = serverUrl
|
let url = serverUrl
|
||||||
.appendingPathComponent("images")
|
.appendingPathComponent("images")
|
||||||
.appendingPathComponent("\(cap)?key=\(key)")
|
.appendingPathComponent("\(cap)")
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
|
request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key")
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
do {
|
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 {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
log("Unexpected response for upload of image \(url.lastPathComponent): \(response)")
|
log("Unexpected response for upload of image \(url.lastPathComponent): \(response)")
|
||||||
return false
|
return false
|
||||||
@ -477,43 +488,45 @@ final class Database: ObservableObject {
|
|||||||
changedCaps.count
|
changedCaps.count
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uploadAllChangedCaps() async {
|
private func uploadAllChangedCaps() async -> Set<Int> {
|
||||||
guard hasServerAuthentication else {
|
guard hasServerAuthentication else {
|
||||||
log("No server authentication to upload to server")
|
log("No server authentication to upload to server")
|
||||||
return
|
return .init()
|
||||||
}
|
}
|
||||||
var uploaded = Set<Int>()
|
var uploaded = Set<Int>()
|
||||||
for capId in changedCaps {
|
for capId in changedCaps {
|
||||||
guard let cap = caps[capId] else {
|
guard let cap = caps[capId] else {
|
||||||
|
log("Missing cap \(capId) to upload")
|
||||||
uploaded.insert(capId)
|
uploaded.insert(capId)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard await upload(cap: cap) else {
|
guard await upload(cap: cap) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
log("Uploaded cap \(capId)")
|
||||||
uploaded.insert(capId)
|
uploaded.insert(capId)
|
||||||
}
|
}
|
||||||
changedCaps.subtract(uploaded)
|
return uploaded
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func upload(cap: Cap) async -> Bool {
|
private func upload(cap: Cap) async -> Bool {
|
||||||
guard let key = serverAuthenticationKey else {
|
guard hasServerAuthentication else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let data: Data
|
let data: Data
|
||||||
do {
|
do {
|
||||||
/// `Cap` and `CapData` have equivalent JSON layout
|
/// `Cap` and `CapData` have equivalent JSON layout
|
||||||
data = try encoder.encode(cap)
|
data = try encoder.encode(cap.data)
|
||||||
} catch {
|
} catch {
|
||||||
log("Failed to encode cap \(cap.id) for upload: \(error)")
|
log("Failed to encode cap \(cap.id) for upload: \(error)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let url = serverUrl
|
let url = serverUrl
|
||||||
.appendingPathComponent("images")
|
.appendingPathComponent("cap")
|
||||||
.appendingPathComponent("\(cap)?key=\(key)")
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
|
request.addValue(serverAuthenticationKey, forHTTPHeaderField: "key")
|
||||||
do {
|
do {
|
||||||
let (_, response) = try await URLSession.shared.upload(for: request, from: data)
|
let (_, response) = try await URLSession.shared.upload(for: request, from: data)
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
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)")
|
log("Failed to upload cap \(cap.id): Response \(httpResponse.statusCode)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
changedCaps.remove(cap.id)
|
DispatchQueue.main.async {
|
||||||
|
self.changedCaps.remove(cap.id)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
log("Failed to upload cap \(cap.id): \(error)")
|
log("Failed to upload cap \(cap.id): \(error)")
|
||||||
|
47
Caps/Views/FancyTextField.swift
Normal file
47
Caps/Views/FancyTextField.swift
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -7,28 +7,7 @@ struct SearchField: View {
|
|||||||
var searchString: String
|
var searchString: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TextField("Search", text: $searchString, prompt: Text("Search..."))
|
FancyTextField(text: $searchString, icon: .magnifyingglass, placeholder: "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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,65 +1,68 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var database: Database
|
var database: Database
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var isPresented: Bool
|
var isPresented: Bool
|
||||||
|
|
||||||
|
@AppStorage("authKey")
|
||||||
|
private var serverAuthenticationKey: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
NavigationView {
|
||||||
HStack {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text("Settings")
|
Text("Authentication")
|
||||||
.font(.title2)
|
.font(.footnote)
|
||||||
.bold()
|
.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()
|
Spacer()
|
||||||
Button(action: hide) {
|
|
||||||
Image(systemSymbol: .xmarkCircleFill)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.system(size: 26))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Text("Statistics")
|
.padding(.horizontal)
|
||||||
.font(.footnote)
|
.navigationTitle("Settings")
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hide() {
|
private func hide() {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func byteString(_ count: Int) -> String {
|
private func byteString(_ count: Int) -> String {
|
||||||
ByteCountFormatter.string(fromByteCount: Int64(count), countStyle: .file)
|
ByteCountFormatter.string(fromByteCount: Int64(count), countStyle: .file)
|
||||||
}
|
}
|
||||||
@ -69,6 +72,6 @@ struct SettingsView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsView(isPresented: .constant(true))
|
SettingsView(isPresented: .constant(true))
|
||||||
.environmentObject(Database.mock)
|
.environmentObject(Database.mock)
|
||||||
.previewLayout(.fixed(width: 375, height: 330))
|
//.previewLayout(.fixed(width: 375, height: 410))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user