This commit is contained in:
Michael Freno
2026-01-27 18:46:15 -05:00
parent f8868c9253
commit 224f6d2a68
8 changed files with 108 additions and 82 deletions

View File

@@ -15,6 +15,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private let serviceContainer: ServiceContainer private let serviceContainer: ServiceContainer
private let windowManager: WindowManaging private let windowManager: WindowManaging
private var updateManager: UpdateManager? private var updateManager: UpdateManager?
private var systemSleepManager: SystemSleepManager?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var hasStartedTimers = false private var hasStartedTimers = false
@@ -46,6 +47,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
timerEngine = serviceContainer.timerEngine timerEngine = serviceContainer.timerEngine
systemSleepManager = SystemSleepManager(
timerEngine: timerEngine,
settingsManager: settingsManager
)
systemSleepManager?.startObserving()
serviceContainer.setupSmartModeServices() serviceContainer.setupSmartModeServices()
@@ -54,8 +60,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
updateManager = UpdateManager.shared updateManager = UpdateManager.shared
} }
setupLifecycleObservers()
observeSettingsChanges() observeSettingsChanges()
if settingsManager.settings.hasCompletedOnboarding { if settingsManager.settings.hasCompletedOnboarding {
@@ -129,34 +133,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
logInfo(" applicationWill terminate") logInfo(" applicationWill terminate")
settingsManager.saveImmediately() settingsManager.saveImmediately()
stopLifecycleObservers()
timerEngine?.stop() timerEngine?.stop()
} }
private func setupLifecycleObservers() { private func stopLifecycleObservers() {
NSWorkspace.shared.notificationCenter.addObserver( systemSleepManager?.stopObserving()
self, systemSleepManager = nil
selector: #selector(systemWillSleep),
name: NSWorkspace.willSleepNotification,
object: nil
)
NSWorkspace.shared.notificationCenter.addObserver(
self,
selector: #selector(systemDidWake),
name: NSWorkspace.didWakeNotification,
object: nil
)
}
@objc private func systemWillSleep() {
logInfo("System will sleep")
timerEngine?.handleSystemSleep()
settingsManager.saveImmediately()
}
@objc private func systemDidWake() {
logInfo("System did wake")
timerEngine?.handleSystemWake()
} }
private func observeReminderEvents() { private func observeReminderEvents() {

View File

@@ -60,12 +60,6 @@ protocol TimerEngineProviding: AnyObject, ObservableObject {
/// Checks if a timer is currently paused /// Checks if a timer is currently paused
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool func isTimerPaused(_ identifier: TimerIdentifier) -> Bool
/// Handles system sleep event
func handleSystemSleep()
/// Handles system wake event
func handleSystemWake()
/// Sets up smart mode with fullscreen and idle detection services /// Sets up smart mode with fullscreen and idle detection services
func setupSmartMode( func setupSmartMode(
fullscreenService: FullscreenDetectionService?, fullscreenService: FullscreenDetectionService?,

View File

@@ -0,0 +1,63 @@
//
// SystemSleepManager.swift
// Gaze
//
// Coordinates system sleep/wake handling.
//
import AppKit
import Foundation
@MainActor
final class SystemSleepManager {
private let settingsManager: any SettingsProviding
private weak var timerEngine: (any TimerEngineProviding)?
private var observers: [NSObjectProtocol] = []
init(timerEngine: (any TimerEngineProviding)?, settingsManager: any SettingsProviding) {
self.timerEngine = timerEngine
self.settingsManager = settingsManager
}
func startObserving() {
guard observers.isEmpty else { return }
let center = NSWorkspace.shared.notificationCenter
let willSleep = center.addObserver(
forName: NSWorkspace.willSleepNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleSystemWillSleep()
}
let didWake = center.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleSystemDidWake()
}
observers = [willSleep, didWake]
}
func stopObserving() {
let center = NSWorkspace.shared.notificationCenter
for observer in observers {
center.removeObserver(observer)
}
observers.removeAll()
}
private func handleSystemWillSleep() {
logInfo("System will sleep")
timerEngine?.stop()
settingsManager.saveImmediately()
}
private func handleSystemDidWake() {
logInfo("System did wake")
timerEngine?.start()
}
}

View File

@@ -138,7 +138,7 @@ class TimerEngine: ObservableObject {
let intervalSeconds = getTimerInterval(for: identifier) let intervalSeconds = getTimerInterval(for: identifier)
stateManager.resetTimer(identifier: identifier, intervalSeconds: intervalSeconds) stateManager.resetTimer(identifier: identifier, intervalSeconds: intervalSeconds)
} }
/// Unified way to get interval for any timer type /// Unified way to get interval for any timer type
private func getTimerInterval(for identifier: TimerIdentifier) -> Int { private func getTimerInterval(for identifier: TimerIdentifier) -> Int {
switch identifier { switch identifier {
@@ -146,7 +146,8 @@ class TimerEngine: ObservableObject {
let config = settingsProvider.timerConfiguration(for: type) let config = settingsProvider.timerConfiguration(for: type)
return config.intervalSeconds return config.intervalSeconds
case .user(let id): case .user(let id):
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else { guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id })
else {
return 0 return 0
} }
return userTimer.intervalMinutes * 60 return userTimer.intervalMinutes * 60
@@ -183,7 +184,8 @@ class TimerEngine: ObservableObject {
secondsRemaining: updatedState.remainingSeconds secondsRemaining: updatedState.remainingSeconds
) { ) {
Task { @MainActor in Task { @MainActor in
await reminderService.prepareEnforceMode(secondsRemaining: updatedState.remainingSeconds) await reminderService.prepareEnforceMode(
secondsRemaining: updatedState.remainingSeconds)
} }
} }
@@ -215,22 +217,6 @@ class TimerEngine: ObservableObject {
return stateManager.isTimerPaused(identifier) return stateManager.isTimerPaused(identifier)
} }
// System sleep/wake handling is now managed by SystemSleepManager
// This method is kept for compatibility but will be removed in future versions
/// Handles system sleep event - deprecated
@available(*, deprecated, message: "Use SystemSleepManager instead")
func handleSystemSleep() {
logDebug("System going to sleep (deprecated)")
// This functionality has been moved to SystemSleepManager
}
/// Handles system wake event - deprecated
@available(*, deprecated, message: "Use SystemSleepManager instead")
func handleSystemWake() {
logDebug("System waking up (deprecated)")
// This functionality has been moved to SystemSleepManager
}
private func timerConfigurations() -> [TimerIdentifier: TimerConfiguration] { private func timerConfigurations() -> [TimerIdentifier: TimerConfiguration] {
var configurations: [TimerIdentifier: TimerConfiguration] = [:] var configurations: [TimerIdentifier: TimerConfiguration] = [:]
for timerType in TimerType.allCases { for timerType in TimerType.allCases {

View File

@@ -21,7 +21,7 @@ struct AdditionalModifiersView: View {
GeometryReader { geometry in GeometryReader { geometry in
let availableWidth = geometry.size.width - 80 // Account for padding let availableWidth = geometry.size.width - 80 // Account for padding
let availableHeight = geometry.size.height - 200 // Account for header and nav let availableHeight = geometry.size.height - 200 // Account for header and nav
let cardWidth = min( let cardWidth = min(
max(availableWidth * 0.85, AdaptiveLayout.Card.minWidth), max(availableWidth * 0.85, AdaptiveLayout.Card.minWidth),
AdaptiveLayout.Card.maxWidth AdaptiveLayout.Card.maxWidth
@@ -30,9 +30,10 @@ struct AdditionalModifiersView: View {
max(availableHeight * 0.75, AdaptiveLayout.Card.minHeight), max(availableHeight * 0.75, AdaptiveLayout.Card.minHeight),
AdaptiveLayout.Card.maxHeight AdaptiveLayout.Card.maxHeight
) )
VStack(spacing: 0) { VStack(spacing: 0) {
SetupHeader(icon: "slider.horizontal.3", title: "Additional Options", color: .purple) SetupHeader(
icon: "slider.horizontal.3", title: "Additional Options", color: .purple)
Text("Optional features to enhance your experience") Text("Optional features to enhance your experience")
.font(isCompact ? .subheadline : .title3) .font(isCompact ? .subheadline : .title3)
@@ -77,7 +78,7 @@ struct AdditionalModifiersView: View {
HStack(spacing: isCompact ? 10 : 16) { HStack(spacing: isCompact ? 10 : 16) {
cardIndicator(index: 0, icon: "video.fill", label: "Enforce") cardIndicator(index: 0, icon: "video.fill", label: "Enforce")
cardIndicator(index: 1, icon: "brain.fill", label: "Smart") cardIndicator(index: 1, icon: "brain.fill", label: "Smart")
} }.padding(.all, 20)
Button(action: { swapCards() }) { Button(action: { swapCards() }) {
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
@@ -211,7 +212,11 @@ struct AdditionalModifiersView: View {
private var enforceModeContent: some View { private var enforceModeContent: some View {
VStack(spacing: isCompact ? 10 : 16) { VStack(spacing: isCompact ? 10 : 16) {
Image(systemName: "video.fill") Image(systemName: "video.fill")
.font(.system(size: isCompact ? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon)) .font(
.system(
size: isCompact
? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon)
)
.foregroundStyle(Color.accentColor) .foregroundStyle(Color.accentColor)
Text("Enforce Mode") Text("Enforce Mode")
@@ -292,7 +297,11 @@ struct AdditionalModifiersView: View {
private var smartModeContent: some View { private var smartModeContent: some View {
VStack(spacing: isCompact ? 10 : 16) { VStack(spacing: isCompact ? 10 : 16) {
Image(systemName: "brain.fill") Image(systemName: "brain.fill")
.font(.system(size: isCompact ? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon)) .font(
.system(
size: isCompact
? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon)
)
.foregroundStyle(.purple) .foregroundStyle(.purple)
Text("Smart Mode") Text("Smart Mode")

View File

@@ -118,13 +118,13 @@ final class MenuBarGuideOverlayPresenter {
let overlayView = MenuBarGuideOverlayView() let overlayView = MenuBarGuideOverlayView()
window.contentView = NSHostingView(rootView: overlayView) window.contentView = NSHostingView(rootView: overlayView)
} }
func setupOnboardingWindowObserver() { func setupOnboardingWindowObserver() {
// Remove any existing observer to prevent duplicates // Remove any existing observer to prevent duplicates
if let observer = onboardingWindowObserver { if let observer = onboardingWindowObserver {
NotificationCenter.default.removeObserver(observer) NotificationCenter.default.removeObserver(observer)
} }
// Add observer for when the onboarding window is closed // Add observer for when the onboarding window is closed
onboardingWindowObserver = NotificationCenter.default.addObserver( onboardingWindowObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification, forName: NSWindow.willCloseNotification,
@@ -132,10 +132,11 @@ final class MenuBarGuideOverlayPresenter {
queue: .main queue: .main
) { [weak self] notification in ) { [weak self] notification in
guard let window = notification.object as? NSWindow, guard let window = notification.object as? NSWindow,
window.identifier == WindowIdentifiers.onboarding else { window.identifier == WindowIdentifiers.onboarding
else {
return return
} }
// Hide the overlay when onboarding window closes // Hide the overlay when onboarding window closes
self?.hide() self?.hide()
} }
@@ -176,8 +177,8 @@ struct MenuBarGuideOverlayView: View {
$0.identifier == WindowIdentifiers.onboarding $0.identifier == WindowIdentifiers.onboarding
}) { }) {
let windowFrame = onboardingWindow.frame let windowFrame = onboardingWindow.frame
let textRightX = windowFrame.midX + 210 let textRightX = windowFrame.midX
let textY = screenFrame.maxY - windowFrame.maxY + 505 let textY = screenFrame.maxY - windowFrame.maxY + 305
return CGPoint(x: textRightX, y: textY) return CGPoint(x: textRightX, y: textY)
} }
return CGPoint(x: screenSize.width * 0.5, y: screenSize.height * 0.45) return CGPoint(x: screenSize.width * 0.5, y: screenSize.height * 0.45)
@@ -195,7 +196,6 @@ struct HandDrawnArrowShape: Shape {
// This creates a more playful, hand-drawn feel // This creates a more playful, hand-drawn feel
let dx = end.x - start.x let dx = end.x - start.x
let dy = end.y - start.y
// First control point: go DOWN and slightly toward target // First control point: go DOWN and slightly toward target
let ctrl1 = CGPoint( let ctrl1 = CGPoint(
@@ -203,23 +203,14 @@ struct HandDrawnArrowShape: Shape {
y: start.y + 120 // Go DOWN first y: start.y + 120 // Go DOWN first
) )
// Second control point: curve back up toward target
let ctrl2 = CGPoint( let ctrl2 = CGPoint(
x: start.x + dx * 0.6, x: start.x + dx * 0.6,
y: start.y + 80 y: start.y + 80
) )
// Third control point: approach target from below-ish
let ctrl3 = CGPoint(
x: end.x - dx * 0.15,
y: end.y + 60
)
// Add slight hand-drawn wobble
let wobble: CGFloat = 2.5 let wobble: CGFloat = 2.5
let wobbledCtrl1 = CGPoint(x: ctrl1.x + wobble, y: ctrl1.y - wobble) let wobbledCtrl1 = CGPoint(x: ctrl1.x + wobble, y: ctrl1.y - wobble)
let wobbledCtrl2 = CGPoint(x: ctrl2.x - wobble, y: ctrl2.y + wobble) let wobbledCtrl2 = CGPoint(x: ctrl2.x - wobble, y: ctrl2.y + wobble)
let wobbledCtrl3 = CGPoint(x: ctrl3.x + wobble * 0.5, y: ctrl3.y - wobble)
path.move(to: start) path.move(to: start)
path.addCurve(to: end, control1: wobbledCtrl1, control2: wobbledCtrl2) path.addCurve(to: end, control1: wobbledCtrl1, control2: wobbledCtrl2)

View File

@@ -136,7 +136,7 @@ struct OnboardingContainerView: View {
.tag(0) .tag(0)
.tabItem { Image(systemName: "hand.wave.fill") } .tabItem { Image(systemName: "hand.wave.fill") }
MenuBarWelcomeView() MenuBarTargetView()
.tag(1) .tag(1)
.tabItem { Image(systemName: "menubar.rectangle") } .tabItem { Image(systemName: "menubar.rectangle") }

View File

@@ -1,5 +1,5 @@
// //
// MenuBarWelcomeView.swift // MenuBarTargetView.swift
// Gaze // Gaze
// //
// Created by Mike Freno on 1/17/26. // Created by Mike Freno on 1/17/26.
@@ -7,21 +7,21 @@
import SwiftUI import SwiftUI
struct MenuBarWelcomeView: View { struct MenuBarTargetView: View {
@Environment(\.isCompactLayout) private var isCompact @Environment(\.isCompactLayout) private var isCompact
private var iconSize: CGFloat { private var iconSize: CGFloat {
isCompact ? AdaptiveLayout.Font.heroIconSmall : AdaptiveLayout.Font.heroIcon isCompact ? AdaptiveLayout.Font.heroIconSmall : AdaptiveLayout.Font.heroIcon
} }
private var titleSize: CGFloat { private var titleSize: CGFloat {
isCompact ? AdaptiveLayout.Font.heroTitleSmall : AdaptiveLayout.Font.heroTitle isCompact ? AdaptiveLayout.Font.heroTitleSmall : AdaptiveLayout.Font.heroTitle
} }
private var spacing: CGFloat { private var spacing: CGFloat {
isCompact ? AdaptiveLayout.Spacing.compact : AdaptiveLayout.Spacing.standard isCompact ? AdaptiveLayout.Spacing.compact : AdaptiveLayout.Spacing.standard
} }
var body: some View { var body: some View {
VStack(spacing: spacing * 1.5) { VStack(spacing: spacing * 1.5) {
Spacer() Spacer()
@@ -64,5 +64,5 @@ struct MenuBarWelcomeView: View {
} }
#Preview("Menu Bar Welcome") { #Preview("Menu Bar Welcome") {
MenuBarWelcomeView() MenuBarTargetView()
} }