general: refinements to ui, run, and menubar settings triggers
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
20
run
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user