diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index c1220c4..fb9d40b 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -188,6 +188,16 @@ private func showReminderWindow(_ content: AnyView) { // Public method to open settings window func openSettings(tab: Int = 0) { + // Post notification to close menu bar popover + NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil) + + // Small delay to allow menu bar to close before opening settings + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.openSettingsWindow(tab: tab) + } + } + + private func openSettingsWindow(tab: Int) { // If window already exists, switch to the tab and bring it to front if let existingWindow = settingsWindowController?.window { NotificationCenter.default.post( diff --git a/Gaze/Models/UserTimer.swift b/Gaze/Models/UserTimer.swift index 62f1b41..60a94dd 100644 --- a/Gaze/Models/UserTimer.swift +++ b/Gaze/Models/UserTimer.swift @@ -6,29 +6,98 @@ // import Foundation +import SwiftUI /// Represents a user-defined timer with customizable properties struct UserTimer: Codable, Equatable, Identifiable { let id: String + var title: String var type: UserTimerType var timeOnScreenSeconds: Int var message: String? + var colorHex: String + var enabled: Bool init( id: String = UUID().uuidString, + title: String? = nil, type: UserTimerType = .subtle, timeOnScreenSeconds: Int = 30, - message: String? = nil + message: String? = nil, + colorHex: String? = nil, + enabled: Bool = true ) { self.id = id + self.title = title ?? "User Reminder" self.type = type self.timeOnScreenSeconds = timeOnScreenSeconds self.message = message + self.colorHex = colorHex ?? UserTimer.defaultColors[0] + self.enabled = enabled } static func == (lhs: UserTimer, rhs: UserTimer) -> Bool { - lhs.id == rhs.id && lhs.type == rhs.type + lhs.id == rhs.id && lhs.title == rhs.title && lhs.type == rhs.type && lhs.timeOnScreenSeconds == rhs.timeOnScreenSeconds && lhs.message == rhs.message + && lhs.colorHex == rhs.colorHex && lhs.enabled == rhs.enabled + } + + // Default color palette for user timers + static let defaultColors = [ + "9B59B6", // Purple + "3498DB", // Blue + "E74C3C", // Red + "2ECC71", // Green + "F39C12", // Orange + "1ABC9C", // Teal + "E91E63", // Pink + "FF5722" // Deep Orange + ] + + var color: Color { + Color(hex: colorHex) ?? .purple + } + + static func generateTitle(for index: Int) -> String { + "User Reminder \(index + 1)" + } +} + +extension Color { + init?(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + + guard Scanner(string: hex).scanHexInt64(&int) else { return nil } + + let r, g, b: UInt64 + switch hex.count { + case 6: // RGB (24-bit) + (r, g, b) = ((int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF) + default: + return nil + } + + self.init( + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255 + ) + } + + var hexString: String? { + guard let components = NSColor(self).cgColor.components, components.count >= 3 else { + return nil + } + + let r = Float(components[0]) + let g = Float(components[1]) + let b = Float(components[2]) + + return String(format: "%02lX%02lX%02lX", + lroundf(r * 255), + lroundf(g * 255), + lroundf(b * 255)) } } diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index 18c697d..8336fed 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -46,6 +46,7 @@ struct MenuBarHoverButtonStyle: ButtonStyle { struct MenuBarContentView: View { @ObservedObject var timerEngine: TimerEngine @ObservedObject var settingsManager: SettingsManager + @Environment(\.dismiss) private var dismiss var onQuit: () -> Void var onOpenSettings: () -> Void var onOpenSettingsTab: (Int) -> Void @@ -98,8 +99,8 @@ struct MenuBarContentView: View { } } - // Show user timers if any exist - ForEach(settingsManager.settings.userTimers, id: \.id) { userTimer in + // Show user timers if any exist and are enabled + ForEach(settingsManager.settings.userTimers.filter { $0.enabled }, id: \.id) { userTimer in UserTimerStatusRow( timer: userTimer, state: nil, // We'll implement proper state tracking later @@ -166,6 +167,9 @@ struct MenuBarContentView: View { .padding(.vertical, 8) } .frame(width: 300) + .onReceive(NotificationCenter.default.publisher(for: Notification.Name("CloseMenuBarPopover"))) { _ in + dismiss() + } } private var isPaused: Bool { @@ -336,12 +340,16 @@ struct UserTimerStatusRow: View { var body: some View { Button(action: onTap) { HStack { + Circle() + .fill(timer.color) + .frame(width: 8, height: 8) + Image(systemName: "clock.fill") - .foregroundColor(.purple) + .foregroundColor(timer.color) .frame(width: 20) VStack(alignment: .leading, spacing: 2) { - Text(timer.message ?? "Custom Timer") + Text(timer.title) .font(.subheadline) .fontWeight(.medium) .lineLimit(1) @@ -352,7 +360,7 @@ struct UserTimerStatusRow: View { .foregroundColor(.secondary) .monospacedDigit() } else { - Text("Not active") + Text(timer.enabled ? "Not active" : "Disabled") .font(.caption) .foregroundColor(.secondary) } @@ -370,7 +378,7 @@ struct UserTimerStatusRow: View { } .buttonStyle(.plain) .glassEffect( - isHovered ? .regular.tint(.purple.opacity(0.5)) : .regular, + isHovered ? .regular.tint(timer.color.opacity(0.3)) : .regular, in: .rect(cornerRadius: 6) ) .padding(.horizontal, 8) @@ -383,7 +391,8 @@ struct UserTimerStatusRow: View { private var tooltipText: String { let typeText = timer.type == .subtle ? "Subtle" : "Overlay" let durationText = "\(timer.timeOnScreenSeconds)s on screen" - return "\(typeText) timer - \(durationText)" + let statusText = timer.enabled ? "" : " (Disabled)" + return "\(typeText) timer - \(durationText)\(statusText)" } private func timeRemaining(_ state: TimerState) -> String { diff --git a/Gaze/Views/Onboarding/BlinkSetupView.swift b/Gaze/Views/Onboarding/BlinkSetupView.swift index e35449f..5da8ed1 100644 --- a/Gaze/Views/Onboarding/BlinkSetupView.swift +++ b/Gaze/Views/Onboarding/BlinkSetupView.swift @@ -12,84 +12,94 @@ struct BlinkSetupView: View { @Binding var intervalMinutes: Int var body: some View { - VStack(spacing: 30) { - Image(systemName: "eye.circle") - .font(.system(size: 60)) - .foregroundColor(.green) + VStack(spacing: 0) { + // Fixed header section + VStack(spacing: 16) { + Image(systemName: "eye.circle") + .font(.system(size: 60)) + .foregroundColor(.green) - Text("Blink Reminder") - .font(.system(size: 28, weight: .bold)) - - Text("Keep your eyes hydrated") - .font(.title3) - .foregroundColor(.secondary) - - // InfoBox with link functionality - HStack(spacing: 12) { - Button(action: { - if let url = URL( - string: "https://www.healthline.com/health/eye-health/eye-strain#symptoms") - { - #if os(iOS) - UIApplication.shared.open(url) - #elseif os(macOS) - NSWorkspace.shared.open(url) - #endif - } - }) { - Image(systemName: "info.circle") - .foregroundColor(.white) - }.buttonStyle(.plain) - Text( - "We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes" - ) - .font(.headline) - .foregroundColor(.white) + Text("Blink Reminder") + .font(.system(size: 28, weight: .bold)) } - .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .padding(.top, 20) + .padding(.bottom, 30) - VStack(alignment: .leading, spacing: 20) { - Toggle("Enable Blink Reminders", isOn: $enabled) + // Vertically centered content + Spacer() + + VStack(spacing: 30) { + Text("Keep your eyes hydrated") + .font(.title3) + .foregroundColor(.secondary) + + // InfoBox with link functionality + HStack(spacing: 12) { + Button(action: { + if let url = URL( + string: "https://www.healthline.com/health/eye-health/eye-strain#symptoms") + { + #if os(iOS) + UIApplication.shared.open(url) + #elseif os(macOS) + NSWorkspace.shared.open(url) + #endif + } + }) { + Image(systemName: "info.circle") + .foregroundColor(.white) + }.buttonStyle(.plain) + Text( + "We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes" + ) .font(.headline) + .foregroundColor(.white) + } + .padding() + .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) - if enabled { - VStack(alignment: .leading, spacing: 12) { - Text("Remind me every:") - .font(.subheadline) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 20) { + Toggle("Enable Blink Reminders", isOn: $enabled) + .font(.headline) - HStack { - Slider( - value: Binding( - get: { Double(intervalMinutes) }, - set: { intervalMinutes = Int($0) } - ), in: 1...15, step: 1) + if enabled { + VStack(alignment: .leading, spacing: 12) { + Text("Remind me every:") + .font(.subheadline) + .foregroundColor(.secondary) - Text("\(intervalMinutes) min") - .frame(width: 60, alignment: .trailing) - .monospacedDigit() + HStack { + Slider( + value: Binding( + get: { Double(intervalMinutes) }, + set: { intervalMinutes = Int($0) } + ), in: 1...15, step: 1) + + Text("\(intervalMinutes) min") + .frame(width: 60, alignment: .trailing) + .monospacedDigit() + } } } } - } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .padding() + .glassEffect(.regular, in: .rect(cornerRadius: 12)) - if enabled { - Text( - "You will be subtly reminded every \(intervalMinutes) minutes to blink" - ) - .font(.subheadline) - .foregroundColor(.secondary) - } else { - Text( - "Blink reminders are currently disabled." - ) - .font(.caption) - .foregroundColor(.secondary) + if enabled { + Text( + "You will be subtly reminded every \(intervalMinutes) minutes to blink" + ) + .font(.subheadline) + .foregroundColor(.secondary) + } else { + Text( + "Blink reminders are currently disabled." + ) + .font(.caption) + .foregroundColor(.secondary) + } } - + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Gaze/Views/Onboarding/LookAwaySetupView.swift b/Gaze/Views/Onboarding/LookAwaySetupView.swift index c41445d..ce284d0 100644 --- a/Gaze/Views/Onboarding/LookAwaySetupView.swift +++ b/Gaze/Views/Onboarding/LookAwaySetupView.swift @@ -19,95 +19,105 @@ struct LookAwaySetupView: View { @Binding var countdownSeconds: Int var body: some View { - VStack(spacing: 30) { - Image(systemName: "eye.fill") - .font(.system(size: 60)) - .foregroundColor(.accentColor) + VStack(spacing: 0) { + // Fixed header section + VStack(spacing: 16) { + Image(systemName: "eye.fill") + .font(.system(size: 60)) + .foregroundColor(.accentColor) - Text("Look Away Reminder") - .font(.system(size: 28, weight: .bold)) - - // InfoBox with link functionality - HStack(spacing: 12) { - Button(action: { - if let url = URL( - string: "https://www.healthline.com/health/eye-health/20-20-20-rule") - { - #if os(iOS) - UIApplication.shared.open(url) - #elseif os(macOS) - NSWorkspace.shared.open(url) - #endif - } - }) { - Image(systemName: "info.circle") - .foregroundColor(.white) - }.buttonStyle(.plain) - Text("Suggested: 20-20-20 rule") - .font(.headline) - .foregroundColor(.white) + Text("Look Away Reminder") + .font(.system(size: 28, weight: .bold)) } - .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .padding(.top, 20) + .padding(.bottom, 30) - VStack(alignment: .leading, spacing: 20) { - Toggle("Enable Look Away Reminders", isOn: $enabled) - .font(.headline) - - if enabled { - VStack(alignment: .leading, spacing: 12) { - Text("Remind me every:") - .font(.subheadline) - .foregroundColor(.secondary) - - HStack { - Slider( - value: Binding( - get: { Double(intervalMinutes) }, - set: { intervalMinutes = Int($0) } - ), in: 5...60, step: 5) - - Text("\(intervalMinutes) min") - .frame(width: 60, alignment: .trailing) - .monospacedDigit() + // Vertically centered content + Spacer() + + VStack(spacing: 30) { + // InfoBox with link functionality + HStack(spacing: 12) { + Button(action: { + if let url = URL( + string: "https://www.healthline.com/health/eye-health/20-20-20-rule") + { + #if os(iOS) + UIApplication.shared.open(url) + #elseif os(macOS) + NSWorkspace.shared.open(url) + #endif } + }) { + Image(systemName: "info.circle") + .foregroundColor(.white) + }.buttonStyle(.plain) + Text("Suggested: 20-20-20 rule") + .font(.headline) + .foregroundColor(.white) + } + .padding() + .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) - Text("Look away for:") - .font(.subheadline) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 20) { + Toggle("Enable Look Away Reminders", isOn: $enabled) + .font(.headline) - HStack { - Slider( - value: Binding( - get: { Double(countdownSeconds) }, - set: { countdownSeconds = Int($0) } - ), in: 10...30, step: 5) + if enabled { + VStack(alignment: .leading, spacing: 12) { + Text("Remind me every:") + .font(.subheadline) + .foregroundColor(.secondary) - Text("\(countdownSeconds) sec") - .frame(width: 60, alignment: .trailing) - .monospacedDigit() + HStack { + Slider( + value: Binding( + get: { Double(intervalMinutes) }, + set: { intervalMinutes = Int($0) } + ), in: 5...60, step: 5) + + Text("\(intervalMinutes) min") + .frame(width: 60, alignment: .trailing) + .monospacedDigit() + } + + Text("Look away for:") + .font(.subheadline) + .foregroundColor(.secondary) + + HStack { + Slider( + value: Binding( + get: { Double(countdownSeconds) }, + set: { countdownSeconds = Int($0) } + ), in: 10...30, step: 5) + + Text("\(countdownSeconds) sec") + .frame(width: 60, alignment: .trailing) + .monospacedDigit() + } } } } - } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .padding() + .glassEffect(.regular, in: .rect(cornerRadius: 12)) - if enabled { - Text( - "You will be reminded every \(intervalMinutes) minutes to look in the distance for \(countdownSeconds) seconds" - ) - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } else { - Text( - "Look away reminders are currently disabled." - ) - .font(.caption) - .foregroundColor(.secondary) + if enabled { + Text( + "You will be reminded every \(intervalMinutes) minutes to look in the distance for \(countdownSeconds) seconds" + ) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } else { + Text( + "Look away reminders are currently disabled." + ) + .font(.caption) + .foregroundColor(.secondary) + } } - + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Gaze/Views/Onboarding/PostureSetupView.swift b/Gaze/Views/Onboarding/PostureSetupView.swift index f769233..1d7cf34 100644 --- a/Gaze/Views/Onboarding/PostureSetupView.swift +++ b/Gaze/Views/Onboarding/PostureSetupView.swift @@ -12,84 +12,94 @@ struct PostureSetupView: View { @Binding var intervalMinutes: Int var body: some View { - VStack(spacing: 30) { - Image(systemName: "figure.stand") - .font(.system(size: 60)) - .foregroundColor(.orange) + VStack(spacing: 0) { + // Fixed header section + VStack(spacing: 16) { + Image(systemName: "figure.stand") + .font(.system(size: 60)) + .foregroundColor(.orange) - Text("Posture Reminder") - .font(.system(size: 28, weight: .bold)) - - Text("Maintain proper ergonomics") - .font(.title3) - .foregroundColor(.secondary) - - // InfoBox with link functionality - HStack(spacing: 12) { - Button(action: { - if let url = URL( - string: "https://www.healthline.com/health/ergonomic-workspace") - { - #if os(iOS) - UIApplication.shared.open(url) - #elseif os(macOS) - NSWorkspace.shared.open(url) - #endif - } - }) { - Image(systemName: "info.circle") - .foregroundColor(.white) - }.buttonStyle(.plain) - Text( - "Regular posture checks help prevent back and neck pain from prolonged sitting" - ) - .font(.headline) - .foregroundColor(.white) + Text("Posture Reminder") + .font(.system(size: 28, weight: .bold)) } - .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .padding(.top, 20) + .padding(.bottom, 30) - VStack(alignment: .leading, spacing: 20) { - Toggle("Enable Posture Reminders", isOn: $enabled) + // Vertically centered content + Spacer() + + VStack(spacing: 30) { + Text("Maintain proper ergonomics") + .font(.title3) + .foregroundColor(.secondary) + + // InfoBox with link functionality + HStack(spacing: 12) { + Button(action: { + if let url = URL( + string: "https://www.healthline.com/health/ergonomic-workspace") + { + #if os(iOS) + UIApplication.shared.open(url) + #elseif os(macOS) + NSWorkspace.shared.open(url) + #endif + } + }) { + Image(systemName: "info.circle") + .foregroundColor(.white) + }.buttonStyle(.plain) + Text( + "Regular posture checks help prevent back and neck pain from prolonged sitting" + ) .font(.headline) + .foregroundColor(.white) + } + .padding() + .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) - if enabled { - VStack(alignment: .leading, spacing: 12) { - Text("Remind me every:") - .font(.subheadline) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 20) { + Toggle("Enable Posture Reminders", isOn: $enabled) + .font(.headline) - HStack { - Slider( - value: Binding( - get: { Double(intervalMinutes) }, - set: { intervalMinutes = Int($0) } - ), in: 15...60, step: 5) + if enabled { + VStack(alignment: .leading, spacing: 12) { + Text("Remind me every:") + .font(.subheadline) + .foregroundColor(.secondary) - Text("\(intervalMinutes) min") - .frame(width: 60, alignment: .trailing) - .monospacedDigit() + HStack { + Slider( + value: Binding( + get: { Double(intervalMinutes) }, + set: { intervalMinutes = Int($0) } + ), in: 15...60, step: 5) + + Text("\(intervalMinutes) min") + .frame(width: 60, alignment: .trailing) + .monospacedDigit() + } } } } - } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .padding() + .glassEffect(.regular, in: .rect(cornerRadius: 12)) - if enabled { - Text( - "You will be subtly reminded every \(intervalMinutes) minutes to check your posture" - ) - .font(.subheadline) - .foregroundColor(.secondary) - } else { - Text( - "Posture reminders are currently disabled." - ) - .font(.caption) - .foregroundColor(.secondary) + if enabled { + Text( + "You will be subtly reminded every \(intervalMinutes) minutes to check your posture" + ) + .font(.subheadline) + .foregroundColor(.secondary) + } else { + Text( + "Posture reminders are currently disabled." + ) + .font(.caption) + .foregroundColor(.secondary) + } } - + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Gaze/Views/Onboarding/SettingsOnboardingView.swift b/Gaze/Views/Onboarding/SettingsOnboardingView.swift index 428b42f..3bd4ba5 100644 --- a/Gaze/Views/Onboarding/SettingsOnboardingView.swift +++ b/Gaze/Views/Onboarding/SettingsOnboardingView.swift @@ -13,134 +13,138 @@ struct SettingsOnboardingView: View { var isOnboarding: Bool = true var body: some View { - VStack(spacing: 30) { + VStack(spacing: 0) { + // Fixed header section + VStack(spacing: 16) { + Image(systemName: "gearshape.fill") + .font(.system(size: 60)) + .foregroundColor(.accentColor) + Text(isOnboarding ? "Final Settings" : "General Settings") + .font(.system(size: 28, weight: .bold)) + } + .padding(.top, 20) + .padding(.bottom, 30) + + // Vertically centered content Spacer() + VStack(spacing: 30) { + Text("Configure app preferences and support the project") + .font(.title3) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) - Image(systemName: "gearshape.fill") - .font(.system(size: 80)) - .foregroundColor(.accentColor) + VStack(spacing: 20) { + // Launch at Login Toggle + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Launch at Login") + .font(.headline) + Text("Start Gaze automatically when you log in") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Toggle("", isOn: $launchAtLogin) + .labelsHidden() + .onChange(of: launchAtLogin) { oldValue, newValue in + applyLaunchAtLoginSetting(enabled: newValue) + } + } + .padding() + .glassEffect(.regular, in: .rect(cornerRadius: 12)) - Text(isOnboarding ? "Final Settings" : "General Settings") - .font(.system(size: 36, weight: .bold)) - - Text("Configure app preferences and support the project") - .font(.title3) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - - VStack(spacing: 20) { - // Launch at Login Toggle - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Launch at Login") + // Subtle Reminder Size Configuration + VStack(alignment: .leading, spacing: 12) { + Text("Subtle Reminder Size") .font(.headline) - Text("Start Gaze automatically when you log in") + + Text("Adjust the size of blink and posture reminders") .font(.caption) .foregroundColor(.secondary) - } - Spacer() - Toggle("", isOn: $launchAtLogin) - .labelsHidden() - .onChange(of: launchAtLogin) { oldValue, newValue in - applyLaunchAtLoginSetting(enabled: newValue) - } - } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) - - // Subtle Reminder Size Configuration - VStack(alignment: .leading, spacing: 12) { - Text("Subtle Reminder Size") - .font(.headline) - - Text("Adjust the size of blink and posture reminders") - .font(.caption) - .foregroundColor(.secondary) - - HStack { - Slider( - value: $subtleReminderSizePercentage, - in: 2...35, - step: 1 - ) - Text("\(Int(subtleReminderSizePercentage))%") - .frame(width: 50, alignment: .trailing) - .monospacedDigit() - } - } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) - - // Links Section - VStack(spacing: 12) { - Text("Support & Contribute") - .font(.headline) - .frame(maxWidth: .infinity, alignment: .leading) - - // GitHub Link - Button(action: { - if let url = URL(string: "https://github.com/mikefreno/Gaze") { - NSWorkspace.shared.open(url) - } - }) { + HStack { - Image(systemName: "chevron.left.forwardslash.chevron.right") - .font(.title3) - VStack(alignment: .leading, spacing: 2) { - Text("View on GitHub") - .font(.subheadline) - .fontWeight(.semibold) - Text("Star the repo, report issues, contribute") - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "arrow.up.right") - .font(.caption) + Slider( + value: $subtleReminderSizePercentage, + in: 2...35, + step: 1 + ) + Text("\(Int(subtleReminderSizePercentage))%") + .frame(width: 50, alignment: .trailing) + .monospacedDigit() } - .padding() - .frame(maxWidth: .infinity) } - .buttonStyle(.plain) - .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10)) + .padding() + .glassEffect(.regular, in: .rect(cornerRadius: 12)) - // Buy Me a Coffee - Button(action: { - if let url = URL(string: "https://buymeacoffee.com/placeholder") { - NSWorkspace.shared.open(url) - } - }) { - HStack { - Image(systemName: "cup.and.saucer.fill") - .font(.title3) - .foregroundColor(.orange) - VStack(alignment: .leading, spacing: 2) { - Text("Buy Me a Coffee") - .font(.subheadline) - .fontWeight(.semibold) - Text("Support development of Gaze") - .font(.caption) - .foregroundColor(.secondary) + // Links Section + VStack(spacing: 12) { + Text("Support & Contribute") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + // GitHub Link + Button(action: { + if let url = URL(string: "https://github.com/mikefreno/Gaze") { + NSWorkspace.shared.open(url) } - Spacer() - Image(systemName: "arrow.up.right") - .font(.caption) + }) { + HStack { + Image(systemName: "chevron.left.forwardslash.chevron.right") + .font(.title3) + VStack(alignment: .leading, spacing: 2) { + Text("View on GitHub") + .font(.subheadline) + .fontWeight(.semibold) + Text("Star the repo, report issues, contribute") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "arrow.up.right") + .font(.caption) + } + .padding() + .frame(maxWidth: .infinity) } - .padding() - .frame(maxWidth: .infinity) - .background(Color.orange.opacity(0.1)) - .cornerRadius(10) + .buttonStyle(.plain) + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10)) + + // Buy Me a Coffee + Button(action: { + if let url = URL(string: "https://buymeacoffee.com/placeholder") { + NSWorkspace.shared.open(url) + } + }) { + HStack { + Image(systemName: "cup.and.saucer.fill") + .font(.title3) + .foregroundColor(.orange) + VStack(alignment: .leading, spacing: 2) { + Text("Buy Me a Coffee") + .font(.subheadline) + .fontWeight(.semibold) + Text("Support development of Gaze") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "arrow.up.right") + .font(.caption) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.orange.opacity(0.1)) + .cornerRadius(10) + } + .buttonStyle(.plain) + .glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10)) } - .buttonStyle(.plain) - .glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10)) + .padding() } - .padding() } - Spacer() } - .frame(minWidth: 650, minHeight: 650) + .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() .background(.clear) } diff --git a/Gaze/Views/Onboarding/UserTimersView.swift b/Gaze/Views/Onboarding/UserTimersView.swift index 0e2bbc4..697e0a4 100644 --- a/Gaze/Views/Onboarding/UserTimersView.swift +++ b/Gaze/Views/Onboarding/UserTimersView.swift @@ -13,83 +13,90 @@ struct UserTimersView: View { @State private var showingAddTimer = false var body: some View { - VStack(spacing: 30) { - Image(systemName: "clock.badge.checkmark") - .font(.system(size: 60)) - .foregroundColor(.purple) - - Text("Custom Timers") - .font(.system(size: 28, weight: .bold)) - - Text("Create your own reminder schedules") - .font(.title3) - .foregroundColor(.secondary) - - HStack(spacing: 12) { - Image(systemName: "info.circle") - .foregroundColor(.white) - Text("Add up to 3 custom timers with your own intervals and messages") - .font(.headline) - .foregroundColor(.white) + VStack(spacing: 0) { + // Fixed header section + VStack(spacing: 16) { + Image(systemName: "clock.badge.checkmark") + .font(.system(size: 60)) + .foregroundColor(.purple) + Text("Custom Timers") + .font(.system(size: 28, weight: .bold)) } - .padding() - .glassEffect(.regular.tint(.purple), in: .rect(cornerRadius: 8)) + .padding(.top, 20) + .padding(.bottom, 30) - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Active Timers (\(userTimers.count)/3)") + // Vertically centered content + Spacer() + VStack(spacing: 30) { + Text("Create your own reminder schedules") + .font(.title3) + .foregroundColor(.secondary) + + HStack(spacing: 12) { + Image(systemName: "info.circle") + .foregroundColor(.white) + Text("Add up to 3 custom timers with your own intervals and messages") .font(.headline) - Spacer() - if userTimers.count < 3 { - Button(action: { - showingAddTimer = true - }) { - Label("Add Timer", systemImage: "plus.circle.fill") - } - .buttonStyle(.borderedProminent) - } + .foregroundColor(.white) } + .padding() + .glassEffect(.regular.tint(.purple), in: .rect(cornerRadius: 8)) - if userTimers.isEmpty { - VStack(spacing: 12) { - Image(systemName: "clock.badge.questionmark") - .font(.system(size: 40)) - .foregroundColor(.secondary) - Text("No custom timers yet") - .font(.subheadline) - .foregroundColor(.secondary) - Text("Click 'Add Timer' to create your first custom reminder") - .font(.caption) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Active Timers (\(userTimers.count)/3)") + .font(.headline) + Spacer() + if userTimers.count < 3 { + Button(action: { + showingAddTimer = true + }) { + Label("Add Timer", systemImage: "plus.circle.fill") + } + .buttonStyle(.borderedProminent) + } } - .frame(maxWidth: .infinity) - .padding(40) - } else { - ScrollView { - VStack(spacing: 8) { - ForEach(userTimers) { timer in - UserTimerRow( - timer: timer, - onEdit: { - editingTimer = timer - }, - onDelete: { - if let index = userTimers.firstIndex(where: { - $0.id == timer.id - }) { - userTimers.remove(at: index) + + if userTimers.isEmpty { + VStack(spacing: 12) { + Image(systemName: "clock.badge.questionmark") + .font(.system(size: 40)) + .foregroundColor(.secondary) + Text("No custom timers yet") + .font(.subheadline) + .foregroundColor(.secondary) + Text("Click 'Add Timer' to create your first custom reminder") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(40) + } else { + ScrollView { + VStack(spacing: 8) { + ForEach(Array(userTimers.enumerated()), id: \.element.id) { index, timer in + UserTimerRow( + timer: $userTimers[index], + onEdit: { + editingTimer = timer + }, + onDelete: { + if let idx = userTimers.firstIndex(where: { + $0.id == timer.id + }) { + userTimers.remove(at: idx) + } } - } - ) + ) + } } } + .frame(maxHeight: 200) } - .frame(maxHeight: 200) } + .padding() + .glassEffect(.regular, in: .rect(cornerRadius: 12)) } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) - Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -98,6 +105,7 @@ struct UserTimersView: View { .sheet(isPresented: $showingAddTimer) { UserTimerEditSheet( timer: nil, + existingTimersCount: userTimers.count, onSave: { newTimer in userTimers.append(newTimer) showingAddTimer = false @@ -110,6 +118,7 @@ struct UserTimersView: View { .sheet(item: $editingTimer) { timer in UserTimerEditSheet( timer: timer, + existingTimersCount: userTimers.count, onSave: { updatedTimer in if let index = userTimers.firstIndex(where: { $0.id == timer.id }) { userTimers[index] = updatedTimer @@ -125,19 +134,23 @@ struct UserTimersView: View { } struct UserTimerRow: View { - let timer: UserTimer + @Binding var timer: UserTimer var onEdit: () -> Void var onDelete: () -> Void @State private var isHovered = false var body: some View { HStack(spacing: 12) { + Circle() + .fill(timer.color) + .frame(width: 12, height: 12) + Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle") - .foregroundColor(.purple) + .foregroundColor(timer.color) .frame(width: 24) VStack(alignment: .leading, spacing: 4) { - Text(timer.message ?? "Custom Timer") + Text(timer.title) .font(.subheadline) .fontWeight(.medium) .lineLimit(1) @@ -148,16 +161,21 @@ struct UserTimerRow: View { Spacer() - HStack(spacing: 4) { + HStack(spacing: 8) { + Toggle("", isOn: $timer.enabled) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) + Button(action: onEdit) { - Image(systemName: "pencil.circle") + Image(systemName: "pencil.circle.fill") .font(.title3) .foregroundColor(.accentColor) } .buttonStyle(.plain) Button(action: onDelete) { - Image(systemName: "trash.circle") + Image(systemName: "trash.circle.fill") .font(.title3) .foregroundColor(.red) } @@ -177,25 +195,32 @@ struct UserTimerRow: View { struct UserTimerEditSheet: View { let timer: UserTimer? + let existingTimersCount: Int var onSave: (UserTimer) -> Void var onCancel: () -> Void + @State private var title: String @State private var message: String @State private var type: UserTimerType @State private var timeOnScreen: Int + @State private var selectedColorHex: String init( timer: UserTimer?, + existingTimersCount: Int = 0, onSave: @escaping (UserTimer) -> Void, onCancel: @escaping () -> Void ) { self.timer = timer + self.existingTimersCount = existingTimersCount self.onSave = onSave self.onCancel = onCancel + _title = State(initialValue: timer?.title ?? UserTimer.generateTitle(for: existingTimersCount)) _message = State(initialValue: timer?.message ?? "") _type = State(initialValue: timer?.type ?? .subtle) _timeOnScreen = State(initialValue: timer?.timeOnScreenSeconds ?? 30) + _selectedColorHex = State(initialValue: timer?.colorHex ?? UserTimer.defaultColors[existingTimersCount % UserTimer.defaultColors.count]) } var body: some View { @@ -205,6 +230,39 @@ struct UserTimerEditSheet: View { .fontWeight(.bold) VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Title") + .font(.headline) + TextField("Timer title", text: $title) + .textFieldStyle(.roundedBorder) + Text("Example: \"Stretch Break\", \"Eye Rest\", \"Water Break\"") + .font(.caption) + .foregroundColor(.secondary) + } + + VStack(alignment: .leading, spacing: 8) { + Text("Color") + .font(.headline) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 8), spacing: 12) { + ForEach(UserTimer.defaultColors, id: \.self) { colorHex in + Button(action: { + selectedColorHex = colorHex + }) { + Circle() + .fill(Color(hex: colorHex) ?? .purple) + .frame(width: 32, height: 32) + .overlay( + Circle() + .strokeBorder(Color.white, lineWidth: selectedColorHex == colorHex ? 3 : 0) + ) + .shadow(color: selectedColorHex == colorHex ? .accentColor : .clear, radius: 4) + } + .buttonStyle(.plain) + } + } + } + VStack(alignment: .leading, spacing: 8) { Text("Display Type") .font(.headline) @@ -263,9 +321,12 @@ struct UserTimerEditSheet: View { Button(timer == nil ? "Add" : "Save") { let newTimer = UserTimer( id: timer?.id ?? UUID().uuidString, + title: title, type: type, timeOnScreenSeconds: timeOnScreen, - message: message.isEmpty ? nil : message + message: message.isEmpty ? nil : message, + colorHex: selectedColorHex, + enabled: timer?.enabled ?? true ) onSave(newTimer) } @@ -274,7 +335,7 @@ struct UserTimerEditSheet: View { } } .padding(24) - .frame(width: 400) + .frame(width: 450) } } @@ -286,10 +347,10 @@ struct UserTimerEditSheet: View { UserTimersView( userTimers: .constant([ UserTimer( - id: "1", type: .subtle, timeOnScreenSeconds: 30, message: "Take a break"), + id: "1", title: "User Reminder 1", type: .subtle, timeOnScreenSeconds: 30, message: "Take a break", colorHex: "9B59B6"), UserTimer( - id: "2", type: .overlay, timeOnScreenSeconds: 60, - message: "Stretch your legs"), + id: "2", title: "User Reminder 2", type: .overlay, timeOnScreenSeconds: 60, + message: "Stretch your legs", colorHex: "3498DB"), ]) ) } diff --git a/Gaze/Views/SettingsWindowView.swift b/Gaze/Views/SettingsWindowView.swift index 46d7c7a..1469f84 100644 --- a/Gaze/Views/SettingsWindowView.swift +++ b/Gaze/Views/SettingsWindowView.swift @@ -19,6 +19,7 @@ struct SettingsWindowView: View { @State private var postureIntervalMinutes: Int @State private var launchAtLogin: Bool @State private var subtleReminderSizePercentage: Double + @State private var userTimers: [UserTimer] init(settingsManager: SettingsManager, initialTab: Int = 0) { self.settingsManager = settingsManager @@ -38,6 +39,7 @@ struct SettingsWindowView: View { _launchAtLogin = State(initialValue: settingsManager.settings.launchAtLogin) _subtleReminderSizePercentage = State( initialValue: settingsManager.settings.subtleReminderSizePercentage) + _userTimers = State(initialValue: settingsManager.settings.userTimers) } var body: some View { @@ -71,7 +73,7 @@ struct SettingsWindowView: View { Label("Posture", systemImage: "figure.stand") } - UserTimersView(userTimers: $settingsManager.settings.userTimers) + UserTimersView(userTimers: $userTimers) .tag(3) .tabItem { Label("User Timers", systemImage: "plus.circle") @@ -107,7 +109,7 @@ struct SettingsWindowView: View { } .padding() } - .frame(minWidth: 700, minHeight: 800) + .frame(minWidth: 700, minHeight: 750) .onReceive( NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab")) ) { notification in @@ -136,6 +138,10 @@ struct SettingsWindowView: View { settingsManager.settings.launchAtLogin = launchAtLogin settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage + settingsManager.settings.userTimers = userTimers + + // Save settings to persist changes + settingsManager.save() do { if launchAtLogin {