fix: menubarextra shows update immediately

This commit is contained in:
Michael Freno
2026-01-09 19:41:49 -05:00
parent ec87520ba6
commit edf2d0115d
9 changed files with 172 additions and 30 deletions

View File

@@ -49,11 +49,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private func observeSettingsChanges() {
settingsManager?.$settings
.sink { [weak self] settings in
if settings.hasCompletedOnboarding {
print("📢 [AppDelegate] Settings changed!")
if settings.hasCompletedOnboarding && self?.hasStartedTimers == false {
print("📢 [AppDelegate] Starting timers for first time")
self?.startTimers()
} else if self?.hasStartedTimers == true {
// Restart timers when settings change (only if already started)
self?.timerEngine?.start()
print("📢 [AppDelegate] Restarting timers with new config")
// Defer timer restart to next runloop to ensure settings are fully propagated
DispatchQueue.main.async {
self?.timerEngine?.start()
}
}
}
.store(in: &cancellables)

View File

@@ -11,6 +11,7 @@ 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)
@@ -40,6 +41,7 @@ struct GazeApp: App {
// Menu bar extra (always present once onboarding is complete)
MenuBarExtra("Gaze", systemImage: "eye.fill") {
if let timerEngine = appDelegate.timerEngine {
let _ = print("🔵 [GazeApp] MenuBarExtra body evaluated, refreshID: \(menuBarRefreshID)")
MenuBarContentView(
timerEngine: timerEngine,
settingsManager: settingsManager,
@@ -47,9 +49,14 @@ struct GazeApp: App {
onOpenSettings: { appDelegate.openSettings() },
onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) }
)
.id(menuBarRefreshID)
}
}
.menuBarExtraStyle(.window)
.onChange(of: settingsManager.settings) { _ in
menuBarRefreshID += 1
print("🔵 [GazeApp] Settings changed, refreshID now: \(menuBarRefreshID)")
}
}
private func closeAllWindows() {

View File

@@ -10,7 +10,7 @@ import Foundation
// MARK: - Centralized Configuration System
/// Unified configuration class that manages all app settings in a centralized way
struct AppSettings: Codable, Equatable {
struct AppSettings: Codable, Equatable, Hashable {
// Timer configurations
var lookAwayTimer: TimerConfiguration
var lookAwayCountdownSeconds: Int

View File

@@ -7,7 +7,7 @@
import Foundation
struct TimerConfiguration: Codable, Equatable {
struct TimerConfiguration: Codable, Equatable, Hashable {
var enabled: Bool
var intervalSeconds: Int

View File

@@ -7,12 +7,13 @@
import Foundation
struct TimerState: Equatable {
struct TimerState: Equatable, Hashable {
let type: TimerType
var remainingSeconds: Int
var isPaused: Bool
var isActive: Bool
var targetDate: Date
let originalIntervalSeconds: Int // Store original interval for comparison
init(type: TimerType, intervalSeconds: Int, isPaused: Bool = false, isActive: Bool = true) {
self.type = type
@@ -20,6 +21,7 @@ struct TimerState: Equatable {
self.isPaused = isPaused
self.isActive = isActive
self.targetDate = Date().addingTimeInterval(Double(intervalSeconds))
self.originalIntervalSeconds = intervalSeconds
}
static func == (lhs: TimerState, rhs: TimerState) -> Bool {
@@ -27,5 +29,6 @@ struct TimerState: Equatable {
&& lhs.isPaused == rhs.isPaused && lhs.isActive == rhs.isActive
&& lhs.targetDate.timeIntervalSince1970.rounded()
== rhs.targetDate.timeIntervalSince1970.rounded()
&& lhs.originalIntervalSeconds == rhs.originalIntervalSeconds
}
}

View File

@@ -9,7 +9,7 @@ import Foundation
import SwiftUI
/// Represents a user-defined timer with customizable properties
struct UserTimer: Codable, Equatable, Identifiable {
struct UserTimer: Codable, Equatable, Identifiable, Hashable {
let id: String
var title: String
var type: UserTimerType

View File

@@ -24,20 +24,39 @@ class TimerEngine: ObservableObject {
}
func start() {
print("🎯 [TimerEngine] start() called, subscription exists: \(timerSubscription != nil)")
// If timers are already running, just update configurations without resetting
if timerSubscription != nil {
print("🎯 [TimerEngine] Updating existing configurations")
updateConfigurations()
return
}
print("🎯 [TimerEngine] Initial start - creating all timer states")
// Initial start - create all timer states
stop()
var newStates: [TimerType: TimerState] = [:]
for timerType in TimerType.allCases {
let config = settingsManager.timerConfiguration(for: timerType)
if config.enabled {
timerStates[timerType] = TimerState(
newStates[timerType] = TimerState(
type: timerType,
intervalSeconds: config.intervalSeconds,
isPaused: false,
isActive: true
)
print("🎯 [TimerEngine] Created state for \(timerType.displayName): \(config.intervalSeconds)s")
}
}
// Assign the entire dictionary at once to trigger @Published
timerStates = newStates
print("🎯 [TimerEngine] Assigned \(newStates.count) timer states")
// Start user timers
for userTimer in settingsManager.settings.userTimers {
startUserTimer(userTimer)
@@ -52,6 +71,97 @@ class TimerEngine: ObservableObject {
}
}
private func updateConfigurations() {
print("🔄 [TimerEngine] updateConfigurations() called")
print("🔄 [TimerEngine] Current timerStates keys: \(timerStates.keys.map { $0.displayName })")
var newStates: [TimerType: TimerState] = [:]
for timerType in TimerType.allCases {
let config = settingsManager.timerConfiguration(for: timerType)
print("🔄 [TimerEngine] Processing \(timerType.displayName): enabled=\(config.enabled), intervalSeconds=\(config.intervalSeconds)")
if config.enabled {
if let existingState = timerStates[timerType] {
// Timer exists - check if interval changed
print("🔄 [TimerEngine] \(timerType.displayName) exists in current states")
if existingState.originalIntervalSeconds != config.intervalSeconds {
// Interval changed - reset with new interval
print("🔄 [TimerEngine] \(timerType.displayName) interval changed: \(existingState.originalIntervalSeconds)s -> \(config.intervalSeconds)s, resetting")
newStates[timerType] = TimerState(
type: timerType,
intervalSeconds: config.intervalSeconds,
isPaused: existingState.isPaused,
isActive: true
)
} else {
// Interval unchanged - keep existing state
print("🔄 [TimerEngine] \(timerType.displayName) unchanged, keeping state (remaining: \(existingState.remainingSeconds)s)")
newStates[timerType] = existingState
}
} else {
// Timer was just enabled - create new state
print("🔄 [TimerEngine] \(timerType.displayName) NOT in current states, newly enabled, creating state")
newStates[timerType] = TimerState(
type: timerType,
intervalSeconds: config.intervalSeconds,
isPaused: false,
isActive: true
)
}
} else {
if timerStates[timerType] != nil {
print("🔄 [TimerEngine] \(timerType.displayName) disabled, removing state")
} else {
print("🔄 [TimerEngine] \(timerType.displayName) disabled and not in current states")
}
}
// If config.enabled is false and timer exists, it will be removed
}
print("🔄 [TimerEngine] New states keys: \(newStates.keys.map { $0.displayName })")
print("🔄 [TimerEngine] Assigning \(newStates.count) timer states (was \(timerStates.count))")
// Assign the entire dictionary at once to trigger @Published
timerStates = newStates
// Update user timers
updateUserTimers()
}
private func updateUserTimers() {
let currentTimerIds = Set(userTimerStates.keys)
let newTimerIds = Set(settingsManager.settings.userTimers.map { $0.id })
// Remove timers that no longer exist
let removedIds = currentTimerIds.subtracting(newTimerIds)
for id in removedIds {
userTimerStates.removeValue(forKey: id)
}
// Add or update timers
for userTimer in settingsManager.settings.userTimers {
if let existingState = userTimerStates[userTimer.id] {
// Check if interval changed
if existingState.originalIntervalSeconds != userTimer.timeOnScreenSeconds {
// Interval changed - reset with new interval
userTimerStates[userTimer.id] = TimerState(
type: .lookAway, // Placeholder
intervalSeconds: userTimer.timeOnScreenSeconds,
isPaused: existingState.isPaused,
isActive: userTimer.enabled
)
} else {
// Just update enabled state if needed
var state = existingState
state.isActive = userTimer.enabled
userTimerStates[userTimer.id] = state
}
} else {
// New timer - create state
startUserTimer(userTimer)
}
}
}
func stop() {
timerSubscription?.cancel()
timerSubscription = nil

View File

@@ -51,6 +51,9 @@ 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
@@ -167,9 +170,13 @@ 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

@@ -120,28 +120,38 @@ struct SettingsWindowView: View {
}
private func applySettings() {
settingsManager.settings.lookAwayTimer = TimerConfiguration(
enabled: lookAwayEnabled,
intervalSeconds: lookAwayIntervalMinutes * 60
)
settingsManager.settings.lookAwayCountdownSeconds = lookAwayCountdownSeconds
print("🔧 [SettingsWindow] Applying settings...")
settingsManager.settings.blinkTimer = TimerConfiguration(
enabled: blinkEnabled,
intervalSeconds: blinkIntervalMinutes * 60
// Create a new AppSettings object with updated values
// This triggers the didSet observer in SettingsManager
let updatedSettings = AppSettings(
lookAwayTimer: TimerConfiguration(
enabled: lookAwayEnabled,
intervalSeconds: lookAwayIntervalMinutes * 60
),
lookAwayCountdownSeconds: lookAwayCountdownSeconds,
blinkTimer: TimerConfiguration(
enabled: blinkEnabled,
intervalSeconds: blinkIntervalMinutes * 60
),
postureTimer: TimerConfiguration(
enabled: postureEnabled,
intervalSeconds: postureIntervalMinutes * 60
),
userTimers: userTimers,
subtleReminderSizePercentage: subtleReminderSizePercentage,
hasCompletedOnboarding: settingsManager.settings.hasCompletedOnboarding,
launchAtLogin: launchAtLogin,
playSounds: settingsManager.settings.playSounds
)
settingsManager.settings.postureTimer = TimerConfiguration(
enabled: postureEnabled,
intervalSeconds: postureIntervalMinutes * 60
)
print("🔧 [SettingsWindow] Old settings - Blink: \(settingsManager.settings.blinkTimer.enabled), interval: \(settingsManager.settings.blinkTimer.intervalSeconds)")
print("🔧 [SettingsWindow] New settings - Blink: \(updatedSettings.blinkTimer.enabled), interval: \(updatedSettings.blinkTimer.intervalSeconds)")
settingsManager.settings.launchAtLogin = launchAtLogin
settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage
settingsManager.settings.userTimers = userTimers
// Assign the entire settings object to trigger didSet and observers
settingsManager.settings = updatedSettings
// Save settings to persist changes
settingsManager.save()
print("🔧 [SettingsWindow] Settings assigned to manager")
do {
if launchAtLogin {