import Foundation import Combine final class SettingsStore: ObservableObject { @Published var serverURL: String { didSet { UserDefaults.standard.set(serverURL, forKey: Keys.serverURL) } } @Published var voiceID: String { didSet { UserDefaults.standard.set(voiceID, forKey: Keys.voiceID) } } @Published var modelID: String? { didSet { UserDefaults.standard.set(modelID, forKey: Keys.modelID) } } @Published var textMode: Bool { didSet { UserDefaults.standard.set(textMode, forKey: Keys.textMode) } } @Published var volume: Double { didSet { UserDefaults.standard.set(volume, forKey: Keys.volume) } } private enum Keys { static let serverURL = "serverURL" static let voiceID = "voiceID" static let modelID = "modelID" static let textMode = "textMode" static let volume = "volume" } static let defaultServerURL = "https://192.168.86.2:9178" init() { let d = UserDefaults.standard // Sanitize legacy values: earlier versions wrote the field to storage on // every keystroke, so an invalid half-typed URL could be persisted and // shipped to the network layer. Fall back to the documented default and // repair the stored value once. let storedURL = d.string(forKey: Keys.serverURL) ?? Self.defaultServerURL switch ServerConfig.parse(storedURL) { case .success: serverURL = storedURL case .failure: // Legacy invalid value: repair to the documented default. serverURL = Self.defaultServerURL d.set(Self.defaultServerURL, forKey: Keys.serverURL) } let storedVoice = d.string(forKey: Keys.voiceID) voiceID = Voices.all.contains { $0.id == storedVoice } ? (storedVoice ?? "") : Voices.defaultID modelID = d.string(forKey: Keys.modelID) textMode = d.bool(forKey: Keys.textMode) volume = d.object(forKey: Keys.volume) as? Double ?? 0.8 } var trimmedServerURL: String { serverURL.trimmingCharacters(in: .whitespacesAndNewlines) } }