feat: persistence

This commit is contained in:
Michael Freno
2026-01-13 10:16:33 -05:00
parent f74390e4c3
commit 338e79c6c6
7 changed files with 363 additions and 61 deletions

View File

@@ -76,7 +76,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}
func applicationWillTerminate(_ notification: Notification) {
settingsManager.save()
settingsManager.saveImmediately()
timerEngine?.stop()
}
@@ -98,7 +98,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
@objc private func systemWillSleep() {
timerEngine?.handleSystemSleep()
settingsManager.save()
settingsManager.saveImmediately()
}
@objc private func systemDidWake() {

View File

@@ -26,10 +26,6 @@ class SettingsManager: ObservableObject {
]
private init() {
#if DEBUG
// Clear settings on every development build
UserDefaults.standard.removeObject(forKey: "gazeAppSettings")
#endif
self.settings = Self.loadSettings()
#if DEBUG
validateTimerConfigMappings()
@@ -39,7 +35,7 @@ class SettingsManager: ObservableObject {
deinit {
saveCancellable?.cancel()
// Final save will be called by AppDelegate.applicationWillTerminate
// Final save is called by AppDelegate.applicationWillTerminate
}
private func setupDebouncedSave() {
@@ -52,23 +48,72 @@ class SettingsManager: ObservableObject {
}
private static func loadSettings() -> AppSettings {
guard let data = UserDefaults.standard.data(forKey: "gazeAppSettings"),
let settings = try? JSONDecoder().decode(AppSettings.self, from: data)
else {
guard let data = UserDefaults.standard.data(forKey: "gazeAppSettings") else {
#if DEBUG
print(" No saved settings found, using defaults")
#endif
return .defaults
}
do {
let settings = try JSONDecoder().decode(AppSettings.self, from: data)
#if DEBUG
print("✅ Settings loaded successfully (\(data.count) bytes)")
#endif
return settings
} catch {
print("⚠️ Failed to decode settings, using defaults: \(error.localizedDescription)")
if let decodingError = error as? DecodingError {
switch decodingError {
case .keyNotFound(let key, let context):
print(" Missing key: \(key.stringValue) at path: \(context.codingPath)")
case .typeMismatch(let type, let context):
print(" Type mismatch for type: \(type) at path: \(context.codingPath)")
case .valueNotFound(let type, let context):
print(" Value not found for type: \(type) at path: \(context.codingPath)")
case .dataCorrupted(let context):
print(" Data corrupted at path: \(context.codingPath)")
@unknown default:
print(" Unknown decoding error: \(decodingError)")
}
}
return .defaults
}
return settings
}
/// Saves settings to UserDefaults.
/// Note: Settings are automatically saved via debouncing (500ms delay) when the `settings` property changes.
/// This method is also called explicitly during app termination to ensure final state is persisted.
func save() {
guard let data = try? JSONEncoder().encode(settings) else {
print("Failed to encode settings")
return
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(settings)
userDefaults.set(data, forKey: settingsKey)
#if DEBUG
print("✅ Settings saved successfully (\(data.count) bytes)")
#endif
} catch {
print("❌ Failed to encode settings: \(error.localizedDescription)")
if let encodingError = error as? EncodingError {
switch encodingError {
case .invalidValue(let value, let context):
print(" Invalid value: \(value) at path: \(context.codingPath)")
default:
print(" Encoding error: \(encodingError)")
}
}
}
userDefaults.set(data, forKey: settingsKey)
}
/// Forces immediate save and ensures UserDefaults are persisted to disk.
/// Use this for critical save points like app termination or system sleep.
func saveImmediately() {
save()
// Cancel any pending debounced saves
saveCancellable?.cancel()
setupDebouncedSave()
}
func load() {

View File

@@ -59,6 +59,13 @@ struct SettingsWindowView: View {
Divider()
HStack {
#if DEBUG
Button("Retrigger Onboarding") {
retriggerOnboarding()
}
.buttonStyle(.bordered)
#endif
Spacer()
Button("Close") {
@@ -87,6 +94,22 @@ struct SettingsWindowView: View {
window.close()
}
}
#if DEBUG
private func retriggerOnboarding() {
// Close settings window first
closeWindow()
// Get AppDelegate and open onboarding
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
// Reset onboarding state so it shows as fresh
settingsManager.settings.hasCompletedOnboarding = false
// Open onboarding window
appDelegate.openOnboarding()
}
}
#endif
}
#Preview {