fix: menubarextra shows update immediately
This commit is contained in:
@@ -49,11 +49,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
private func observeSettingsChanges() {
|
private func observeSettingsChanges() {
|
||||||
settingsManager?.$settings
|
settingsManager?.$settings
|
||||||
.sink { [weak self] settings in
|
.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()
|
self?.startTimers()
|
||||||
} else if self?.hasStartedTimers == true {
|
} else if self?.hasStartedTimers == true {
|
||||||
// Restart timers when settings change (only if already started)
|
print("📢 [AppDelegate] Restarting timers with new config")
|
||||||
self?.timerEngine?.start()
|
// Defer timer restart to next runloop to ensure settings are fully propagated
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.timerEngine?.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import SwiftUI
|
|||||||
struct GazeApp: App {
|
struct GazeApp: App {
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
@StateObject private var settingsManager = SettingsManager.shared
|
@StateObject private var settingsManager = SettingsManager.shared
|
||||||
|
@State private var menuBarRefreshID = 0
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
// Onboarding window (only shown when not completed)
|
// Onboarding window (only shown when not completed)
|
||||||
@@ -40,6 +41,7 @@ struct GazeApp: App {
|
|||||||
// Menu bar extra (always present once onboarding is complete)
|
// Menu bar extra (always present once onboarding is complete)
|
||||||
MenuBarExtra("Gaze", systemImage: "eye.fill") {
|
MenuBarExtra("Gaze", systemImage: "eye.fill") {
|
||||||
if let timerEngine = appDelegate.timerEngine {
|
if let timerEngine = appDelegate.timerEngine {
|
||||||
|
let _ = print("🔵 [GazeApp] MenuBarExtra body evaluated, refreshID: \(menuBarRefreshID)")
|
||||||
MenuBarContentView(
|
MenuBarContentView(
|
||||||
timerEngine: timerEngine,
|
timerEngine: timerEngine,
|
||||||
settingsManager: settingsManager,
|
settingsManager: settingsManager,
|
||||||
@@ -47,9 +49,14 @@ struct GazeApp: App {
|
|||||||
onOpenSettings: { appDelegate.openSettings() },
|
onOpenSettings: { appDelegate.openSettings() },
|
||||||
onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) }
|
onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) }
|
||||||
)
|
)
|
||||||
|
.id(menuBarRefreshID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.menuBarExtraStyle(.window)
|
.menuBarExtraStyle(.window)
|
||||||
|
.onChange(of: settingsManager.settings) { _ in
|
||||||
|
menuBarRefreshID += 1
|
||||||
|
print("🔵 [GazeApp] Settings changed, refreshID now: \(menuBarRefreshID)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func closeAllWindows() {
|
private func closeAllWindows() {
|
||||||
@@ -57,4 +64,4 @@ struct GazeApp: App {
|
|||||||
window.close()
|
window.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import Foundation
|
|||||||
// MARK: - Centralized Configuration System
|
// MARK: - Centralized Configuration System
|
||||||
|
|
||||||
/// Unified configuration class that manages all app settings in a centralized way
|
/// Unified configuration class that manages all app settings in a centralized way
|
||||||
struct AppSettings: Codable, Equatable {
|
struct AppSettings: Codable, Equatable, Hashable {
|
||||||
// Timer configurations
|
// Timer configurations
|
||||||
var lookAwayTimer: TimerConfiguration
|
var lookAwayTimer: TimerConfiguration
|
||||||
var lookAwayCountdownSeconds: Int
|
var lookAwayCountdownSeconds: Int
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TimerConfiguration: Codable, Equatable {
|
struct TimerConfiguration: Codable, Equatable, Hashable {
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
var intervalSeconds: Int
|
var intervalSeconds: Int
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TimerState: Equatable {
|
struct TimerState: Equatable, Hashable {
|
||||||
let type: TimerType
|
let type: TimerType
|
||||||
var remainingSeconds: Int
|
var remainingSeconds: Int
|
||||||
var isPaused: Bool
|
var isPaused: Bool
|
||||||
var isActive: Bool
|
var isActive: Bool
|
||||||
var targetDate: Date
|
var targetDate: Date
|
||||||
|
let originalIntervalSeconds: Int // Store original interval for comparison
|
||||||
|
|
||||||
init(type: TimerType, intervalSeconds: Int, isPaused: Bool = false, isActive: Bool = true) {
|
init(type: TimerType, intervalSeconds: Int, isPaused: Bool = false, isActive: Bool = true) {
|
||||||
self.type = type
|
self.type = type
|
||||||
@@ -20,6 +21,7 @@ struct TimerState: Equatable {
|
|||||||
self.isPaused = isPaused
|
self.isPaused = isPaused
|
||||||
self.isActive = isActive
|
self.isActive = isActive
|
||||||
self.targetDate = Date().addingTimeInterval(Double(intervalSeconds))
|
self.targetDate = Date().addingTimeInterval(Double(intervalSeconds))
|
||||||
|
self.originalIntervalSeconds = intervalSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: TimerState, rhs: TimerState) -> Bool {
|
static func == (lhs: TimerState, rhs: TimerState) -> Bool {
|
||||||
@@ -27,5 +29,6 @@ struct TimerState: Equatable {
|
|||||||
&& lhs.isPaused == rhs.isPaused && lhs.isActive == rhs.isActive
|
&& lhs.isPaused == rhs.isPaused && lhs.isActive == rhs.isActive
|
||||||
&& lhs.targetDate.timeIntervalSince1970.rounded()
|
&& lhs.targetDate.timeIntervalSince1970.rounded()
|
||||||
== rhs.targetDate.timeIntervalSince1970.rounded()
|
== rhs.targetDate.timeIntervalSince1970.rounded()
|
||||||
|
&& lhs.originalIntervalSeconds == rhs.originalIntervalSeconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Represents a user-defined timer with customizable properties
|
/// Represents a user-defined timer with customizable properties
|
||||||
struct UserTimer: Codable, Equatable, Identifiable {
|
struct UserTimer: Codable, Equatable, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
var title: String
|
var title: String
|
||||||
var type: UserTimerType
|
var type: UserTimerType
|
||||||
|
|||||||
@@ -24,19 +24,38 @@ class TimerEngine: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
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()
|
stop()
|
||||||
|
|
||||||
|
var newStates: [TimerType: TimerState] = [:]
|
||||||
|
|
||||||
for timerType in TimerType.allCases {
|
for timerType in TimerType.allCases {
|
||||||
let config = settingsManager.timerConfiguration(for: timerType)
|
let config = settingsManager.timerConfiguration(for: timerType)
|
||||||
if config.enabled {
|
if config.enabled {
|
||||||
timerStates[timerType] = TimerState(
|
newStates[timerType] = TimerState(
|
||||||
type: timerType,
|
type: timerType,
|
||||||
intervalSeconds: config.intervalSeconds,
|
intervalSeconds: config.intervalSeconds,
|
||||||
isPaused: false,
|
isPaused: false,
|
||||||
isActive: true
|
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
|
// Start user timers
|
||||||
for userTimer in settingsManager.settings.userTimers {
|
for userTimer in settingsManager.settings.userTimers {
|
||||||
@@ -51,6 +70,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() {
|
func stop() {
|
||||||
timerSubscription?.cancel()
|
timerSubscription?.cancel()
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ struct MenuBarContentView: View {
|
|||||||
var onQuit: () -> Void
|
var onQuit: () -> Void
|
||||||
var onOpenSettings: () -> Void
|
var onOpenSettings: () -> Void
|
||||||
var onOpenSettingsTab: (Int) -> Void
|
var onOpenSettingsTab: (Int) -> Void
|
||||||
|
|
||||||
|
// Force view refresh when timer states change
|
||||||
|
@State private var refreshID = UUID()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
@@ -167,9 +170,13 @@ struct MenuBarContentView: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
.frame(width: 300)
|
.frame(width: 300)
|
||||||
|
.id(refreshID)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("CloseMenuBarPopover"))) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("CloseMenuBarPopover"))) { _ in
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
.onReceive(timerEngine.$timerStates) { _ in
|
||||||
|
refreshID = UUID()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isPaused: Bool {
|
private var isPaused: Bool {
|
||||||
|
|||||||
@@ -120,28 +120,38 @@ struct SettingsWindowView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func applySettings() {
|
private func applySettings() {
|
||||||
settingsManager.settings.lookAwayTimer = TimerConfiguration(
|
print("🔧 [SettingsWindow] Applying settings...")
|
||||||
enabled: lookAwayEnabled,
|
|
||||||
intervalSeconds: lookAwayIntervalMinutes * 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.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
|
||||||
|
print("🔧 [SettingsWindow] Old settings - Blink: \(settingsManager.settings.blinkTimer.enabled), interval: \(settingsManager.settings.blinkTimer.intervalSeconds)")
|
||||||
settingsManager.settings.blinkTimer = TimerConfiguration(
|
print("🔧 [SettingsWindow] New settings - Blink: \(updatedSettings.blinkTimer.enabled), interval: \(updatedSettings.blinkTimer.intervalSeconds)")
|
||||||
enabled: blinkEnabled,
|
|
||||||
intervalSeconds: blinkIntervalMinutes * 60
|
// Assign the entire settings object to trigger didSet and observers
|
||||||
)
|
settingsManager.settings = updatedSettings
|
||||||
|
|
||||||
settingsManager.settings.postureTimer = TimerConfiguration(
|
print("🔧 [SettingsWindow] Settings assigned to manager")
|
||||||
enabled: postureEnabled,
|
|
||||||
intervalSeconds: postureIntervalMinutes * 60
|
|
||||||
)
|
|
||||||
|
|
||||||
settingsManager.settings.launchAtLogin = launchAtLogin
|
|
||||||
settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage
|
|
||||||
settingsManager.settings.userTimers = userTimers
|
|
||||||
|
|
||||||
// Save settings to persist changes
|
|
||||||
settingsManager.save()
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if launchAtLogin {
|
if launchAtLogin {
|
||||||
|
|||||||
Reference in New Issue
Block a user