// // PreferencesView.swift // kordophone2 // // Created by James Magahern on 8/24/25. // import KeychainAccess import SwiftUI struct PreferencesView: View { @State var accountSettingsModel = AccountSettings.ViewModel() var body: some View { TabView { AccountSettings(model: $accountSettingsModel) .tabItem { Label("Account", systemImage: "person.crop.circle") } } .frame(width: 480.0, height: 300.0) .padding(20.0) } } struct AccountSettings: View { @Binding var model: ViewModel var body: some View { Form { Section("Server Settings") { TextField("Server", text: $model.serverURL) } Spacer() .frame(height: 44.0) Section("Authentication") { TextField("Username", text: $model.username) .textContentType(.username) SecureField("Password", text: $model.password) .textContentType(.password) } } .task { await model.load() } } // MARK: - Types @Observable class ViewModel { var serverURL: String var username: String var password: String private let xpc = XPCClient() private let keychain = Keychain(service: "net.buzzert.kordophonecd") init(serverURL: String = "", username: String = "", password: String = "") { self.serverURL = serverURL self.username = username self.password = password autosave() } func load() async { do { let settings = try await xpc.getSettings() self.serverURL = settings.serverUrl self.username = settings.username self.password = keychain[settings.username] ?? "" } catch { print("Error getting settings: \(error)") } } private func autosave() { withObservationTracking { _ = serverURL _ = username _ = password } onChange: { Task { @MainActor [weak self] in guard let self else { return } do { let currentSettings = try await xpc.getSettings() if currentSettings.serverUrl != serverURL || currentSettings.username != username { try await xpc.setSettings(settings: Serialized.Settings( serverUrl: serverURL, username: username )) } keychain[username] = password } catch { print("Error saving settings: \(error)") } autosave() } } } } }