6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
290 lines
8.2 KiB
Swift
290 lines
8.2 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Model
|
|
|
|
struct AppSettings: Equatable {
|
|
var systemName: String
|
|
var timezone: String
|
|
var jwtExpiryHours: Int
|
|
var twoFactorEnabled: Bool
|
|
var serverURL: String
|
|
|
|
static let `default` = AppSettings(
|
|
systemName: "AAMOS",
|
|
timezone: "Europe/Stockholm",
|
|
jwtExpiryHours: 24,
|
|
twoFactorEnabled: false,
|
|
serverURL: "https://api.aamos.io"
|
|
)
|
|
}
|
|
|
|
// MARK: - ViewModel
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class SettingsViewModel {
|
|
var settings: AppSettings
|
|
var savedSettings: AppSettings
|
|
var isSaving = false
|
|
var showLogoutConfirmation = false
|
|
var saveError: String?
|
|
|
|
var hasChanges: Bool { settings != savedSettings }
|
|
|
|
init(initial: AppSettings = .default) {
|
|
self.settings = initial
|
|
self.savedSettings = initial
|
|
}
|
|
|
|
func save() async {
|
|
isSaving = true
|
|
defer { isSaving = false }
|
|
do {
|
|
try await Task.sleep(for: .milliseconds(400))
|
|
savedSettings = settings
|
|
} catch {
|
|
saveError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func discardChanges() {
|
|
settings = savedSettings
|
|
}
|
|
|
|
func logout() {
|
|
// Clear credentials and navigate to auth screen
|
|
}
|
|
}
|
|
|
|
// MARK: - View
|
|
|
|
struct SettingsView: View {
|
|
@State private var vm = SettingsViewModel()
|
|
|
|
private let timezones: [String] = {
|
|
var zones = TimeZone.knownTimeZoneIdentifiers.sorted()
|
|
if let idx = zones.firstIndex(of: "Europe/Stockholm") {
|
|
zones.move(fromOffsets: IndexSet(integer: idx), toOffset: 0)
|
|
}
|
|
return zones
|
|
}()
|
|
|
|
private var appVersion: String {
|
|
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "—"
|
|
}
|
|
|
|
private var buildNumber: String {
|
|
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "—"
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
generalSection
|
|
securitySection
|
|
aboutSection
|
|
logoutSection
|
|
}
|
|
.navigationTitle("Inställningar")
|
|
.navigationBarTitleDisplayMode(.large)
|
|
.toolbar { toolbarContent }
|
|
.disabled(vm.isSaving)
|
|
.overlay {
|
|
if vm.isSaving {
|
|
savingOverlay
|
|
}
|
|
}
|
|
.confirmationDialog(
|
|
"Logga ut",
|
|
isPresented: $vm.showLogoutConfirmation,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("Logga ut", role: .destructive) { vm.logout() }
|
|
Button("Avbryt", role: .cancel) {}
|
|
} message: {
|
|
Text("Du loggas ut från AAMOS Admin. Osparade ändringar försvinner.")
|
|
}
|
|
.alert("Kunde inte spara", isPresented: Binding(
|
|
get: { vm.saveError != nil },
|
|
set: { if !$0 { vm.saveError = nil } }
|
|
)) {
|
|
Button("OK", role: .cancel) {}
|
|
} message: {
|
|
Text(vm.saveError ?? "")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: General
|
|
|
|
private var generalSection: some View {
|
|
Section {
|
|
LabeledContent {
|
|
TextField("Systemnamn", text: $vm.settings.systemName)
|
|
.multilineTextAlignment(.trailing)
|
|
.foregroundStyle(Color(.systemBlue))
|
|
} label: {
|
|
Label("Systemnamn", systemImage: "server.rack")
|
|
}
|
|
|
|
Picker(selection: $vm.settings.timezone) {
|
|
ForEach(timezones, id: \.self) { tz in
|
|
Text(tz.replacingOccurrences(of: "_", with: " "))
|
|
.tag(tz)
|
|
}
|
|
} label: {
|
|
Label("Tidszon", systemImage: "globe")
|
|
}
|
|
.pickerStyle(.navigationLink)
|
|
} header: {
|
|
sectionHeader("Allmänt", icon: "gearshape")
|
|
}
|
|
}
|
|
|
|
// MARK: Security
|
|
|
|
private var securitySection: some View {
|
|
Section {
|
|
LabeledContent {
|
|
Stepper(
|
|
"\(vm.settings.jwtExpiryHours) tim",
|
|
value: $vm.settings.jwtExpiryHours,
|
|
in: 1...168,
|
|
step: 1
|
|
)
|
|
.fixedSize()
|
|
} label: {
|
|
Label("JWT-utgång", systemImage: "key.fill")
|
|
}
|
|
|
|
Toggle(isOn: $vm.settings.twoFactorEnabled) {
|
|
Label("Tvåfaktorsautentisering", systemImage: "lock.shield.fill")
|
|
}
|
|
.tint(Color(.systemGreen))
|
|
} header: {
|
|
sectionHeader("Säkerhet", icon: "lock.fill")
|
|
} footer: {
|
|
Text("JWT-tokens ogiltigförklaras automatiskt efter vald tid. Tvåfaktorsinloggning kräver TOTP-app.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
// MARK: About
|
|
|
|
private var aboutSection: some View {
|
|
Section {
|
|
LabeledContent {
|
|
TextField("https://api.aamos.io", text: $vm.settings.serverURL)
|
|
.multilineTextAlignment(.trailing)
|
|
.foregroundStyle(Color(.systemBlue))
|
|
.keyboardType(.URL)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
} label: {
|
|
Label("Server-URL", systemImage: "network")
|
|
}
|
|
|
|
LabeledContent {
|
|
Text(appVersion)
|
|
.foregroundStyle(.secondary)
|
|
.monospacedDigit()
|
|
} label: {
|
|
Label("Version", systemImage: "info.circle")
|
|
}
|
|
|
|
LabeledContent {
|
|
Text(buildNumber)
|
|
.foregroundStyle(.secondary)
|
|
.monospacedDigit()
|
|
} label: {
|
|
Label("Build", systemImage: "hammer")
|
|
}
|
|
} header: {
|
|
sectionHeader("Om appen", icon: "info.circle")
|
|
}
|
|
}
|
|
|
|
// MARK: Logout
|
|
|
|
private var logoutSection: some View {
|
|
Section {
|
|
Button(role: .destructive) {
|
|
vm.showLogoutConfirmation = true
|
|
} label: {
|
|
HStack {
|
|
Spacer()
|
|
Label("Logga ut", systemImage: "rectangle.portrait.and.arrow.right")
|
|
.fontWeight(.semibold)
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Toolbar
|
|
|
|
@ToolbarContentBuilder
|
|
private var toolbarContent: some ToolbarContent {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
Task { await vm.save() }
|
|
} label: {
|
|
if vm.isSaving {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
} else {
|
|
Text("Spara")
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(vm.hasChanges ? Color(.systemBlue) : .secondary)
|
|
}
|
|
}
|
|
.disabled(!vm.hasChanges || vm.isSaving)
|
|
}
|
|
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
if vm.hasChanges {
|
|
Button("Ångra") { vm.discardChanges() }
|
|
.foregroundStyle(Color(.systemOrange))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Helpers
|
|
|
|
private func sectionHeader(_ title: String, icon: String) -> some View {
|
|
Label(title, systemImage: icon)
|
|
.font(.footnote)
|
|
.fontWeight(.semibold)
|
|
.textCase(nil)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
private var savingOverlay: some View {
|
|
ZStack {
|
|
Color(.systemBackground)
|
|
.opacity(0.6)
|
|
.ignoresSafeArea()
|
|
VStack(spacing: 8) {
|
|
ProgressView()
|
|
.controlSize(.large)
|
|
Text("Sparar…")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(24)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.fill(Color(.secondarySystemBackground))
|
|
.shadow(color: .black.opacity(0.12), radius: 16, y: 4)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
SettingsView()
|
|
}
|