general: refinements to ui, run, and menubar settings triggers

This commit is contained in:
Michael Freno
2026-01-08 23:38:12 -05:00
parent acc84bd86d
commit f243011a23
8 changed files with 189 additions and 82 deletions

View File

@@ -187,7 +187,7 @@ private func showReminderWindow(_ content: AnyView) {
} }
// Public method to open settings window // Public method to open settings window
func openSettings() { func openSettings(tab: Int = 0) {
// If window already exists, just bring it to front // If window already exists, just bring it to front
if let existingWindow = settingsWindowController?.window { if let existingWindow = settingsWindowController?.window {
existingWindow.makeKeyAndOrderFront(nil) existingWindow.makeKeyAndOrderFront(nil)
@@ -207,7 +207,7 @@ private func showReminderWindow(_ content: AnyView) {
window.setFrameAutosaveName("SettingsWindow") window.setFrameAutosaveName("SettingsWindow")
window.isReleasedWhenClosed = false window.isReleasedWhenClosed = false
window.contentView = NSHostingView( window.contentView = NSHostingView(
rootView: SettingsWindowView(settingsManager: settingsManager!) rootView: SettingsWindowView(settingsManager: settingsManager!, initialTab: tab)
) )
let windowController = NSWindowController(window: window) let windowController = NSWindowController(window: window)

View File

@@ -42,7 +42,8 @@ struct GazeApp: App {
timerEngine: timerEngine, timerEngine: timerEngine,
settingsManager: settingsManager, settingsManager: settingsManager,
onQuit: { NSApplication.shared.terminate(nil) }, onQuit: { NSApplication.shared.terminate(nil) },
onOpenSettings: { appDelegate.openSettings() } onOpenSettings: { appDelegate.openSettings() },
onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) }
) )
} }
} }

View File

@@ -35,4 +35,26 @@ enum TimerType: String, Codable, CaseIterable, Identifiable {
return "figure.stand" return "figure.stand"
} }
} }
var tabIndex: Int {
switch self {
case .lookAway:
return 0
case .blink:
return 1
case .posture:
return 2
}
}
var tooltipText: String {
switch self {
case .lookAway:
return "Full screen reminder"
case .blink:
return "Subtle reminder"
case .posture:
return "Subtle reminder"
}
}
} }

View File

@@ -48,6 +48,7 @@ struct MenuBarContentView: View {
@ObservedObject var settingsManager: SettingsManager @ObservedObject var settingsManager: SettingsManager
var onQuit: () -> Void var onQuit: () -> Void
var onOpenSettings: () -> Void var onOpenSettings: () -> Void
var onOpenSettingsTab: (Int) -> Void
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@@ -65,33 +66,41 @@ struct MenuBarContentView: View {
Divider() Divider()
// Timer Status // Timer Status
if !timerEngine.timerStates.isEmpty { VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) { Text("Active Timers")
Text("Active Timers") .font(.caption)
.font(.caption) .foregroundColor(.secondary)
.foregroundColor(.secondary) .padding(.horizontal)
.padding(.horizontal) .padding(.top, 8)
.padding(.top, 8)
ForEach(TimerType.allCases) { timerType in ForEach(TimerType.allCases) { timerType in
if let state = timerEngine.timerStates[timerType] { if let state = timerEngine.timerStates[timerType] {
TimerStatusRow( TimerStatusRow(
type: timerType, type: timerType,
state: state, state: state,
onSkip: { onSkip: {
timerEngine.skipNext(type: timerType) timerEngine.skipNext(type: timerType)
}, },
onDevTrigger: { onDevTrigger: {
timerEngine.triggerReminder(for: timerType) timerEngine.triggerReminder(for: timerType)
} },
) onTap: {
} onOpenSettingsTab(timerType.tabIndex)
}
)
} else {
InactiveTimerRow(
type: timerType,
onTap: {
onOpenSettingsTab(timerType.tabIndex)
}
)
} }
} }
.padding(.bottom, 8)
Divider()
} }
.padding(.bottom, 8)
Divider()
// Controls // Controls
VStack(spacing: 4) { VStack(spacing: 4) {
@@ -158,74 +167,85 @@ struct TimerStatusRow: View {
let state: TimerState let state: TimerState
var onSkip: () -> Void var onSkip: () -> Void
var onDevTrigger: (() -> Void)? = nil var onDevTrigger: (() -> Void)? = nil
var onTap: (() -> Void)? = nil
@State private var isHoveredSkip = false @State private var isHoveredSkip = false
@State private var isHoveredDevTrigger = false @State private var isHoveredDevTrigger = false
@State private var isHoveredBody = false @State private var isHoveredBody = false
var body: some View { var body: some View {
HStack { Button(action: {
Image(systemName: type.iconName) onTap?()
.foregroundColor(iconColor) }) {
.frame(width: 20) HStack {
Image(systemName: type.iconName)
.foregroundColor(iconColor)
.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) Text(timeRemaining)
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.monospacedDigit() .monospacedDigit()
}
Spacer()
#if DEBUG
if let onDevTrigger = onDevTrigger {
Button(action: onDevTrigger) {
Image(systemName: "bolt.fill")
.font(.caption)
.foregroundColor(.yellow)
.padding(6)
}
.buttonStyle(.plain)
.glassEffect(
isHoveredDevTrigger ? .regular.tint(.yellow) : .regular,
in: .circle
)
.help("Trigger \(type.displayName) reminder now (dev)")
.onHover { hovering in
isHoveredDevTrigger = hovering
}
} }
#endif
Button(action: onSkip) { Spacer()
Image(systemName: "forward.fill")
.font(.caption) #if DEBUG
.foregroundColor(.accentColor) if let onDevTrigger = onDevTrigger {
.padding(6) Button(action: onDevTrigger) {
} Image(systemName: "bolt.fill")
.buttonStyle(.plain) .font(.caption)
.glassEffect( .foregroundColor(.yellow)
isHoveredSkip ? .regular.tint(.accentColor) : .regular, .padding(6)
in: .circle }
) .buttonStyle(.plain)
.help("Skip to next \(type.displayName) reminder") .glassEffect(
.onHover { hovering in isHoveredDevTrigger ? .regular.tint(.yellow) : .regular,
isHoveredSkip = hovering in: .circle
)
.help("Trigger \(type.displayName) reminder now (dev)")
.onHover { hovering in
isHoveredDevTrigger = hovering
}
}
#endif
Button(action: onSkip) {
Image(systemName: "forward.fill")
.font(.caption)
.foregroundColor(.accentColor)
.padding(6)
}
.buttonStyle(.plain)
.glassEffect(
isHoveredSkip ? .regular.tint(.accentColor.opacity(0.5)) : .regular,
in: .circle
)
.help("Skip to next \(type.displayName) reminder")
.onHover { hovering in
isHoveredSkip = hovering
}
} }
.padding(.horizontal, 8)
.padding(.vertical, 6)
} }
.padding(.horizontal, 8) .buttonStyle(.plain)
.padding(.vertical, 6)
.glassEffect( .glassEffect(
isHoveredBody ? .regular.tint(.accentColor) : .regular, isHoveredBody ? .regular.tint(.accentColor.opacity(0.5)) : .regular,
in: .rect(cornerRadius: 6) in: .rect(cornerRadius: 6)
) )
.padding(.horizontal, 8) .padding(.horizontal, 8)
.onHover { hovering in .onHover { hovering in
isHoveredBody = hovering isHoveredBody = hovering
} }
.help(tooltipText)
}
private var tooltipText: String {
type.tooltipText
} }
private var iconColor: Color { private var iconColor: Color {
@@ -253,6 +273,48 @@ struct TimerStatusRow: View {
} }
} }
struct InactiveTimerRow: View {
let type: TimerType
var onTap: () -> Void
@State private var isHovered = false
var body: some View {
Button(action: onTap) {
HStack {
Image(systemName: type.iconName)
.foregroundColor(.secondary)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text(type.displayName)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "plus.circle")
.font(.title3)
.foregroundColor(.accentColor)
.padding(6)
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(.plain)
.glassEffect(
isHovered ? .regular.tint(.accentColor.opacity(0.5)) : .regular,
in: .rect(cornerRadius: 6)
)
.padding(.horizontal, 8)
.onHover { hovering in
isHovered = hovering
}
.help("Enable \(type.displayName) reminders")
}
}
#Preview("Menu Bar Content") { #Preview("Menu Bar Content") {
let settingsManager = SettingsManager.shared let settingsManager = SettingsManager.shared
let timerEngine = TimerEngine(settingsManager: settingsManager) let timerEngine = TimerEngine(settingsManager: settingsManager)
@@ -260,6 +322,7 @@ struct TimerStatusRow: View {
timerEngine: timerEngine, timerEngine: timerEngine,
settingsManager: settingsManager, settingsManager: settingsManager,
onQuit: {}, onQuit: {},
onOpenSettings: {} onOpenSettings: {},
onOpenSettingsTab: { _ in }
) )
} }

View File

@@ -25,7 +25,7 @@ struct OnboardingContainerView: View {
@State private var lookAwayEnabled = true @State private var lookAwayEnabled = true
@State private var lookAwayIntervalMinutes = 20 @State private var lookAwayIntervalMinutes = 20
@State private var lookAwayCountdownSeconds = 20 @State private var lookAwayCountdownSeconds = 20
@State private var blinkEnabled = true @State private var blinkEnabled = false
@State private var blinkIntervalMinutes = 5 @State private var blinkIntervalMinutes = 5
@State private var postureEnabled = true @State private var postureEnabled = true
@State private var postureIntervalMinutes = 30 @State private var postureIntervalMinutes = 30

View File

@@ -41,7 +41,7 @@ struct LookAwayReminderView: View {
LottieView( LottieView(
animationName: AnimationAsset.lookAway.fileName, animationName: AnimationAsset.lookAway.fileName,
loopMode: .loop, loopMode: .loop,
animationSpeed: 1.0 animationSpeed: 0.75
) )
.frame(width: 200, height: 200) .frame(width: 200, height: 200)
.padding(.vertical, 30) .padding(.vertical, 30)

View File

@@ -9,7 +9,7 @@ import SwiftUI
struct SettingsWindowView: View { struct SettingsWindowView: View {
@ObservedObject var settingsManager: SettingsManager @ObservedObject var settingsManager: SettingsManager
@State private var currentTab = 0 @State private var currentTab: Int
@State private var lookAwayEnabled: Bool @State private var lookAwayEnabled: Bool
@State private var lookAwayIntervalMinutes: Int @State private var lookAwayIntervalMinutes: Int
@State private var lookAwayCountdownSeconds: Int @State private var lookAwayCountdownSeconds: Int
@@ -19,9 +19,10 @@ struct SettingsWindowView: View {
@State private var postureIntervalMinutes: Int @State private var postureIntervalMinutes: Int
@State private var launchAtLogin: Bool @State private var launchAtLogin: Bool
init(settingsManager: SettingsManager) { init(settingsManager: SettingsManager, initialTab: Int = 0) {
self.settingsManager = settingsManager self.settingsManager = settingsManager
_currentTab = State(initialValue: initialTab)
_lookAwayEnabled = State(initialValue: settingsManager.settings.lookAwayTimer.enabled) _lookAwayEnabled = State(initialValue: settingsManager.settings.lookAwayTimer.enabled)
_lookAwayIntervalMinutes = State(initialValue: settingsManager.settings.lookAwayTimer.intervalSeconds / 60) _lookAwayIntervalMinutes = State(initialValue: settingsManager.settings.lookAwayTimer.intervalSeconds / 60)
_lookAwayCountdownSeconds = State(initialValue: settingsManager.settings.lookAwayCountdownSeconds) _lookAwayCountdownSeconds = State(initialValue: settingsManager.settings.lookAwayCountdownSeconds)

20
run
View File

@@ -8,6 +8,22 @@ ACTION=${1:-run}
VERBOSE=false VERBOSE=false
OUTPUT_FILE="" OUTPUT_FILE=""
# Function to kill any existing Gaze processes
kill_existing_gaze_processes() {
echo "🔍 Checking for existing Gaze processes..."
# Find and kill any running Gaze processes
pids=$(pgrep -f "Gaze.app")
if [ -n "$pids" ]; then
echo "🛑 Killing existing Gaze processes (PID(s): $pids)..."
kill $pids 2>/dev/null
# Wait a moment for processes to terminate
sleep 1
else
echo "✅ No existing Gaze processes found"
fi
}
# Parse command line arguments # Parse command line arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
@@ -72,6 +88,10 @@ elif [ "$ACTION" = "test" ]; then
elif [ "$ACTION" = "run" ]; then elif [ "$ACTION" = "run" ]; then
echo "Building and running Gaze application..." echo "Building and running Gaze application..."
# Kill any existing Gaze processes first
kill_existing_gaze_processes
# Always build first, then run # Always build first, then run
run_with_output "xcodebuild -project Gaze.xcodeproj -scheme Gaze -configuration Debug build" run_with_output "xcodebuild -project Gaze.xcodeproj -scheme Gaze -configuration Debug build"