fix: timers update properly, cleanup coloring

This commit is contained in:
Michael Freno
2026-01-11 16:22:06 -05:00
parent f9484f8a1c
commit 09d2e49225
3 changed files with 63 additions and 29 deletions

View File

@@ -10,8 +10,8 @@ import AppKit
import Combine import Combine
@MainActor @MainActor
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
var timerEngine: TimerEngine? @Published var timerEngine: TimerEngine?
private var settingsManager: SettingsManager? private var settingsManager: SettingsManager?
private var reminderWindowController: NSWindowController? private var reminderWindowController: NSWindowController?
private var settingsWindowController: NSWindowController? private var settingsWindowController: NSWindowController?

View File

@@ -39,8 +39,8 @@ struct GazeApp: App {
// Menu bar extra (always present) // Menu bar extra (always present)
MenuBarExtra("Gaze", systemImage: "eye.fill") { MenuBarExtra("Gaze", systemImage: "eye.fill") {
MenuBarContentView( MenuBarContentWrapper(
timerEngine: appDelegate.timerEngine, appDelegate: appDelegate,
settingsManager: settingsManager, settingsManager: settingsManager,
onQuit: { NSApplication.shared.terminate(nil) }, onQuit: { NSApplication.shared.terminate(nil) },
onOpenSettings: { appDelegate.openSettings() }, onOpenSettings: { appDelegate.openSettings() },

View File

@@ -7,6 +7,27 @@
import SwiftUI import SwiftUI
// Wrapper to properly observe AppDelegate changes in MenuBarExtra
struct MenuBarContentWrapper: View {
@ObservedObject var appDelegate: AppDelegate
@ObservedObject var settingsManager: SettingsManager
var onQuit: () -> Void
var onOpenSettings: () -> Void
var onOpenSettingsTab: (Int) -> Void
var onOpenOnboarding: () -> Void
var body: some View {
MenuBarContentView(
timerEngine: appDelegate.timerEngine,
settingsManager: settingsManager,
onQuit: onQuit,
onOpenSettings: onOpenSettings,
onOpenSettingsTab: onOpenSettingsTab,
onOpenOnboarding: onOpenOnboarding
)
}
}
// Hover button style for menubar items // Hover button style for menubar items
struct MenuBarButtonStyle: ButtonStyle { struct MenuBarButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
@@ -29,8 +50,11 @@ struct MenuBarHoverButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
configuration.label configuration.label
.foregroundColor(isHovered ? .white : .primary)
.glassEffectIfAvailable( .glassEffectIfAvailable(
isHovered ? GlassStyle.regular.tint(.accentColor.opacity(0.5)).interactive() : GlassStyle.regular, isHovered
? GlassStyle.regular.tint(.accentColor).interactive()
: GlassStyle.regular,
in: .rect(cornerRadius: 6) in: .rect(cornerRadius: 6)
) )
.contentShape(Rectangle()) .contentShape(Rectangle())
@@ -164,10 +188,10 @@ struct MenuBarContentView: View {
.padding(.top, 8) .padding(.top, 8)
ForEach(TimerType.allCases) { timerType in ForEach(TimerType.allCases) { timerType in
if let state = timerEngine.timerStates[timerType] { if timerEngine.timerStates[timerType] != nil {
TimerStatusRow( TimerStatusRow(
type: timerType, type: timerType,
state: state, timerEngine: timerEngine,
onSkip: { onSkip: {
timerEngine.skipNext(type: timerType) timerEngine.skipNext(type: timerType)
}, },
@@ -275,7 +299,7 @@ struct MenuBarContentView: View {
struct TimerStatusRow: View { struct TimerStatusRow: View {
let type: TimerType let type: TimerType
let state: TimerState @ObservedObject var timerEngine: TimerEngine
var onSkip: () -> Void var onSkip: () -> Void
var onDevTrigger: (() -> Void)? = nil var onDevTrigger: (() -> Void)? = nil
var onTap: (() -> Void)? = nil var onTap: (() -> Void)? = nil
@@ -283,21 +307,28 @@ struct TimerStatusRow: View {
@State private var isHoveredDevTrigger = false @State private var isHoveredDevTrigger = false
@State private var isHoveredBody = false @State private var isHoveredBody = false
private var state: TimerState? {
timerEngine.timerStates[type]
}
var body: some View { var body: some View {
HStack { HStack {
HStack { HStack {
Image(systemName: type.iconName) Image(systemName: type.iconName)
.foregroundColor(iconColor) .foregroundColor(isHoveredBody ? .white : iconColor)
.frame(width: 20) .frame(width: 20)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(type.displayName) Text(type.displayName)
.font(.subheadline) .font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
Text(timeRemaining) .foregroundColor(isHoveredBody ? .white : .primary)
.font(.caption) if let state = state {
.foregroundColor(.secondary) Text(timeRemaining(state))
.monospacedDigit() .font(.caption)
.foregroundColor(isHoveredBody ? .white.opacity(0.8) : .secondary)
.monospacedDigit()
}
} }
Spacer() Spacer()
@@ -312,13 +343,14 @@ struct TimerStatusRow: View {
Button(action: onDevTrigger) { Button(action: onDevTrigger) {
Image(systemName: "bolt.fill") Image(systemName: "bolt.fill")
.font(.caption) .font(.caption)
.foregroundColor(.yellow) .foregroundColor(isHoveredDevTrigger ? .white : .yellow)
.padding(6) .padding(6)
.contentShape(Circle()) .contentShape(Circle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.glassEffectIfAvailable( .glassEffectIfAvailable(
isHoveredDevTrigger ? GlassStyle.regular.tint(.yellow.opacity(0.5)) : GlassStyle.regular, isHoveredDevTrigger
? GlassStyle.regular.tint(.yellow) : GlassStyle.regular,
in: .circle in: .circle
) )
.help("Trigger \(type.displayName) reminder now (dev)") .help("Trigger \(type.displayName) reminder now (dev)")
@@ -331,13 +363,14 @@ struct TimerStatusRow: View {
Button(action: onSkip) { Button(action: onSkip) {
Image(systemName: "forward.fill") Image(systemName: "forward.fill")
.font(.caption) .font(.caption)
.foregroundColor(.accentColor) .foregroundColor(isHoveredSkip ? .white : .accentColor)
.padding(6) .padding(6)
.contentShape(Circle()) .contentShape(Circle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.glassEffectIfAvailable( .glassEffectIfAvailable(
isHoveredSkip ? GlassStyle.regular.tint(.accentColor.opacity(0.5)) : GlassStyle.regular, isHoveredSkip
? GlassStyle.regular.tint(.accentColor) : GlassStyle.regular,
in: .circle in: .circle
) )
.help("Skip to next \(type.displayName) reminder") .help("Skip to next \(type.displayName) reminder")
@@ -348,7 +381,7 @@ struct TimerStatusRow: View {
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 6) .padding(.vertical, 6)
.glassEffectIfAvailable( .glassEffectIfAvailable(
isHoveredBody ? GlassStyle.regular.tint(.accentColor.opacity(0.5)) : GlassStyle.regular, isHoveredBody ? GlassStyle.regular.tint(.accentColor) : GlassStyle.regular,
in: .rect(cornerRadius: 6) in: .rect(cornerRadius: 6)
) )
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -370,7 +403,7 @@ struct TimerStatusRow: View {
} }
} }
private var timeRemaining: String { private func timeRemaining(_ state: TimerState) -> String {
let seconds = state.remainingSeconds let seconds = state.remainingSeconds
let minutes = seconds / 60 let minutes = seconds / 60
let remainingSeconds = seconds % 60 let remainingSeconds = seconds % 60
@@ -396,21 +429,21 @@ struct InactiveTimerRow: View {
Button(action: onTap) { Button(action: onTap) {
HStack { HStack {
Image(systemName: type.iconName) Image(systemName: type.iconName)
.foregroundColor(.secondary) .foregroundColor(isHovered ? .white : .secondary)
.frame(width: 20) .frame(width: 20)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(type.displayName) Text(type.displayName)
.font(.subheadline) .font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(isHovered ? .white : .secondary)
} }
Spacer() Spacer()
Image(systemName: "plus.circle") Image(systemName: "plus.circle")
.font(.title3) .font(.title3)
.foregroundColor(.accentColor) .foregroundColor(isHovered ? .white : .accentColor)
.padding(6) .padding(6)
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -419,7 +452,7 @@ struct InactiveTimerRow: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.glassEffectIfAvailable( .glassEffectIfAvailable(
isHovered ? GlassStyle.regular.tint(.accentColor.opacity(0.5)) : GlassStyle.regular, isHovered ? GlassStyle.regular.tint(.accentColor) : GlassStyle.regular,
in: .rect(cornerRadius: 6) in: .rect(cornerRadius: 6)
) )
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -440,28 +473,29 @@ struct UserTimerStatusRow: View {
Button(action: onTap) { Button(action: onTap) {
HStack { HStack {
Circle() Circle()
.fill(timer.color) .fill(isHovered ? .white : timer.color)
.frame(width: 8, height: 8) .frame(width: 8, height: 8)
Image(systemName: "clock.fill") Image(systemName: "clock.fill")
.foregroundColor(timer.color) .foregroundColor(isHovered ? .white : timer.color)
.frame(width: 20) .frame(width: 20)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(timer.title) Text(timer.title)
.font(.subheadline) .font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(isHovered ? .white : .primary)
.lineLimit(1) .lineLimit(1)
if let state = state { if let state = state {
Text(timeRemaining(state)) Text(timeRemaining(state))
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(isHovered ? .white.opacity(0.8) : .secondary)
.monospacedDigit() .monospacedDigit()
} else { } else {
Text(timer.enabled ? "Not active" : "Disabled") Text(timer.enabled ? "Not active" : "Disabled")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(isHovered ? .white.opacity(0.8) : .secondary)
} }
} }
@@ -469,7 +503,7 @@ struct UserTimerStatusRow: View {
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle") Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(isHovered ? .white : .secondary)
.padding(6) .padding(6)
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -478,7 +512,7 @@ struct UserTimerStatusRow: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.glassEffectIfAvailable( .glassEffectIfAvailable(
isHovered ? GlassStyle.regular.tint(timer.color.opacity(0.3)) : GlassStyle.regular, isHovered ? GlassStyle.regular.tint(timer.color) : GlassStyle.regular,
in: .rect(cornerRadius: 6) in: .rect(cornerRadius: 6)
) )
.padding(.horizontal, 8) .padding(.horizontal, 8)