Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion EyeBreak/EyeBreakApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
NSApp.setActivationPolicy(.accessory)

// Verify status bar is still visible after mode change
if let bar = self.statusBar, let item = bar.statusItem {
if let bar = self.statusBar, let _ = bar.statusItem {
}
}

Expand Down
58 changes: 44 additions & 14 deletions EyeBreak/Managers/BreakTimerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import Foundation
import Combine
import AppKit
import OSLog

private let log = Logger(subsystem: "com.eyebreak.app", category: "BreakTimerManager")

/// Manages the core timer logic for work/break cycles
class BreakTimerManager: ObservableObject {
Expand All @@ -26,6 +29,7 @@ class BreakTimerManager: ObservableObject {
private var timer: Timer?
private var remainingSeconds: Int = 0
private var idleDetector: IdleDetector?
private var presentationDetector: PresentationDetector?
private var cancellables = Set<AnyCancellable>()
private var wasWorkingBeforePause = false
private var isForcedBreak = false // Flag to bypass Smart Schedule during forced breaks
Expand All @@ -34,6 +38,7 @@ class BreakTimerManager: ObservableObject {

private init() {
setupIdleDetection()
setupPresentationDetection()
setupWorkspaceNotifications()
}

Expand Down Expand Up @@ -124,8 +129,9 @@ class BreakTimerManager: ObservableObject {
}

/// Pause the timer
func pause() {
func pause(reason: String = "manual") {
guard state.isActive else { return }
log.info("Timer paused — reason: \(reason), state: \(String(describing: self.state))")

let wasWorking: Bool
switch state {
Expand All @@ -144,8 +150,9 @@ class BreakTimerManager: ObservableObject {
}

/// Resume from paused state
func resume() {
func resume(reason: String = "manual") {
guard case .paused(let wasWorking, let seconds) = state else { return }
log.info("Timer resumed — reason: \(reason)")

remainingSeconds = seconds
if wasWorking {
Expand Down Expand Up @@ -175,6 +182,11 @@ class BreakTimerManager: ObservableObject {
if settings.idleDetectionEnabled {
idleDetector?.start()
}

// Start presentation detection if either flag is enabled
if settings.pauseWhenSharing || settings.pauseWhenWatchingMedia {
presentationDetector?.start()
}
}

private func tick() {
Expand Down Expand Up @@ -302,30 +314,48 @@ class BreakTimerManager: ObservableObject {

if isIdle && self.state.isActive {
// User went idle, pause timer
self.pause()
self.pause(reason: "idle")
NotificationManager.shared.sendIdlePausedNotification()
} else if !isIdle, case .paused = self.state {
// User returned, resume timer
self.resume()
self.resume(reason: "idle ended")
}
}
}

// MARK: - Presentation Detection Setup

private func setupPresentationDetection() {
presentationDetector = PresentationDetector()

presentationDetector?.onPresentationStateChanged = { [weak self] isPresenting in
guard let self = self else { return }

if isPresenting && self.state.isActive {
// User started presenting — pause the timer
self.pause(reason: "presenting")
} else if !isPresenting, case .paused = self.state {
// Presentation ended — resume the timer
self.resume(reason: "presentation ended")
}
}
}

// MARK: - Screen Lock and Sleep Handling

/// Sets up system notifications to automatically pause/resume timer during sleep and screen lock
private func setupWorkspaceNotifications() {
// Mac sleep events
NotificationCenter.default.publisher(for: NSWorkspace.willSleepNotification)
.sink { [weak self] _ in
self?.pause()
self?.pause(reason: "system sleep")
}
.store(in: &cancellables)

NotificationCenter.default.publisher(for: NSWorkspace.didWakeNotification)
.sink { [weak self] _ in
if case .paused = self?.state {
self?.resume()
self?.resume(reason: "system wake")
}
}
.store(in: &cancellables)
Expand All @@ -338,35 +368,35 @@ class BreakTimerManager: ObservableObject {
object: nil,
queue: .main
) { [weak self] _ in
self?.pause()
self?.pause(reason: "screen locked")
}

notificationCenter.addObserver(
forName: NSNotification.Name("com.apple.screenIsUnlocked"),
object: nil,
queue: .main
) { [weak self] _ in
if case .paused = self?.state {
self?.resume()
self?.resume(reason: "screen unlocked")
}
}

// Screen saver events (treated same as screen lock)
notificationCenter.addObserver(
forName: NSNotification.Name("com.apple.screensaver.didstart"),
object: nil,
queue: .main
) { [weak self] _ in
self?.pause()
self?.pause(reason: "screensaver started")
}

notificationCenter.addObserver(
forName: NSNotification.Name("com.apple.screensaver.didstop"),
object: nil,
queue: .main
) { [weak self] _ in
if case .paused = self?.state {
self?.resume()
self?.resume(reason: "screensaver stopped")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion EyeBreak/Managers/IdleDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class IdleDetector {

if let property = property?.takeRetainedValue() {
var idleNanos: Int64 = 0
CFNumberGetValue(property as! CFNumber, .sInt64Type, &idleNanos)
CFNumberGetValue((property as! CFNumber), .sInt64Type, &idleNanos)
idleTime = TimeInterval(idleNanos) / TimeInterval(NSEC_PER_SEC)
}
}
Expand Down
2 changes: 1 addition & 1 deletion EyeBreak/Managers/LaunchAtLoginManager.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
 //
//
// LaunchAtLoginManager.swift
// EyeBreak
//
Expand Down
28 changes: 5 additions & 23 deletions EyeBreak/Managers/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,7 @@ class NotificationManager: NSObject {
// MARK: - Authorization

func requestAuthorization() {
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
}

if granted {
} else {
}
center.requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in
}
}

Expand All @@ -56,10 +50,7 @@ class NotificationManager: NSObject {
trigger: nil
)

center.add(request) { error in
if let error = error {
}
}
center.add(request) { _ in }
}

func sendBreakStartNotification() {
Expand All @@ -74,10 +65,7 @@ class NotificationManager: NSObject {
trigger: nil
)

center.add(request) { error in
if let error = error {
}
}
center.add(request) { _ in }
}

func sendBreakCompleteNotification() {
Expand All @@ -92,10 +80,7 @@ class NotificationManager: NSObject {
trigger: nil
)

center.add(request) { error in
if let error = error {
}
}
center.add(request) { _ in }
}

func sendIdlePausedNotification() {
Expand All @@ -110,10 +95,7 @@ class NotificationManager: NSObject {
trigger: nil
)

center.add(request) { error in
if let error = error {
}
}
center.add(request) { _ in }
}

func cancelAllNotifications() {
Expand Down
Loading