From bbf1cbb3b5f1bf51b5c73e67e48b2666276380d0 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 9 Jan 2026 23:01:57 -0500 Subject: [PATCH] feat: switch to size preset for more consistent control --- Gaze/AppDelegate.swift | 4 +- Gaze/GazeApp.swift | 5 --- Gaze/Models/AppSettings.swift | 37 +++++++++++++++---- Gaze/Services/MigrationManager.swift | 30 +++++++++++++++ Gaze/Services/SettingsManager.swift | 27 +++++++------- Gaze/Views/MenuBar/MenuBarContentView.swift | 7 ---- .../Onboarding/OnboardingContainerView.swift | 6 +-- .../Onboarding/SettingsOnboardingView.swift | 21 +++++------ Gaze/Views/Reminders/BlinkReminderView.swift | 6 +-- .../Views/Reminders/PostureReminderView.swift | 2 +- Gaze/Views/SettingsWindowView.swift | 10 ++--- 11 files changed, 96 insertions(+), 59 deletions(-) diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 6d9bea6..caa8dca 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -140,14 +140,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { } ) case .blinkTriggered: - let sizePercentage = settingsManager?.settings.subtleReminderSizePercentage ?? 15.0 + let sizePercentage = settingsManager?.settings.subtleReminderSize.percentage ?? 5.0 contentView = AnyView( BlinkReminderView(sizePercentage: sizePercentage) { [weak self] in self?.timerEngine?.dismissReminder() } ) case .postureTriggered: - let sizePercentage = settingsManager?.settings.subtleReminderSizePercentage ?? 10.0 + let sizePercentage = settingsManager?.settings.subtleReminderSize.percentage ?? 5.0 contentView = AnyView( PostureReminderView(sizePercentage: sizePercentage) { [weak self] in self?.timerEngine?.dismissReminder() diff --git a/Gaze/GazeApp.swift b/Gaze/GazeApp.swift index 2359fe5..4cefd4b 100644 --- a/Gaze/GazeApp.swift +++ b/Gaze/GazeApp.swift @@ -11,7 +11,6 @@ import SwiftUI struct GazeApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject private var settingsManager = SettingsManager.shared - @State private var menuBarRefreshID = 0 var body: some Scene { // Onboarding window (only shown when not completed) @@ -48,13 +47,9 @@ struct GazeApp: App { onOpenSettings: { appDelegate.openSettings() }, onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) } ) - .id(menuBarRefreshID) } } .menuBarExtraStyle(.window) - .onChange(of: settingsManager.settings) { _ in - menuBarRefreshID += 1 - } } private func closeAllWindows() { diff --git a/Gaze/Models/AppSettings.swift b/Gaze/Models/AppSettings.swift index c8894e6..ad0faf1 100644 --- a/Gaze/Models/AppSettings.swift +++ b/Gaze/Models/AppSettings.swift @@ -7,6 +7,30 @@ import Foundation +// MARK: - Reminder Size + +enum ReminderSize: String, Codable, CaseIterable { + case small + case medium + case large + + var percentage: Double { + switch self { + case .small: return 1.5 + case .medium: return 2.5 + case .large: return 5.0 + } + } + + var displayName: String { + switch self { + case .small: return "Small" + case .medium: return "Medium" + case .large: return "Large" + } + } +} + // MARK: - Centralized Configuration System /// Unified configuration class that manages all app settings in a centralized way @@ -17,11 +41,9 @@ struct AppSettings: Codable, Equatable, Hashable { var blinkTimer: TimerConfiguration var postureTimer: TimerConfiguration - // User-defined timers (up to 3) var userTimers: [UserTimer] - // UI and display settings - var subtleReminderSizePercentage: Double // 0.5-25% of screen width + var subtleReminderSize: ReminderSize // App state and behavior var hasCompletedOnboarding: Bool @@ -37,7 +59,7 @@ struct AppSettings: Codable, Equatable, Hashable { postureTimer: TimerConfiguration = TimerConfiguration( enabled: true, intervalSeconds: 30 * 60), userTimers: [UserTimer] = [], - subtleReminderSizePercentage: Double = 5.0, + subtleReminderSize: ReminderSize = .large, hasCompletedOnboarding: Bool = false, launchAtLogin: Bool = false, playSounds: Bool = true @@ -47,8 +69,7 @@ struct AppSettings: Codable, Equatable, Hashable { self.blinkTimer = blinkTimer self.postureTimer = postureTimer self.userTimers = userTimers - // Clamp the subtle reminder size to valid range (2-35%) - self.subtleReminderSizePercentage = max(2.0, min(35.0, subtleReminderSizePercentage)) + self.subtleReminderSize = subtleReminderSize self.hasCompletedOnboarding = hasCompletedOnboarding self.launchAtLogin = launchAtLogin self.playSounds = playSounds @@ -61,7 +82,7 @@ struct AppSettings: Codable, Equatable, Hashable { blinkTimer: TimerConfiguration(enabled: false, intervalSeconds: 7 * 60), postureTimer: TimerConfiguration(enabled: true, intervalSeconds: 30 * 60), userTimers: [], - subtleReminderSizePercentage: 5.0, + subtleReminderSize: .large, hasCompletedOnboarding: false, launchAtLogin: false, playSounds: true @@ -73,7 +94,7 @@ struct AppSettings: Codable, Equatable, Hashable { && lhs.lookAwayCountdownSeconds == rhs.lookAwayCountdownSeconds && lhs.blinkTimer == rhs.blinkTimer && lhs.postureTimer == rhs.postureTimer && lhs.userTimers == rhs.userTimers - && lhs.subtleReminderSizePercentage == rhs.subtleReminderSizePercentage + && lhs.subtleReminderSize == rhs.subtleReminderSize && lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding && lhs.launchAtLogin == rhs.launchAtLogin && lhs.playSounds == rhs.playSounds } diff --git a/Gaze/Services/MigrationManager.swift b/Gaze/Services/MigrationManager.swift index 7fe5393..a69e1a5 100644 --- a/Gaze/Services/MigrationManager.swift +++ b/Gaze/Services/MigrationManager.swift @@ -83,6 +83,7 @@ class MigrationManager { private func setupMigrations() { migrations.append(Version101Migration()) + migrations.append(Version102Migration()) } private func getTargetVersion() -> String { @@ -165,6 +166,35 @@ class Version101Migration: Migration { // Add any new fields with default values if they don't exist // Transform data structures as needed + return migratedData + } +} + +class Version102Migration: Migration { + var targetVersion: String = "1.0.2" + + func migrate(_ data: [String: Any]) throws -> [String: Any] { + var migratedData = data + + // Migrate subtleReminderSizePercentage (Double) to subtleReminderSize (ReminderSize enum) + if let oldPercentage = migratedData["subtleReminderSizePercentage"] as? Double { + // Map old percentage values to new enum cases + let reminderSize: String + if oldPercentage <= 2.0 { + reminderSize = "small" + } else if oldPercentage <= 3.5 { + reminderSize = "medium" + } else { + reminderSize = "large" + } + + migratedData["subtleReminderSize"] = reminderSize + migratedData.removeValue(forKey: "subtleReminderSizePercentage") + } else if migratedData["subtleReminderSize"] == nil { + // If neither old nor new key exists, set default + migratedData["subtleReminderSize"] = "large" + } + return migratedData } } \ No newline at end of file diff --git a/Gaze/Services/SettingsManager.swift b/Gaze/Services/SettingsManager.swift index 93602f5..e58d7e5 100644 --- a/Gaze/Services/SettingsManager.swift +++ b/Gaze/Services/SettingsManager.swift @@ -5,38 +5,39 @@ // Created by Mike Freno on 1/7/26. // -import Foundation import Combine +import Foundation @MainActor class SettingsManager: ObservableObject { static let shared = SettingsManager() - + @Published var settings: AppSettings { didSet { save() } } - + private let userDefaults = UserDefaults.standard private let settingsKey = "gazeAppSettings" - + private init() { #if DEBUG - // Clear settings on every development build - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") + // Clear settings on every development build + UserDefaults.standard.removeObject(forKey: "gazeAppSettings") #endif self.settings = Self.loadSettings() } - + private static func loadSettings() -> AppSettings { guard let data = UserDefaults.standard.data(forKey: "gazeAppSettings"), - let settings = try? JSONDecoder().decode(AppSettings.self, from: data) else { + let settings = try? JSONDecoder().decode(AppSettings.self, from: data) + else { return .defaults } return settings } - + func save() { guard let data = try? JSONEncoder().encode(settings) else { print("Failed to encode settings") @@ -44,15 +45,15 @@ class SettingsManager: ObservableObject { } userDefaults.set(data, forKey: settingsKey) } - + func load() { settings = Self.loadSettings() } - + func resetToDefaults() { settings = .defaults } - + func timerConfiguration(for type: TimerType) -> TimerConfiguration { switch type { case .lookAway: @@ -63,7 +64,7 @@ class SettingsManager: ObservableObject { return settings.postureTimer } } - + func updateTimerConfiguration(for type: TimerType, configuration: TimerConfiguration) { switch type { case .lookAway: diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index 2c4f138..8336fed 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -50,9 +50,6 @@ struct MenuBarContentView: View { var onQuit: () -> Void var onOpenSettings: () -> Void var onOpenSettingsTab: (Int) -> Void - - // Force view refresh when timer states change - @State private var refreshID = UUID() var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -170,13 +167,9 @@ struct MenuBarContentView: View { .padding(.vertical, 8) } .frame(width: 300) - .id(refreshID) .onReceive(NotificationCenter.default.publisher(for: Notification.Name("CloseMenuBarPopover"))) { _ in dismiss() } - .onReceive(timerEngine.$timerStates) { _ in - refreshID = UUID() - } } private var isPaused: Bool { diff --git a/Gaze/Views/Onboarding/OnboardingContainerView.swift b/Gaze/Views/Onboarding/OnboardingContainerView.swift index e071d99..92e8688 100644 --- a/Gaze/Views/Onboarding/OnboardingContainerView.swift +++ b/Gaze/Views/Onboarding/OnboardingContainerView.swift @@ -30,7 +30,7 @@ struct OnboardingContainerView: View { @State private var postureEnabled = true @State private var postureIntervalMinutes = 30 @State private var launchAtLogin = false - @State private var subtleReminderSizePercentage = 5.0 + @State private var subtleReminderSize: ReminderSize = .large @State private var isAnimatingOut = false @Environment(\.dismiss) private var dismiss @@ -76,7 +76,7 @@ struct OnboardingContainerView: View { SettingsOnboardingView( launchAtLogin: $launchAtLogin, - subtleReminderSizePercentage: $subtleReminderSizePercentage, + subtleReminderSize: $subtleReminderSize, isOnboarding: true ) .tag(4) @@ -162,7 +162,7 @@ struct OnboardingContainerView: View { ) settingsManager.settings.launchAtLogin = launchAtLogin - settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage + settingsManager.settings.subtleReminderSize = subtleReminderSize settingsManager.settings.hasCompletedOnboarding = true // Apply launch at login setting diff --git a/Gaze/Views/Onboarding/SettingsOnboardingView.swift b/Gaze/Views/Onboarding/SettingsOnboardingView.swift index 8e909cc..863f505 100644 --- a/Gaze/Views/Onboarding/SettingsOnboardingView.swift +++ b/Gaze/Views/Onboarding/SettingsOnboardingView.swift @@ -9,7 +9,7 @@ import SwiftUI struct SettingsOnboardingView: View { @Binding var launchAtLogin: Bool - @Binding var subtleReminderSizePercentage: Double + @Binding var subtleReminderSize: ReminderSize var isOnboarding: Bool = true var body: some View { @@ -62,16 +62,13 @@ struct SettingsOnboardingView: View { .font(.caption) .foregroundColor(.secondary) - HStack { - Slider( - value: $subtleReminderSizePercentage, - in: 0.5...25, - step: 0.5 - ) - Text("\(String(format: "%.1f", subtleReminderSizePercentage))%") - .frame(width: 50, alignment: .trailing) - .monospacedDigit() + Picker("Size", selection: $subtleReminderSize) { + ForEach(ReminderSize.allCases, id: \.self) { size in + Text(size.displayName).tag(size) + } } + .pickerStyle(.segmented) + .labelsHidden() } .padding() .glassEffect(.regular, in: .rect(cornerRadius: 12)) @@ -166,7 +163,7 @@ struct SettingsOnboardingView: View { #Preview("Settings Onboarding - Launch Disabled") { SettingsOnboardingView( launchAtLogin: .constant(false), - subtleReminderSizePercentage: .constant(5.0), + subtleReminderSize: .constant(.large), isOnboarding: true ) } @@ -174,7 +171,7 @@ struct SettingsOnboardingView: View { #Preview("Settings Onboarding - Launch Enabled") { SettingsOnboardingView( launchAtLogin: .constant(true), - subtleReminderSizePercentage: .constant(10.0), + subtleReminderSize: .constant(.medium), isOnboarding: true ) } diff --git a/Gaze/Views/Reminders/BlinkReminderView.swift b/Gaze/Views/Reminders/BlinkReminderView.swift index c778142..6d41905 100644 --- a/Gaze/Views/Reminders/BlinkReminderView.swift +++ b/Gaze/Views/Reminders/BlinkReminderView.swift @@ -20,7 +20,7 @@ struct BlinkReminderView: View { private let screenWidth = NSScreen.main?.frame.width ?? 1200 private var baseSize: CGFloat { - screenWidth * (sizePercentage / 100.0) + screenWidth * (sizePercentage / 100.0) * 2.5 } var body: some View { @@ -45,7 +45,7 @@ struct BlinkReminderView: View { } .opacity(opacity) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .padding(.top, screenHeight * 0.1) + .padding(.top, screenHeight * 0.05) .onAppear { startAnimation() } @@ -56,7 +56,7 @@ struct BlinkReminderView: View { opacity = 1.0 scale = 1.0 } - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { shouldShowAnimation = true } diff --git a/Gaze/Views/Reminders/PostureReminderView.swift b/Gaze/Views/Reminders/PostureReminderView.swift index 7132fcd..6172b0b 100644 --- a/Gaze/Views/Reminders/PostureReminderView.swift +++ b/Gaze/Views/Reminders/PostureReminderView.swift @@ -28,7 +28,7 @@ struct PostureReminderView: View { .opacity(opacity) .offset(y: yOffset) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .padding(.top, screenHeight * 0.1) + .padding(.top, screenHeight * 0.075) .onAppear { startAnimation() } diff --git a/Gaze/Views/SettingsWindowView.swift b/Gaze/Views/SettingsWindowView.swift index db2eb81..44a13d8 100644 --- a/Gaze/Views/SettingsWindowView.swift +++ b/Gaze/Views/SettingsWindowView.swift @@ -18,7 +18,7 @@ struct SettingsWindowView: View { @State private var postureEnabled: Bool @State private var postureIntervalMinutes: Int @State private var launchAtLogin: Bool - @State private var subtleReminderSizePercentage: Double + @State private var subtleReminderSize: ReminderSize @State private var userTimers: [UserTimer] init(settingsManager: SettingsManager, initialTab: Int = 0) { @@ -37,8 +37,8 @@ struct SettingsWindowView: View { _postureIntervalMinutes = State( initialValue: settingsManager.settings.postureTimer.intervalSeconds / 60) _launchAtLogin = State(initialValue: settingsManager.settings.launchAtLogin) - _subtleReminderSizePercentage = State( - initialValue: settingsManager.settings.subtleReminderSizePercentage) + _subtleReminderSize = State( + initialValue: settingsManager.settings.subtleReminderSize) _userTimers = State(initialValue: settingsManager.settings.userTimers) } @@ -81,7 +81,7 @@ struct SettingsWindowView: View { SettingsOnboardingView( launchAtLogin: $launchAtLogin, - subtleReminderSizePercentage: $subtleReminderSizePercentage, + subtleReminderSize: $subtleReminderSize, isOnboarding: false ) .tag(4) @@ -137,7 +137,7 @@ struct SettingsWindowView: View { intervalSeconds: postureIntervalMinutes * 60 ), userTimers: userTimers, - subtleReminderSizePercentage: subtleReminderSizePercentage, + subtleReminderSize: subtleReminderSize, hasCompletedOnboarding: settingsManager.settings.hasCompletedOnboarding, launchAtLogin: launchAtLogin, playSounds: settingsManager.settings.playSounds