general: qol menubar content close on settings open, positional consitencies
This commit is contained in:
@@ -188,6 +188,16 @@ private func showReminderWindow(_ content: AnyView) {
|
|||||||
|
|
||||||
// Public method to open settings window
|
// Public method to open settings window
|
||||||
func openSettings(tab: Int = 0) {
|
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 window already exists, switch to the tab and bring it to front
|
||||||
if let existingWindow = settingsWindowController?.window {
|
if let existingWindow = settingsWindowController?.window {
|
||||||
NotificationCenter.default.post(
|
NotificationCenter.default.post(
|
||||||
|
|||||||
@@ -6,29 +6,98 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/// Represents a user-defined timer with customizable properties
|
/// Represents a user-defined timer with customizable properties
|
||||||
struct UserTimer: Codable, Equatable, Identifiable {
|
struct UserTimer: Codable, Equatable, Identifiable {
|
||||||
let id: String
|
let id: String
|
||||||
|
var title: String
|
||||||
var type: UserTimerType
|
var type: UserTimerType
|
||||||
var timeOnScreenSeconds: Int
|
var timeOnScreenSeconds: Int
|
||||||
var message: String?
|
var message: String?
|
||||||
|
var colorHex: String
|
||||||
|
var enabled: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String = UUID().uuidString,
|
id: String = UUID().uuidString,
|
||||||
|
title: String? = nil,
|
||||||
type: UserTimerType = .subtle,
|
type: UserTimerType = .subtle,
|
||||||
timeOnScreenSeconds: Int = 30,
|
timeOnScreenSeconds: Int = 30,
|
||||||
message: String? = nil
|
message: String? = nil,
|
||||||
|
colorHex: String? = nil,
|
||||||
|
enabled: Bool = true
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.title = title ?? "User Reminder"
|
||||||
self.type = type
|
self.type = type
|
||||||
self.timeOnScreenSeconds = timeOnScreenSeconds
|
self.timeOnScreenSeconds = timeOnScreenSeconds
|
||||||
self.message = message
|
self.message = message
|
||||||
|
self.colorHex = colorHex ?? UserTimer.defaultColors[0]
|
||||||
|
self.enabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: UserTimer, rhs: UserTimer) -> Bool {
|
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.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ struct MenuBarHoverButtonStyle: ButtonStyle {
|
|||||||
struct MenuBarContentView: View {
|
struct MenuBarContentView: View {
|
||||||
@ObservedObject var timerEngine: TimerEngine
|
@ObservedObject var timerEngine: TimerEngine
|
||||||
@ObservedObject var settingsManager: SettingsManager
|
@ObservedObject var settingsManager: SettingsManager
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
var onQuit: () -> Void
|
var onQuit: () -> Void
|
||||||
var onOpenSettings: () -> Void
|
var onOpenSettings: () -> Void
|
||||||
var onOpenSettingsTab: (Int) -> Void
|
var onOpenSettingsTab: (Int) -> Void
|
||||||
@@ -98,8 +99,8 @@ struct MenuBarContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show user timers if any exist
|
// Show user timers if any exist and are enabled
|
||||||
ForEach(settingsManager.settings.userTimers, id: \.id) { userTimer in
|
ForEach(settingsManager.settings.userTimers.filter { $0.enabled }, id: \.id) { userTimer in
|
||||||
UserTimerStatusRow(
|
UserTimerStatusRow(
|
||||||
timer: userTimer,
|
timer: userTimer,
|
||||||
state: nil, // We'll implement proper state tracking later
|
state: nil, // We'll implement proper state tracking later
|
||||||
@@ -166,6 +167,9 @@ struct MenuBarContentView: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
.frame(width: 300)
|
.frame(width: 300)
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("CloseMenuBarPopover"))) { _ in
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isPaused: Bool {
|
private var isPaused: Bool {
|
||||||
@@ -336,12 +340,16 @@ struct UserTimerStatusRow: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: onTap) {
|
Button(action: onTap) {
|
||||||
HStack {
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(timer.color)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
|
||||||
Image(systemName: "clock.fill")
|
Image(systemName: "clock.fill")
|
||||||
.foregroundColor(.purple)
|
.foregroundColor(timer.color)
|
||||||
.frame(width: 20)
|
.frame(width: 20)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(timer.message ?? "Custom Timer")
|
Text(timer.title)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -352,7 +360,7 @@ struct UserTimerStatusRow: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
} else {
|
} else {
|
||||||
Text("Not active")
|
Text(timer.enabled ? "Not active" : "Disabled")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@@ -370,7 +378,7 @@ struct UserTimerStatusRow: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.glassEffect(
|
.glassEffect(
|
||||||
isHovered ? .regular.tint(.purple.opacity(0.5)) : .regular,
|
isHovered ? .regular.tint(timer.color.opacity(0.3)) : .regular,
|
||||||
in: .rect(cornerRadius: 6)
|
in: .rect(cornerRadius: 6)
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
@@ -383,7 +391,8 @@ struct UserTimerStatusRow: View {
|
|||||||
private var tooltipText: String {
|
private var tooltipText: String {
|
||||||
let typeText = timer.type == .subtle ? "Subtle" : "Overlay"
|
let typeText = timer.type == .subtle ? "Subtle" : "Overlay"
|
||||||
let durationText = "\(timer.timeOnScreenSeconds)s on screen"
|
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 {
|
private func timeRemaining(_ state: TimerState) -> String {
|
||||||
|
|||||||
@@ -12,84 +12,94 @@ struct BlinkSetupView: View {
|
|||||||
@Binding var intervalMinutes: Int
|
@Binding var intervalMinutes: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 0) {
|
||||||
Image(systemName: "eye.circle")
|
// Fixed header section
|
||||||
.font(.system(size: 60))
|
VStack(spacing: 16) {
|
||||||
.foregroundColor(.green)
|
Image(systemName: "eye.circle")
|
||||||
|
.font(.system(size: 60))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
|
||||||
Text("Blink Reminder")
|
Text("Blink Reminder")
|
||||||
.font(.system(size: 28, weight: .bold))
|
.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)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.top, 20)
|
||||||
.glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
// Vertically centered content
|
||||||
Toggle("Enable Blink Reminders", isOn: $enabled)
|
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)
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||||
|
|
||||||
if enabled {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
Toggle("Enable Blink Reminders", isOn: $enabled)
|
||||||
Text("Remind me every:")
|
.font(.headline)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
HStack {
|
if enabled {
|
||||||
Slider(
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
value: Binding(
|
Text("Remind me every:")
|
||||||
get: { Double(intervalMinutes) },
|
.font(.subheadline)
|
||||||
set: { intervalMinutes = Int($0) }
|
.foregroundColor(.secondary)
|
||||||
), in: 1...15, step: 1)
|
|
||||||
|
|
||||||
Text("\(intervalMinutes) min")
|
HStack {
|
||||||
.frame(width: 60, alignment: .trailing)
|
Slider(
|
||||||
.monospacedDigit()
|
value: Binding(
|
||||||
|
get: { Double(intervalMinutes) },
|
||||||
|
set: { intervalMinutes = Int($0) }
|
||||||
|
), in: 1...15, step: 1)
|
||||||
|
|
||||||
|
Text("\(intervalMinutes) min")
|
||||||
|
.frame(width: 60, alignment: .trailing)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding()
|
||||||
.padding()
|
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
||||||
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
Text(
|
Text(
|
||||||
"You will be subtly reminded every \(intervalMinutes) minutes to blink"
|
"You will be subtly reminded every \(intervalMinutes) minutes to blink"
|
||||||
)
|
)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"Blink reminders are currently disabled."
|
"Blink reminders are currently disabled."
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -19,95 +19,105 @@ struct LookAwaySetupView: View {
|
|||||||
@Binding var countdownSeconds: Int
|
@Binding var countdownSeconds: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 0) {
|
||||||
Image(systemName: "eye.fill")
|
// Fixed header section
|
||||||
.font(.system(size: 60))
|
VStack(spacing: 16) {
|
||||||
.foregroundColor(.accentColor)
|
Image(systemName: "eye.fill")
|
||||||
|
.font(.system(size: 60))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
Text("Look Away Reminder")
|
Text("Look Away Reminder")
|
||||||
.font(.system(size: 28, weight: .bold))
|
.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)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.top, 20)
|
||||||
.glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
// Vertically centered content
|
||||||
Toggle("Enable Look Away Reminders", isOn: $enabled)
|
Spacer()
|
||||||
.font(.headline)
|
|
||||||
|
VStack(spacing: 30) {
|
||||||
if enabled {
|
// InfoBox with link functionality
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Text("Remind me every:")
|
Button(action: {
|
||||||
.font(.subheadline)
|
if let url = URL(
|
||||||
.foregroundColor(.secondary)
|
string: "https://www.healthline.com/health/eye-health/20-20-20-rule")
|
||||||
|
{
|
||||||
HStack {
|
#if os(iOS)
|
||||||
Slider(
|
UIApplication.shared.open(url)
|
||||||
value: Binding(
|
#elseif os(macOS)
|
||||||
get: { Double(intervalMinutes) },
|
NSWorkspace.shared.open(url)
|
||||||
set: { intervalMinutes = Int($0) }
|
#endif
|
||||||
), in: 5...60, step: 5)
|
|
||||||
|
|
||||||
Text("\(intervalMinutes) min")
|
|
||||||
.frame(width: 60, alignment: .trailing)
|
|
||||||
.monospacedDigit()
|
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
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:")
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
.font(.subheadline)
|
Toggle("Enable Look Away Reminders", isOn: $enabled)
|
||||||
.foregroundColor(.secondary)
|
.font(.headline)
|
||||||
|
|
||||||
HStack {
|
if enabled {
|
||||||
Slider(
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
value: Binding(
|
Text("Remind me every:")
|
||||||
get: { Double(countdownSeconds) },
|
.font(.subheadline)
|
||||||
set: { countdownSeconds = Int($0) }
|
.foregroundColor(.secondary)
|
||||||
), in: 10...30, step: 5)
|
|
||||||
|
|
||||||
Text("\(countdownSeconds) sec")
|
HStack {
|
||||||
.frame(width: 60, alignment: .trailing)
|
Slider(
|
||||||
.monospacedDigit()
|
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()
|
||||||
.padding()
|
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
||||||
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
Text(
|
Text(
|
||||||
"You will be reminded every \(intervalMinutes) minutes to look in the distance for \(countdownSeconds) seconds"
|
"You will be reminded every \(intervalMinutes) minutes to look in the distance for \(countdownSeconds) seconds"
|
||||||
)
|
)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"Look away reminders are currently disabled."
|
"Look away reminders are currently disabled."
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -12,84 +12,94 @@ struct PostureSetupView: View {
|
|||||||
@Binding var intervalMinutes: Int
|
@Binding var intervalMinutes: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 0) {
|
||||||
Image(systemName: "figure.stand")
|
// Fixed header section
|
||||||
.font(.system(size: 60))
|
VStack(spacing: 16) {
|
||||||
.foregroundColor(.orange)
|
Image(systemName: "figure.stand")
|
||||||
|
.font(.system(size: 60))
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
Text("Posture Reminder")
|
Text("Posture Reminder")
|
||||||
.font(.system(size: 28, weight: .bold))
|
.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)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.top, 20)
|
||||||
.glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
// Vertically centered content
|
||||||
Toggle("Enable Posture Reminders", isOn: $enabled)
|
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)
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||||
|
|
||||||
if enabled {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
Toggle("Enable Posture Reminders", isOn: $enabled)
|
||||||
Text("Remind me every:")
|
.font(.headline)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
HStack {
|
if enabled {
|
||||||
Slider(
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
value: Binding(
|
Text("Remind me every:")
|
||||||
get: { Double(intervalMinutes) },
|
.font(.subheadline)
|
||||||
set: { intervalMinutes = Int($0) }
|
.foregroundColor(.secondary)
|
||||||
), in: 15...60, step: 5)
|
|
||||||
|
|
||||||
Text("\(intervalMinutes) min")
|
HStack {
|
||||||
.frame(width: 60, alignment: .trailing)
|
Slider(
|
||||||
.monospacedDigit()
|
value: Binding(
|
||||||
|
get: { Double(intervalMinutes) },
|
||||||
|
set: { intervalMinutes = Int($0) }
|
||||||
|
), in: 15...60, step: 5)
|
||||||
|
|
||||||
|
Text("\(intervalMinutes) min")
|
||||||
|
.frame(width: 60, alignment: .trailing)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding()
|
||||||
.padding()
|
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
||||||
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
Text(
|
Text(
|
||||||
"You will be subtly reminded every \(intervalMinutes) minutes to check your posture"
|
"You will be subtly reminded every \(intervalMinutes) minutes to check your posture"
|
||||||
)
|
)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"Posture reminders are currently disabled."
|
"Posture reminders are currently disabled."
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -13,134 +13,138 @@ struct SettingsOnboardingView: View {
|
|||||||
var isOnboarding: Bool = true
|
var isOnboarding: Bool = true
|
||||||
|
|
||||||
var body: some View {
|
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()
|
Spacer()
|
||||||
|
VStack(spacing: 30) {
|
||||||
|
Text("Configure app preferences and support the project")
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Image(systemName: "gearshape.fill")
|
VStack(spacing: 20) {
|
||||||
.font(.system(size: 80))
|
// Launch at Login Toggle
|
||||||
.foregroundColor(.accentColor)
|
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")
|
// Subtle Reminder Size Configuration
|
||||||
.font(.system(size: 36, weight: .bold))
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Subtle Reminder Size")
|
||||||
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")
|
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("Start Gaze automatically when you log in")
|
|
||||||
|
Text("Adjust the size of blink and posture reminders")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.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 {
|
HStack {
|
||||||
Image(systemName: "chevron.left.forwardslash.chevron.right")
|
Slider(
|
||||||
.font(.title3)
|
value: $subtleReminderSizePercentage,
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
in: 2...35,
|
||||||
Text("View on GitHub")
|
step: 1
|
||||||
.font(.subheadline)
|
)
|
||||||
.fontWeight(.semibold)
|
Text("\(Int(subtleReminderSizePercentage))%")
|
||||||
Text("Star the repo, report issues, contribute")
|
.frame(width: 50, alignment: .trailing)
|
||||||
.font(.caption)
|
.monospacedDigit()
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "arrow.up.right")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.padding()
|
||||||
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
.glassEffect(.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
// Buy Me a Coffee
|
// Links Section
|
||||||
Button(action: {
|
VStack(spacing: 12) {
|
||||||
if let url = URL(string: "https://buymeacoffee.com/placeholder") {
|
Text("Support & Contribute")
|
||||||
NSWorkspace.shared.open(url)
|
.font(.headline)
|
||||||
}
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}) {
|
|
||||||
HStack {
|
// GitHub Link
|
||||||
Image(systemName: "cup.and.saucer.fill")
|
Button(action: {
|
||||||
.font(.title3)
|
if let url = URL(string: "https://github.com/mikefreno/Gaze") {
|
||||||
.foregroundColor(.orange)
|
NSWorkspace.shared.open(url)
|
||||||
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")
|
HStack {
|
||||||
.font(.caption)
|
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()
|
.buttonStyle(.plain)
|
||||||
.frame(maxWidth: .infinity)
|
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||||
.background(Color.orange.opacity(0.1))
|
|
||||||
.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)
|
.padding()
|
||||||
.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10))
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(minWidth: 650, minHeight: 650)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
.background(.clear)
|
.background(.clear)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,83 +13,90 @@ struct UserTimersView: View {
|
|||||||
@State private var showingAddTimer = false
|
@State private var showingAddTimer = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 0) {
|
||||||
Image(systemName: "clock.badge.checkmark")
|
// Fixed header section
|
||||||
.font(.system(size: 60))
|
VStack(spacing: 16) {
|
||||||
.foregroundColor(.purple)
|
Image(systemName: "clock.badge.checkmark")
|
||||||
|
.font(.system(size: 60))
|
||||||
Text("Custom Timers")
|
.foregroundColor(.purple)
|
||||||
.font(.system(size: 28, weight: .bold))
|
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()
|
.padding(.top, 20)
|
||||||
.glassEffect(.regular.tint(.purple), in: .rect(cornerRadius: 8))
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
// Vertically centered content
|
||||||
HStack {
|
Spacer()
|
||||||
Text("Active Timers (\(userTimers.count)/3)")
|
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)
|
.font(.headline)
|
||||||
Spacer()
|
.foregroundColor(.white)
|
||||||
if userTimers.count < 3 {
|
|
||||||
Button(action: {
|
|
||||||
showingAddTimer = true
|
|
||||||
}) {
|
|
||||||
Label("Add Timer", systemImage: "plus.circle.fill")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
|
.glassEffect(.regular.tint(.purple), in: .rect(cornerRadius: 8))
|
||||||
|
|
||||||
if userTimers.isEmpty {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
VStack(spacing: 12) {
|
HStack {
|
||||||
Image(systemName: "clock.badge.questionmark")
|
Text("Active Timers (\(userTimers.count)/3)")
|
||||||
.font(.system(size: 40))
|
.font(.headline)
|
||||||
.foregroundColor(.secondary)
|
Spacer()
|
||||||
Text("No custom timers yet")
|
if userTimers.count < 3 {
|
||||||
.font(.subheadline)
|
Button(action: {
|
||||||
.foregroundColor(.secondary)
|
showingAddTimer = true
|
||||||
Text("Click 'Add Timer' to create your first custom reminder")
|
}) {
|
||||||
.font(.caption)
|
Label("Add Timer", systemImage: "plus.circle.fill")
|
||||||
.foregroundColor(.secondary)
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(40)
|
if userTimers.isEmpty {
|
||||||
} else {
|
VStack(spacing: 12) {
|
||||||
ScrollView {
|
Image(systemName: "clock.badge.questionmark")
|
||||||
VStack(spacing: 8) {
|
.font(.system(size: 40))
|
||||||
ForEach(userTimers) { timer in
|
.foregroundColor(.secondary)
|
||||||
UserTimerRow(
|
Text("No custom timers yet")
|
||||||
timer: timer,
|
.font(.subheadline)
|
||||||
onEdit: {
|
.foregroundColor(.secondary)
|
||||||
editingTimer = timer
|
Text("Click 'Add Timer' to create your first custom reminder")
|
||||||
},
|
.font(.caption)
|
||||||
onDelete: {
|
.foregroundColor(.secondary)
|
||||||
if let index = userTimers.firstIndex(where: {
|
}
|
||||||
$0.id == timer.id
|
.frame(maxWidth: .infinity)
|
||||||
}) {
|
.padding(40)
|
||||||
userTimers.remove(at: index)
|
} 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()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
@@ -98,6 +105,7 @@ struct UserTimersView: View {
|
|||||||
.sheet(isPresented: $showingAddTimer) {
|
.sheet(isPresented: $showingAddTimer) {
|
||||||
UserTimerEditSheet(
|
UserTimerEditSheet(
|
||||||
timer: nil,
|
timer: nil,
|
||||||
|
existingTimersCount: userTimers.count,
|
||||||
onSave: { newTimer in
|
onSave: { newTimer in
|
||||||
userTimers.append(newTimer)
|
userTimers.append(newTimer)
|
||||||
showingAddTimer = false
|
showingAddTimer = false
|
||||||
@@ -110,6 +118,7 @@ struct UserTimersView: View {
|
|||||||
.sheet(item: $editingTimer) { timer in
|
.sheet(item: $editingTimer) { timer in
|
||||||
UserTimerEditSheet(
|
UserTimerEditSheet(
|
||||||
timer: timer,
|
timer: timer,
|
||||||
|
existingTimersCount: userTimers.count,
|
||||||
onSave: { updatedTimer in
|
onSave: { updatedTimer in
|
||||||
if let index = userTimers.firstIndex(where: { $0.id == timer.id }) {
|
if let index = userTimers.firstIndex(where: { $0.id == timer.id }) {
|
||||||
userTimers[index] = updatedTimer
|
userTimers[index] = updatedTimer
|
||||||
@@ -125,19 +134,23 @@ struct UserTimersView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct UserTimerRow: View {
|
struct UserTimerRow: View {
|
||||||
let timer: UserTimer
|
@Binding var timer: UserTimer
|
||||||
var onEdit: () -> Void
|
var onEdit: () -> Void
|
||||||
var onDelete: () -> Void
|
var onDelete: () -> Void
|
||||||
@State private var isHovered = false
|
@State private var isHovered = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
|
Circle()
|
||||||
|
.fill(timer.color)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
|
||||||
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
||||||
.foregroundColor(.purple)
|
.foregroundColor(timer.color)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(timer.message ?? "Custom Timer")
|
Text(timer.title)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -148,16 +161,21 @@ struct UserTimerRow: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 8) {
|
||||||
|
Toggle("", isOn: $timer.enabled)
|
||||||
|
.labelsHidden()
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
.controlSize(.small)
|
||||||
|
|
||||||
Button(action: onEdit) {
|
Button(action: onEdit) {
|
||||||
Image(systemName: "pencil.circle")
|
Image(systemName: "pencil.circle.fill")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
Button(action: onDelete) {
|
Button(action: onDelete) {
|
||||||
Image(systemName: "trash.circle")
|
Image(systemName: "trash.circle.fill")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
@@ -177,25 +195,32 @@ struct UserTimerRow: View {
|
|||||||
|
|
||||||
struct UserTimerEditSheet: View {
|
struct UserTimerEditSheet: View {
|
||||||
let timer: UserTimer?
|
let timer: UserTimer?
|
||||||
|
let existingTimersCount: Int
|
||||||
var onSave: (UserTimer) -> Void
|
var onSave: (UserTimer) -> Void
|
||||||
var onCancel: () -> Void
|
var onCancel: () -> Void
|
||||||
|
|
||||||
|
@State private var title: String
|
||||||
@State private var message: String
|
@State private var message: String
|
||||||
@State private var type: UserTimerType
|
@State private var type: UserTimerType
|
||||||
@State private var timeOnScreen: Int
|
@State private var timeOnScreen: Int
|
||||||
|
@State private var selectedColorHex: String
|
||||||
|
|
||||||
init(
|
init(
|
||||||
timer: UserTimer?,
|
timer: UserTimer?,
|
||||||
|
existingTimersCount: Int = 0,
|
||||||
onSave: @escaping (UserTimer) -> Void,
|
onSave: @escaping (UserTimer) -> Void,
|
||||||
onCancel: @escaping () -> Void
|
onCancel: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.timer = timer
|
self.timer = timer
|
||||||
|
self.existingTimersCount = existingTimersCount
|
||||||
self.onSave = onSave
|
self.onSave = onSave
|
||||||
self.onCancel = onCancel
|
self.onCancel = onCancel
|
||||||
|
|
||||||
|
_title = State(initialValue: timer?.title ?? UserTimer.generateTitle(for: existingTimersCount))
|
||||||
_message = State(initialValue: timer?.message ?? "")
|
_message = State(initialValue: timer?.message ?? "")
|
||||||
_type = State(initialValue: timer?.type ?? .subtle)
|
_type = State(initialValue: timer?.type ?? .subtle)
|
||||||
_timeOnScreen = State(initialValue: timer?.timeOnScreenSeconds ?? 30)
|
_timeOnScreen = State(initialValue: timer?.timeOnScreenSeconds ?? 30)
|
||||||
|
_selectedColorHex = State(initialValue: timer?.colorHex ?? UserTimer.defaultColors[existingTimersCount % UserTimer.defaultColors.count])
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -205,6 +230,39 @@ struct UserTimerEditSheet: View {
|
|||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
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) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Display Type")
|
Text("Display Type")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@@ -263,9 +321,12 @@ struct UserTimerEditSheet: View {
|
|||||||
Button(timer == nil ? "Add" : "Save") {
|
Button(timer == nil ? "Add" : "Save") {
|
||||||
let newTimer = UserTimer(
|
let newTimer = UserTimer(
|
||||||
id: timer?.id ?? UUID().uuidString,
|
id: timer?.id ?? UUID().uuidString,
|
||||||
|
title: title,
|
||||||
type: type,
|
type: type,
|
||||||
timeOnScreenSeconds: timeOnScreen,
|
timeOnScreenSeconds: timeOnScreen,
|
||||||
message: message.isEmpty ? nil : message
|
message: message.isEmpty ? nil : message,
|
||||||
|
colorHex: selectedColorHex,
|
||||||
|
enabled: timer?.enabled ?? true
|
||||||
)
|
)
|
||||||
onSave(newTimer)
|
onSave(newTimer)
|
||||||
}
|
}
|
||||||
@@ -274,7 +335,7 @@ struct UserTimerEditSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(24)
|
||||||
.frame(width: 400)
|
.frame(width: 450)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,10 +347,10 @@ struct UserTimerEditSheet: View {
|
|||||||
UserTimersView(
|
UserTimersView(
|
||||||
userTimers: .constant([
|
userTimers: .constant([
|
||||||
UserTimer(
|
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(
|
UserTimer(
|
||||||
id: "2", type: .overlay, timeOnScreenSeconds: 60,
|
id: "2", title: "User Reminder 2", type: .overlay, timeOnScreenSeconds: 60,
|
||||||
message: "Stretch your legs"),
|
message: "Stretch your legs", colorHex: "3498DB"),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct SettingsWindowView: View {
|
|||||||
@State private var postureIntervalMinutes: Int
|
@State private var postureIntervalMinutes: Int
|
||||||
@State private var launchAtLogin: Bool
|
@State private var launchAtLogin: Bool
|
||||||
@State private var subtleReminderSizePercentage: Double
|
@State private var subtleReminderSizePercentage: Double
|
||||||
|
@State private var userTimers: [UserTimer]
|
||||||
|
|
||||||
init(settingsManager: SettingsManager, initialTab: Int = 0) {
|
init(settingsManager: SettingsManager, initialTab: Int = 0) {
|
||||||
self.settingsManager = settingsManager
|
self.settingsManager = settingsManager
|
||||||
@@ -38,6 +39,7 @@ struct SettingsWindowView: View {
|
|||||||
_launchAtLogin = State(initialValue: settingsManager.settings.launchAtLogin)
|
_launchAtLogin = State(initialValue: settingsManager.settings.launchAtLogin)
|
||||||
_subtleReminderSizePercentage = State(
|
_subtleReminderSizePercentage = State(
|
||||||
initialValue: settingsManager.settings.subtleReminderSizePercentage)
|
initialValue: settingsManager.settings.subtleReminderSizePercentage)
|
||||||
|
_userTimers = State(initialValue: settingsManager.settings.userTimers)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -71,7 +73,7 @@ struct SettingsWindowView: View {
|
|||||||
Label("Posture", systemImage: "figure.stand")
|
Label("Posture", systemImage: "figure.stand")
|
||||||
}
|
}
|
||||||
|
|
||||||
UserTimersView(userTimers: $settingsManager.settings.userTimers)
|
UserTimersView(userTimers: $userTimers)
|
||||||
.tag(3)
|
.tag(3)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("User Timers", systemImage: "plus.circle")
|
Label("User Timers", systemImage: "plus.circle")
|
||||||
@@ -107,7 +109,7 @@ struct SettingsWindowView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.frame(minWidth: 700, minHeight: 800)
|
.frame(minWidth: 700, minHeight: 750)
|
||||||
.onReceive(
|
.onReceive(
|
||||||
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
|
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
|
||||||
) { notification in
|
) { notification in
|
||||||
@@ -136,6 +138,10 @@ struct SettingsWindowView: View {
|
|||||||
|
|
||||||
settingsManager.settings.launchAtLogin = launchAtLogin
|
settingsManager.settings.launchAtLogin = launchAtLogin
|
||||||
settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage
|
settingsManager.settings.subtleReminderSizePercentage = subtleReminderSizePercentage
|
||||||
|
settingsManager.settings.userTimers = userTimers
|
||||||
|
|
||||||
|
// Save settings to persist changes
|
||||||
|
settingsManager.save()
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if launchAtLogin {
|
if launchAtLogin {
|
||||||
|
|||||||
Reference in New Issue
Block a user