feat: switch to size preset for more consistent control
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user