import Foundation import Combine private func ud(_ type: T.Type, key: String) -> T? { guard let data = UserDefaults.standard.data(forKey: key) else { return nil } return try? JSONDecoder().decode(type, from: data) } private func saveUD(_ value: T?, key: String) { guard let value else { UserDefaults.standard.removeObject(forKey: key); return } if let data = try? JSONEncoder().encode(value) { UserDefaults.standard.set(data, forKey: key) } } final class ConnectionStore: ObservableObject { static let shared = ConnectionStore() @Published var savedConnection: NASConnection? { didSet { if let conn = savedConnection { SavedConnections.save(conn) } else { SavedConnections.delete() } } } @Published var backupFilter: BackupFilter { didSet { saveUD(backupFilter, key: "backupFilter") } } @Published var trustedSSIDs: [String] { didSet { UserDefaults.standard.set(trustedSSIDs, forKey: "trustedSSIDs") } } @Published var lanTriggerEnabled: Bool { didSet { UserDefaults.standard.set(lanTriggerEnabled, forKey: "lanTriggerEnabled") } } @Published var startDelaySeconds: Int { didSet { UserDefaults.standard.set(startDelaySeconds, forKey: "startDelaySeconds") } } @Published var chargingOnlyMode: Bool { didSet { UserDefaults.standard.set(chargingOnlyMode, forKey: "chargingOnlyMode") } } @Published var quietHoursEnabled: Bool { didSet { UserDefaults.standard.set(quietHoursEnabled, forKey: "quietHoursEnabled") } } @Published var quietHoursStart: Int { didSet { UserDefaults.standard.set(quietHoursStart, forKey: "quietHoursStart") } } @Published var quietHoursEnd: Int { didSet { UserDefaults.standard.set(quietHoursEnd, forKey: "quietHoursEnd") } } // Session — not persisted, resets on every app launch @Published var isSessionActive = false // Onboarding — persisted so we only show each step once @Published var onboardingPhotoShown: Bool { didSet { UserDefaults.standard.set(onboardingPhotoShown, forKey: "onboardingPhotoShown") } } // Appearance @Published var appearanceMode: AppearanceMode { didSet { UserDefaults.standard.set(appearanceMode.rawValue, forKey: "appearanceMode") } } // Local retention: 0 = never delete, 60/90/365 = days after backup @Published var localRetentionDays: Int { didSet { UserDefaults.standard.set(localRetentionDays, forKey: "localRetentionDays") } } // Auto-backup: default on after first destination is configured @Published var autoBackupEnabled: Bool { didSet { UserDefaults.standard.set(autoBackupEnabled, forKey: "autoBackupEnabled") } } // Cellular & remote access @Published var allowCellularBackup: Bool { didSet { UserDefaults.standard.set(allowCellularBackup, forKey: "allowCellularBackup") } } @Published var useTailscaleWhenRemote: Bool { didSet { UserDefaults.standard.set(useTailscaleWhenRemote, forKey: "useTailscaleWhenRemote") } } @Published var tailscaleHost: String { didSet { UserDefaults.standard.set(tailscaleHost, forKey: "tailscaleHost") } } @Published private(set) var historyEntries: [BackupHistoryEntry] = [] private init() { savedConnection = SavedConnections.load() backupFilter = ud(BackupFilter.self, key: "backupFilter") ?? BackupFilter() trustedSSIDs = UserDefaults.standard.stringArray(forKey: "trustedSSIDs") ?? [] lanTriggerEnabled = UserDefaults.standard.object(forKey: "lanTriggerEnabled") as? Bool ?? true startDelaySeconds = UserDefaults.standard.object(forKey: "startDelaySeconds") as? Int ?? 30 chargingOnlyMode = UserDefaults.standard.object(forKey: "chargingOnlyMode") as? Bool ?? true quietHoursEnabled = UserDefaults.standard.object(forKey: "quietHoursEnabled") as? Bool ?? false quietHoursStart = UserDefaults.standard.object(forKey: "quietHoursStart") as? Int ?? 23 quietHoursEnd = UserDefaults.standard.object(forKey: "quietHoursEnd") as? Int ?? 7 onboardingPhotoShown = UserDefaults.standard.bool(forKey: "onboardingPhotoShown") appearanceMode = AppearanceMode(rawValue: UserDefaults.standard.string(forKey: "appearanceMode") ?? "") ?? .system localRetentionDays = UserDefaults.standard.object(forKey: "localRetentionDays") as? Int ?? 0 autoBackupEnabled = UserDefaults.standard.object(forKey: "autoBackupEnabled") as? Bool ?? true allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false useTailscaleWhenRemote = UserDefaults.standard.object(forKey: "useTailscaleWhenRemote") as? Bool ?? false tailscaleHost = UserDefaults.standard.string(forKey: "tailscaleHost") ?? "" historyEntries = ud([BackupHistoryEntry].self, key: "historyEntries") ?? [] } func appendHistoryEntry(_ entry: BackupHistoryEntry) { historyEntries.insert(entry, at: 0) saveUD(historyEntries, key: "historyEntries") } func removeHistoryEntries(at offsets: IndexSet) { historyEntries.remove(atOffsets: offsets) saveUD(historyEntries, key: "historyEntries") } func clearHistory() { historyEntries = [] UserDefaults.standard.removeObject(forKey: "historyEntries") } func clearCompletedHistory() { historyEntries.removeAll { $0.failedCount == 0 } saveUD(historyEntries, key: "historyEntries") } func clearErrorHistory() { historyEntries.removeAll { $0.failedCount > 0 } saveUD(historyEntries, key: "historyEntries") } /// Locks the screen — keeps the saved connection but requires Face ID again. func lock() { isSessionActive = false } /// Clears everything: connection, session, and onboarding state. Returns to ConnectView. func signOut() { isSessionActive = false onboardingPhotoShown = false savedConnection = nil } var isInQuietHours: Bool { guard quietHoursEnabled else { return false } let hour = Calendar.current.component(.hour, from: Date()) if quietHoursStart > quietHoursEnd { return hour >= quietHoursStart || hour < quietHoursEnd } return hour >= quietHoursStart && hour < quietHoursEnd } }