general:add settings window
This commit is contained in:
@@ -11,16 +11,20 @@ import Combine
|
||||
|
||||
@MainActor
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
var timerEngine: TimerEngine?
|
||||
private var statusItem: NSStatusItem?
|
||||
private var popover: NSPopover?
|
||||
private var timerEngine: TimerEngine?
|
||||
private var settingsManager: SettingsManager?
|
||||
private var reminderWindowController: NSWindowController?
|
||||
private var settingsWindowController: NSWindowController?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var timerStateBeforeSleep: [TimerType: Date] = [:]
|
||||
private var hasStartedTimers = false
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Set activation policy to hide dock icon
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
|
||||
settingsManager = SettingsManager.shared
|
||||
timerEngine = TimerEngine(settingsManager: settingsManager!)
|
||||
|
||||
@@ -38,6 +42,44 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
startTimers()
|
||||
}
|
||||
|
||||
private func setupMenuBar() {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
|
||||
if let button = statusItem?.button {
|
||||
button.image = NSImage(systemSymbolName: "eye.fill", accessibilityDescription: "Gaze")
|
||||
button.action = #selector(togglePopover)
|
||||
button.target = self
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func togglePopover() {
|
||||
if let popover = popover, popover.isShown {
|
||||
popover.close()
|
||||
} else {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
|
||||
private func showPopover() {
|
||||
let popover = NSPopover()
|
||||
popover.contentSize = NSSize(width: 300, height: 400)
|
||||
popover.behavior = .transient
|
||||
popover.contentViewController = NSHostingController(
|
||||
rootView: MenuBarContentView(
|
||||
timerEngine: timerEngine!,
|
||||
settingsManager: settingsManager!,
|
||||
onQuit: { NSApplication.shared.terminate(nil) },
|
||||
onOpenSettings: { [weak self] in self?.openSettings() }
|
||||
)
|
||||
)
|
||||
|
||||
if let button = statusItem?.button {
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
}
|
||||
|
||||
self.popover = popover
|
||||
}
|
||||
|
||||
private func startTimers() {
|
||||
guard !hasStartedTimers else { return }
|
||||
hasStartedTimers = true
|
||||
@@ -50,6 +92,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
.sink { [weak self] settings in
|
||||
if settings.hasCompletedOnboarding {
|
||||
self?.startTimers()
|
||||
} else if self?.hasStartedTimers == true {
|
||||
// Restart timers when settings change (only if already started)
|
||||
self?.timerEngine?.start()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -111,43 +156,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
timerEngine.resume()
|
||||
}
|
||||
|
||||
private func setupMenuBar() {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
|
||||
if let button = statusItem?.button {
|
||||
button.image = NSImage(systemSymbolName: "eye.fill", accessibilityDescription: "Gaze")
|
||||
button.action = #selector(togglePopover)
|
||||
button.target = self
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func togglePopover() {
|
||||
if let popover = popover, popover.isShown {
|
||||
popover.close()
|
||||
} else {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
|
||||
private func showPopover() {
|
||||
let popover = NSPopover()
|
||||
popover.contentSize = NSSize(width: 300, height: 400)
|
||||
popover.behavior = .transient
|
||||
popover.contentViewController = NSHostingController(
|
||||
rootView: MenuBarContentView(
|
||||
timerEngine: timerEngine!,
|
||||
settingsManager: settingsManager!,
|
||||
onQuit: { NSApplication.shared.terminate(nil) }
|
||||
)
|
||||
)
|
||||
|
||||
if let button = statusItem?.button {
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
}
|
||||
|
||||
self.popover = popover
|
||||
}
|
||||
|
||||
private func observeReminderEvents() {
|
||||
timerEngine?.$activeReminder
|
||||
.sink { [weak self] reminder in
|
||||
@@ -218,4 +226,45 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
func getMenuBarIconPosition() -> NSRect? {
|
||||
return statusItem?.button?.window?.frame
|
||||
}
|
||||
|
||||
// Public method to open settings window
|
||||
func openSettings() {
|
||||
// If window already exists, just bring it to front
|
||||
if let existingWindow = settingsWindowController?.window {
|
||||
existingWindow.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
return
|
||||
}
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 600, height: 550),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.title = "Settings"
|
||||
window.center()
|
||||
window.setFrameAutosaveName("SettingsWindow")
|
||||
window.isReleasedWhenClosed = false
|
||||
window.contentView = NSHostingView(
|
||||
rootView: SettingsWindowView(settingsManager: settingsManager!)
|
||||
)
|
||||
|
||||
let windowController = NSWindowController(window: window)
|
||||
windowController.showWindow(nil)
|
||||
|
||||
settingsWindowController = windowController
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
// Observe when window is closed to clean up reference
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSWindow.willCloseNotification,
|
||||
object: window,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.settingsWindowController = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ struct MenuBarContentView: View {
|
||||
@ObservedObject var timerEngine: TimerEngine
|
||||
@ObservedObject var settingsManager: SettingsManager
|
||||
var onQuit: () -> Void
|
||||
var onOpenSettings: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@@ -108,7 +109,7 @@ struct MenuBarContentView: View {
|
||||
.buttonStyle(MenuBarHoverButtonStyle())
|
||||
|
||||
Button(action: {
|
||||
// TODO: Open settings window
|
||||
onOpenSettings()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "gearshape")
|
||||
@@ -230,6 +231,7 @@ struct TimerStatusRow: View {
|
||||
MenuBarContentView(
|
||||
timerEngine: timerEngine,
|
||||
settingsManager: settingsManager,
|
||||
onQuit: {}
|
||||
onQuit: {},
|
||||
onOpenSettings: {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ struct OnboardingContainerView: View {
|
||||
minWidth: 100, maxWidth: .infinity, minHeight: 44,
|
||||
maxHeight: 44, alignment: .center
|
||||
)
|
||||
.foregroundColor(.black)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||
}
|
||||
|
||||
@@ -9,24 +9,24 @@ import SwiftUI
|
||||
|
||||
struct SettingsOnboardingView: View {
|
||||
@Binding var launchAtLogin: Bool
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
Spacer()
|
||||
|
||||
|
||||
Image(systemName: "gearshape.fill")
|
||||
.font(.system(size: 80))
|
||||
.foregroundColor(.blue)
|
||||
|
||||
|
||||
Text("Final 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 {
|
||||
@@ -46,13 +46,13 @@ struct SettingsOnboardingView: View {
|
||||
}
|
||||
.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") {
|
||||
@@ -79,7 +79,7 @@ struct SettingsOnboardingView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||
|
||||
|
||||
// Buy Me a Coffee
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://buymeacoffee.com/placeholder") {
|
||||
@@ -112,14 +112,14 @@ struct SettingsOnboardingView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 600, height: 450)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
|
||||
|
||||
private func applyLaunchAtLoginSetting(enabled: Bool) {
|
||||
do {
|
||||
if enabled {
|
||||
|
||||
137
Gaze/Views/SettingsWindowView.swift
Normal file
137
Gaze/Views/SettingsWindowView.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// SettingsWindowView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsWindowView: View {
|
||||
@ObservedObject var settingsManager: SettingsManager
|
||||
@State private var currentTab = 0
|
||||
@State private var lookAwayEnabled: Bool
|
||||
@State private var lookAwayIntervalMinutes: Int
|
||||
@State private var lookAwayCountdownSeconds: Int
|
||||
@State private var blinkEnabled: Bool
|
||||
@State private var blinkIntervalMinutes: Int
|
||||
@State private var postureEnabled: Bool
|
||||
@State private var postureIntervalMinutes: Int
|
||||
@State private var launchAtLogin: Bool
|
||||
|
||||
init(settingsManager: SettingsManager) {
|
||||
self.settingsManager = settingsManager
|
||||
|
||||
_lookAwayEnabled = State(initialValue: settingsManager.settings.lookAwayTimer.enabled)
|
||||
_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)
|
||||
_postureEnabled = State(initialValue: settingsManager.settings.postureTimer.enabled)
|
||||
_postureIntervalMinutes = State(initialValue: settingsManager.settings.postureTimer.intervalSeconds / 60)
|
||||
_launchAtLogin = State(initialValue: settingsManager.settings.launchAtLogin)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
TabView(selection: $currentTab) {
|
||||
LookAwaySetupView(
|
||||
enabled: $lookAwayEnabled,
|
||||
intervalMinutes: $lookAwayIntervalMinutes,
|
||||
countdownSeconds: $lookAwayCountdownSeconds
|
||||
)
|
||||
.tag(0)
|
||||
.tabItem {
|
||||
Label("Look Away", systemImage: "eye.fill")
|
||||
}
|
||||
|
||||
BlinkSetupView(
|
||||
enabled: $blinkEnabled,
|
||||
intervalMinutes: $blinkIntervalMinutes
|
||||
)
|
||||
.tag(1)
|
||||
.tabItem {
|
||||
Label("Blink", systemImage: "eye.circle.fill")
|
||||
}
|
||||
|
||||
PostureSetupView(
|
||||
enabled: $postureEnabled,
|
||||
intervalMinutes: $postureIntervalMinutes
|
||||
)
|
||||
.tag(2)
|
||||
.tabItem {
|
||||
Label("Posture", systemImage: "figure.stand")
|
||||
}
|
||||
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: $launchAtLogin
|
||||
)
|
||||
.tag(3)
|
||||
.tabItem {
|
||||
Label("General", systemImage: "gearshape.fill")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
closeWindow()
|
||||
}
|
||||
.keyboardShortcut(.escape)
|
||||
|
||||
Button("Apply") {
|
||||
applySettings()
|
||||
closeWindow()
|
||||
}
|
||||
.keyboardShortcut(.return)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(width: 600, height: 550)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
do {
|
||||
if launchAtLogin {
|
||||
try LaunchAtLoginManager.enable()
|
||||
} else {
|
||||
try LaunchAtLoginManager.disable()
|
||||
}
|
||||
} catch {
|
||||
print("Failed to set launch at login: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func closeWindow() {
|
||||
if let window = NSApplication.shared.windows.first(where: { $0.title == "Settings" }) {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsWindowView(settingsManager: SettingsManager.shared)
|
||||
}
|
||||
Reference in New Issue
Block a user