feat: user timer, ui improvements
This commit is contained in:
@@ -200,7 +200,7 @@ private func showReminderWindow(_ content: AnyView) {
|
||||
}
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 600, height: 550),
|
||||
contentRect: NSRect(x: 0, y: 0, width: 700, height: 700),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
|
||||
@@ -31,6 +31,8 @@ struct GazeApp: App {
|
||||
}
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.windowResizability(.contentSize)
|
||||
.defaultSize(width: 700, height: 700)
|
||||
.commands {
|
||||
CommandGroup(replacing: .newItem) { }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimerState {
|
||||
struct TimerState: Equatable {
|
||||
let type: TimerType
|
||||
var remainingSeconds: Int
|
||||
var isPaused: Bool
|
||||
@@ -21,4 +21,11 @@ struct TimerState {
|
||||
self.isActive = isActive
|
||||
self.targetDate = Date().addingTimeInterval(Double(intervalSeconds))
|
||||
}
|
||||
|
||||
static func == (lhs: TimerState, rhs: TimerState) -> Bool {
|
||||
lhs.type == rhs.type && lhs.remainingSeconds == rhs.remainingSeconds
|
||||
&& lhs.isPaused == rhs.isPaused && lhs.isActive == rhs.isActive
|
||||
&& lhs.targetDate.timeIntervalSince1970.rounded()
|
||||
== rhs.targetDate.timeIntervalSince1970.rounded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Represents a user-defined timer with customizable properties
|
||||
struct UserTimer: Codable, Equatable {
|
||||
struct UserTimer: Codable, Equatable, Identifiable {
|
||||
let id: String
|
||||
var type: UserTimerType
|
||||
var timeOnScreenSeconds: Int
|
||||
|
||||
@@ -12,6 +12,9 @@ import Foundation
|
||||
class TimerEngine: ObservableObject {
|
||||
@Published var timerStates: [TimerType: TimerState] = [:]
|
||||
@Published var activeReminder: ReminderEvent?
|
||||
|
||||
// Track user timer states separately
|
||||
private var userTimerStates: [String: TimerState] = [:]
|
||||
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let settingsManager: SettingsManager
|
||||
@@ -35,6 +38,11 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Start user timers
|
||||
for userTimer in settingsManager.settings.userTimers {
|
||||
startUserTimer(userTimer)
|
||||
}
|
||||
|
||||
timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
@@ -48,6 +56,7 @@ class TimerEngine: ObservableObject {
|
||||
timerSubscription?.cancel()
|
||||
timerSubscription = nil
|
||||
timerStates.removeAll()
|
||||
userTimerStates.removeAll()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
@@ -87,6 +96,7 @@ class TimerEngine: ObservableObject {
|
||||
private func handleTick() {
|
||||
guard activeReminder == nil else { return }
|
||||
|
||||
// Handle regular timers first
|
||||
for (type, state) in timerStates {
|
||||
guard state.isActive && !state.isPaused else { continue }
|
||||
// prevent overshoot - in case user closes laptop while timer is running, we don't want to
|
||||
@@ -110,6 +120,36 @@ class TimerEngine: ObservableObject {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Handle user timers
|
||||
handleUserTimerTicks()
|
||||
}
|
||||
|
||||
private func handleUserTimerTicks() {
|
||||
for (id, state) in userTimerStates {
|
||||
if !state.isActive || state.isPaused { continue }
|
||||
|
||||
// Update user timer countdown
|
||||
userTimerStates[id]?.remainingSeconds -= 1
|
||||
|
||||
if let updatedState = userTimerStates[id], updatedState.remainingSeconds <= 0 {
|
||||
// Trigger the user timer reminder
|
||||
triggerUserTimerReminder(forId: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func triggerUserTimerReminder(forId id: String) {
|
||||
// Here we'd implement how to show a subtle reminder for user timers
|
||||
// For now, just reset the timer
|
||||
if let userTimer = settingsManager.settings.userTimers.first(where: { $0.id == id }) {
|
||||
userTimerStates[id] = TimerState(
|
||||
type: .lookAway, // Placeholder - user timers won't use this
|
||||
intervalSeconds: userTimer.timeOnScreenSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func triggerReminder(for type: TimerType) {
|
||||
@@ -124,11 +164,44 @@ class TimerEngine: ObservableObject {
|
||||
activeReminder = .postureTriggered
|
||||
}
|
||||
}
|
||||
|
||||
// User timer management methods
|
||||
func startUserTimer(_ userTimer: UserTimer) {
|
||||
userTimerStates[userTimer.id] = TimerState(
|
||||
type: .lookAway, // Placeholder - we'll need to make this more flexible
|
||||
intervalSeconds: userTimer.timeOnScreenSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
|
||||
func stopUserTimer(_ userTimerId: String) {
|
||||
userTimerStates[userTimerId] = nil
|
||||
}
|
||||
|
||||
func pauseUserTimer(_ userTimerId: String) {
|
||||
if var state = userTimerStates[userTimerId] {
|
||||
state.isPaused = true
|
||||
userTimerStates[userTimerId] = state
|
||||
}
|
||||
}
|
||||
|
||||
func resumeUserTimer(_ userTimerId: String) {
|
||||
if var state = userTimerStates[userTimerId] {
|
||||
state.isPaused = false
|
||||
userTimerStates[userTimerId] = state
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeRemaining(for type: TimerType) -> TimeInterval {
|
||||
guard let state = timerStates[type] else { return 0 }
|
||||
return TimeInterval(state.remainingSeconds)
|
||||
}
|
||||
|
||||
func getUserTimeRemaining(for userId: String) -> TimeInterval {
|
||||
guard let state = userTimerStates[userId] else { return 0 }
|
||||
return TimeInterval(state.remainingSeconds)
|
||||
}
|
||||
|
||||
func getFormattedTimeRemaining(for type: TimerType) -> String {
|
||||
let seconds = Int(getTimeRemaining(for: type))
|
||||
@@ -143,4 +216,18 @@ class TimerEngine: ObservableObject {
|
||||
return String(format: "%d:%02d", minutes, remainingSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
func getUserFormattedTimeRemaining(for userId: String) -> String {
|
||||
let seconds = Int(getUserTimeRemaining(for: userId))
|
||||
let minutes = seconds / 60
|
||||
let remainingSeconds = seconds % 60
|
||||
|
||||
if minutes >= 60 {
|
||||
let hours = minutes / 60
|
||||
let remainingMinutes = minutes % 60
|
||||
return String(format: "%d:%02d:%02d", hours, remainingMinutes, remainingSeconds)
|
||||
} else {
|
||||
return String(format: "%d:%02d", minutes, remainingSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,17 @@ struct MenuBarContentView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Show user timers if any exist
|
||||
ForEach(settingsManager.settings.userTimers, id: \.id) { userTimer in
|
||||
UserTimerStatusRow(
|
||||
timer: userTimer,
|
||||
state: nil, // We'll implement proper state tracking later
|
||||
onTap: {
|
||||
onOpenSettingsTab(3) // Switch to User Timers tab
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
|
||||
@@ -316,6 +327,82 @@ struct InactiveTimerRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct UserTimerStatusRow: View {
|
||||
let timer: UserTimer
|
||||
let state: TimerState?
|
||||
var onTap: () -> Void
|
||||
@State private var isHovered = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack {
|
||||
Image(systemName: "clock.fill")
|
||||
.foregroundColor(.purple)
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(timer.message ?? "Custom Timer")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
|
||||
if let state = state {
|
||||
Text(timeRemaining(state))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.monospacedDigit()
|
||||
} else {
|
||||
Text("Not active")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(6)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(
|
||||
isHovered ? .regular.tint(.purple.opacity(0.5)) : .regular,
|
||||
in: .rect(cornerRadius: 6)
|
||||
)
|
||||
.padding(.horizontal, 8)
|
||||
.onHover { hovering in
|
||||
isHovered = hovering
|
||||
}
|
||||
.help(tooltipText)
|
||||
}
|
||||
|
||||
private var tooltipText: String {
|
||||
let typeText = timer.type == .subtle ? "Subtle" : "Overlay"
|
||||
let durationText = "\(timer.timeOnScreenSeconds)s on screen"
|
||||
return "\(typeText) timer - \(durationText)"
|
||||
}
|
||||
|
||||
private func timeRemaining(_ state: TimerState) -> String {
|
||||
let seconds = state.remainingSeconds
|
||||
let minutes = seconds / 60
|
||||
let remainingSeconds = seconds % 60
|
||||
|
||||
if minutes >= 60 {
|
||||
let hours = minutes / 60
|
||||
let remainingMinutes = minutes % 60
|
||||
return String(format: "%dh %dm", hours, remainingMinutes)
|
||||
} else if minutes > 0 {
|
||||
return String(format: "%dm %ds", minutes, remainingSeconds)
|
||||
} else {
|
||||
return String(format: "%ds", remainingSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Menu Bar Content") {
|
||||
let settingsManager = SettingsManager.shared
|
||||
let timerEngine = TimerEngine(settingsManager: settingsManager)
|
||||
@@ -326,4 +413,4 @@ struct InactiveTimerRow: View {
|
||||
onOpenSettings: {},
|
||||
onOpenSettingsTab: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ struct BlinkSetupView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 600, height: 450)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ struct LookAwaySetupView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 600, height: 450)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ struct OnboardingContainerView: View {
|
||||
@State private var postureEnabled = true
|
||||
@State private var postureIntervalMinutes = 30
|
||||
@State private var launchAtLogin = false
|
||||
@State private var subtleReminderSizePercentage = 5.0
|
||||
@State private var isAnimatingOut = false
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@@ -75,6 +76,7 @@ struct OnboardingContainerView: View {
|
||||
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: $launchAtLogin,
|
||||
subtleReminderSizePercentage: $subtleReminderSizePercentage,
|
||||
isOnboarding: true
|
||||
)
|
||||
.tag(4)
|
||||
@@ -136,6 +138,7 @@ struct OnboardingContainerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 1000, minHeight: 750)
|
||||
.opacity(isAnimatingOut ? 0 : 1)
|
||||
.scaleEffect(isAnimatingOut ? 0.3 : 1.0)
|
||||
}
|
||||
@@ -159,6 +162,7 @@ struct OnboardingContainerView: View {
|
||||
)
|
||||
|
||||
settingsManager.settings.launchAtLogin = launchAtLogin
|
||||
settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage
|
||||
settingsManager.settings.hasCompletedOnboarding = true
|
||||
|
||||
// Apply launch at login setting
|
||||
|
||||
@@ -92,7 +92,7 @@ struct PostureSetupView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 600, height: 450)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct SettingsOnboardingView: View {
|
||||
@Binding var launchAtLogin: Bool
|
||||
@Binding var subtleReminderSizePercentage: Double
|
||||
var isOnboarding: Bool = true
|
||||
|
||||
var body: some View {
|
||||
@@ -48,6 +49,29 @@ struct SettingsOnboardingView: View {
|
||||
.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")
|
||||
@@ -116,7 +140,7 @@ struct SettingsOnboardingView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 600, height: 450)
|
||||
.frame(minWidth: 650, minHeight: 650)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
@@ -137,6 +161,7 @@ struct SettingsOnboardingView: View {
|
||||
#Preview("Settings Onboarding - Launch Disabled") {
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: .constant(false),
|
||||
subtleReminderSizePercentage: .constant(5.0),
|
||||
isOnboarding: true
|
||||
)
|
||||
}
|
||||
@@ -144,6 +169,7 @@ struct SettingsOnboardingView: View {
|
||||
#Preview("Settings Onboarding - Launch Enabled") {
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: .constant(true),
|
||||
subtleReminderSizePercentage: .constant(10.0),
|
||||
isOnboarding: true
|
||||
)
|
||||
}
|
||||
|
||||
295
Gaze/Views/Onboarding/UserTimersView.swift
Normal file
295
Gaze/Views/Onboarding/UserTimersView.swift
Normal file
@@ -0,0 +1,295 @@
|
||||
//
|
||||
// UserTimersView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Mike Freno on 1/9/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserTimersView: View {
|
||||
@Binding var userTimers: [UserTimer]
|
||||
@State private var editingTimer: UserTimer?
|
||||
@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)
|
||||
}
|
||||
.padding()
|
||||
.glassEffect(.regular.tint(.purple), in: .rect(cornerRadius: 8))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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(userTimers) { timer in
|
||||
UserTimerRow(
|
||||
timer: timer,
|
||||
onEdit: {
|
||||
editingTimer = timer
|
||||
},
|
||||
onDelete: {
|
||||
if let index = userTimers.firstIndex(where: {
|
||||
$0.id == timer.id
|
||||
}) {
|
||||
userTimers.remove(at: index)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
.sheet(isPresented: $showingAddTimer) {
|
||||
UserTimerEditSheet(
|
||||
timer: nil,
|
||||
onSave: { newTimer in
|
||||
userTimers.append(newTimer)
|
||||
showingAddTimer = false
|
||||
},
|
||||
onCancel: {
|
||||
showingAddTimer = false
|
||||
}
|
||||
)
|
||||
}
|
||||
.sheet(item: $editingTimer) { timer in
|
||||
UserTimerEditSheet(
|
||||
timer: timer,
|
||||
onSave: { updatedTimer in
|
||||
if let index = userTimers.firstIndex(where: { $0.id == timer.id }) {
|
||||
userTimers[index] = updatedTimer
|
||||
}
|
||||
editingTimer = nil
|
||||
},
|
||||
onCancel: {
|
||||
editingTimer = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserTimerRow: View {
|
||||
let timer: UserTimer
|
||||
var onEdit: () -> Void
|
||||
var onDelete: () -> Void
|
||||
@State private var isHovered = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
||||
.foregroundColor(.purple)
|
||||
.frame(width: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(timer.message ?? "Custom Timer")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
Text("\(timer.type.displayName) • \(timer.timeOnScreenSeconds)s on screen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Button(action: onEdit) {
|
||||
Image(systemName: "pencil.circle")
|
||||
.font(.title3)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button(action: onDelete) {
|
||||
Image(systemName: "trash.circle")
|
||||
.font(.title3)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.secondary.opacity(isHovered ? 0.1 : 0.05))
|
||||
)
|
||||
.onHover { hovering in
|
||||
isHovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserTimerEditSheet: View {
|
||||
let timer: UserTimer?
|
||||
var onSave: (UserTimer) -> Void
|
||||
var onCancel: () -> Void
|
||||
|
||||
@State private var message: String
|
||||
@State private var type: UserTimerType
|
||||
@State private var timeOnScreen: Int
|
||||
|
||||
init(
|
||||
timer: UserTimer?,
|
||||
onSave: @escaping (UserTimer) -> Void,
|
||||
onCancel: @escaping () -> Void
|
||||
) {
|
||||
self.timer = timer
|
||||
self.onSave = onSave
|
||||
self.onCancel = onCancel
|
||||
|
||||
_message = State(initialValue: timer?.message ?? "")
|
||||
_type = State(initialValue: timer?.type ?? .subtle)
|
||||
_timeOnScreen = State(initialValue: timer?.timeOnScreenSeconds ?? 30)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
Text(timer == nil ? "Add Custom Timer" : "Edit Custom Timer")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Display Type")
|
||||
.font(.headline)
|
||||
|
||||
Picker("Display Type", selection: $type) {
|
||||
ForEach(UserTimerType.allCases) { timerType in
|
||||
Text(timerType.displayName).tag(timerType)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Text(
|
||||
type == .subtle
|
||||
? "Small reminder in corner of screen"
|
||||
: "Full screen reminder with animation"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Duration on Screen")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(timeOnScreen) },
|
||||
set: { timeOnScreen = Int($0) }
|
||||
),
|
||||
in: 5...120,
|
||||
step: 5
|
||||
)
|
||||
Text("\(timeOnScreen)s")
|
||||
.frame(width: 50, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Message (Optional)")
|
||||
.font(.headline)
|
||||
TextField("Enter custom reminder message", text: $message)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("Leave blank to show a default timer notification")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button("Cancel", action: onCancel)
|
||||
.keyboardShortcut(.escape)
|
||||
|
||||
Button(timer == nil ? "Add" : "Save") {
|
||||
let newTimer = UserTimer(
|
||||
id: timer?.id ?? UUID().uuidString,
|
||||
type: type,
|
||||
timeOnScreenSeconds: timeOnScreen,
|
||||
message: message.isEmpty ? nil : message
|
||||
)
|
||||
onSave(newTimer)
|
||||
}
|
||||
.keyboardShortcut(.return)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(width: 400)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("User Timers - Empty") {
|
||||
UserTimersView(userTimers: .constant([]))
|
||||
}
|
||||
|
||||
#Preview("User Timers - With Timers") {
|
||||
UserTimersView(
|
||||
userTimers: .constant([
|
||||
UserTimer(
|
||||
id: "1", type: .subtle, timeOnScreenSeconds: 30, message: "Take a break"),
|
||||
UserTimer(
|
||||
id: "2", type: .overlay, timeOnScreenSeconds: 60,
|
||||
message: "Stretch your legs"),
|
||||
])
|
||||
)
|
||||
}
|
||||
@@ -18,21 +18,28 @@ struct SettingsWindowView: View {
|
||||
@State private var postureEnabled: Bool
|
||||
@State private var postureIntervalMinutes: Int
|
||||
@State private var launchAtLogin: Bool
|
||||
|
||||
@State private var subtleReminderSizePercentage: Double
|
||||
|
||||
init(settingsManager: SettingsManager, initialTab: Int = 0) {
|
||||
self.settingsManager = settingsManager
|
||||
|
||||
|
||||
_currentTab = State(initialValue: initialTab)
|
||||
_lookAwayEnabled = State(initialValue: settingsManager.settings.lookAwayTimer.enabled)
|
||||
_lookAwayIntervalMinutes = State(initialValue: settingsManager.settings.lookAwayTimer.intervalSeconds / 60)
|
||||
_lookAwayCountdownSeconds = State(initialValue: settingsManager.settings.lookAwayCountdownSeconds)
|
||||
_lookAwayIntervalMinutes = State(
|
||||
initialValue: settingsManager.settings.lookAwayTimer.intervalSeconds / 60)
|
||||
_lookAwayCountdownSeconds = State(
|
||||
initialValue: settingsManager.settings.lookAwayCountdownSeconds)
|
||||
_blinkEnabled = State(initialValue: settingsManager.settings.blinkTimer.enabled)
|
||||
_blinkIntervalMinutes = State(initialValue: settingsManager.settings.blinkTimer.intervalSeconds / 60)
|
||||
_blinkIntervalMinutes = State(
|
||||
initialValue: settingsManager.settings.blinkTimer.intervalSeconds / 60)
|
||||
_postureEnabled = State(initialValue: settingsManager.settings.postureTimer.enabled)
|
||||
_postureIntervalMinutes = State(initialValue: settingsManager.settings.postureTimer.intervalSeconds / 60)
|
||||
_postureIntervalMinutes = State(
|
||||
initialValue: settingsManager.settings.postureTimer.intervalSeconds / 60)
|
||||
_launchAtLogin = State(initialValue: settingsManager.settings.launchAtLogin)
|
||||
_subtleReminderSizePercentage = State(
|
||||
initialValue: settingsManager.settings.subtleReminderSizePercentage)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
TabView(selection: $currentTab) {
|
||||
@@ -45,7 +52,7 @@ struct SettingsWindowView: View {
|
||||
.tabItem {
|
||||
Label("Look Away", systemImage: "eye.fill")
|
||||
}
|
||||
|
||||
|
||||
BlinkSetupView(
|
||||
enabled: $blinkEnabled,
|
||||
intervalMinutes: $blinkIntervalMinutes
|
||||
@@ -54,7 +61,7 @@ struct SettingsWindowView: View {
|
||||
.tabItem {
|
||||
Label("Blink", systemImage: "eye.circle.fill")
|
||||
}
|
||||
|
||||
|
||||
PostureSetupView(
|
||||
enabled: $postureEnabled,
|
||||
intervalMinutes: $postureIntervalMinutes
|
||||
@@ -63,28 +70,34 @@ struct SettingsWindowView: View {
|
||||
.tabItem {
|
||||
Label("Posture", systemImage: "figure.stand")
|
||||
}
|
||||
|
||||
|
||||
UserTimersView(userTimers: $settingsManager.settings.userTimers)
|
||||
.tag(3)
|
||||
.tabItem {
|
||||
Label("User Timers", systemImage: "plus.circle")
|
||||
}
|
||||
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: $launchAtLogin,
|
||||
subtleReminderSizePercentage: $subtleReminderSizePercentage,
|
||||
isOnboarding: false
|
||||
)
|
||||
.tag(3)
|
||||
.tag(4)
|
||||
.tabItem {
|
||||
Label("General", systemImage: "gearshape.fill")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
|
||||
Button("Cancel") {
|
||||
closeWindow()
|
||||
}
|
||||
.keyboardShortcut(.escape)
|
||||
|
||||
|
||||
Button("Apply") {
|
||||
applySettings()
|
||||
closeWindow()
|
||||
@@ -94,33 +107,36 @@ struct SettingsWindowView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(width: 600, height: 550)
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))) { notification in
|
||||
.frame(minWidth: 700, minHeight: 800)
|
||||
.onReceive(
|
||||
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
|
||||
) { notification in
|
||||
if let tab = notification.object as? Int {
|
||||
currentTab = tab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func applySettings() {
|
||||
settingsManager.settings.lookAwayTimer = TimerConfiguration(
|
||||
enabled: lookAwayEnabled,
|
||||
intervalSeconds: lookAwayIntervalMinutes * 60
|
||||
)
|
||||
settingsManager.settings.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
||||
|
||||
|
||||
settingsManager.settings.blinkTimer = TimerConfiguration(
|
||||
enabled: blinkEnabled,
|
||||
intervalSeconds: blinkIntervalMinutes * 60
|
||||
)
|
||||
|
||||
|
||||
settingsManager.settings.postureTimer = TimerConfiguration(
|
||||
enabled: postureEnabled,
|
||||
intervalSeconds: postureIntervalMinutes * 60
|
||||
)
|
||||
|
||||
|
||||
settingsManager.settings.launchAtLogin = launchAtLogin
|
||||
|
||||
settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage
|
||||
|
||||
do {
|
||||
if launchAtLogin {
|
||||
try LaunchAtLoginManager.enable()
|
||||
@@ -131,7 +147,7 @@ struct SettingsWindowView: View {
|
||||
print("Failed to set launch at login: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func closeWindow() {
|
||||
if let window = NSApplication.shared.windows.first(where: { $0.title == "Settings" }) {
|
||||
window.close()
|
||||
|
||||
Reference in New Issue
Block a user