feat: switch to size preset for more consistent control

This commit is contained in:
Michael Freno
2026-01-09 23:01:57 -05:00
parent 56521833e1
commit bbf1cbb3b5
11 changed files with 96 additions and 59 deletions

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -83,6 +83,7 @@ class MigrationManager {
private func setupMigrations() {
migrations.append(Version101Migration())
migrations.append(Version102Migration())
}
private func getTargetVersion() -> String {
@@ -168,3 +169,32 @@ class Version101Migration: Migration {
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
}
}

View File

@@ -5,8 +5,8 @@
// Created by Mike Freno on 1/7/26.
//
import Foundation
import Combine
import Foundation
@MainActor
class SettingsManager: ObservableObject {
@@ -31,7 +31,8 @@ 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 {
let settings = try? JSONDecoder().decode(AppSettings.self, from: data)
else {
return .defaults
}
return settings

View File

@@ -51,9 +51,6 @@ struct MenuBarContentView: View {
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) {
// Header
@@ -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 {

View File

@@ -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

View File

@@ -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,17 +62,14 @@ 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
)
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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