This commit is contained in:
Michael Freno
2026-01-16 01:22:31 -05:00
parent 4ae8d77dab
commit ea3478dfb9
15 changed files with 139 additions and 60 deletions

View File

@@ -431,13 +431,14 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Gaze/Info.plist; INFOPLIST_FILE = Gaze/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Gaze;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 0.4.1; MARKETING_VERSION = 0.4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -467,13 +468,14 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Gaze/Info.plist; INFOPLIST_FILE = Gaze/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Gaze;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 0.4.1; MARKETING_VERSION = 0.4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -8,7 +8,8 @@
import Foundation import Foundation
/// Thread-safe configuration holder for eye tracking thresholds. /// Thread-safe configuration holder for eye tracking thresholds.
enum EyeTrackingConstants { /// All properties are Sendable constants, safe for use in any concurrency context.
enum EyeTrackingConstants: Sendable {
// MARK: - Logging // MARK: - Logging
/// Interval between log messages in seconds /// Interval between log messages in seconds
static let logInterval: TimeInterval = 0.5 static let logInterval: TimeInterval = 0.5

View File

@@ -31,8 +31,7 @@ struct GazeApp: App {
} }
} else { } else {
OnboardingContainerView(settingsManager: settingsManager) OnboardingContainerView(settingsManager: settingsManager)
.onChange(of: settingsManager.settings.hasCompletedOnboarding) { .onChange(of: settingsManager.settings.hasCompletedOnboarding) { _, completed in
completed in
if completed { if completed {
closeAllWindows() closeAllWindows()
appDelegate.onboardingCompleted() appDelegate.onboardingCompleted()

View File

@@ -68,6 +68,24 @@ class EyeTrackingService: NSObject, ObservableObject {
var debugRightPupilRatio: Double? var debugRightPupilRatio: Double?
var debugYaw: Double? var debugYaw: Double?
var debugPitch: Double? var debugPitch: Double?
nonisolated init(
faceDetected: Bool = false,
isEyesClosed: Bool = false,
userLookingAtScreen: Bool = true,
debugLeftPupilRatio: Double? = nil,
debugRightPupilRatio: Double? = nil,
debugYaw: Double? = nil,
debugPitch: Double? = nil
) {
self.faceDetected = faceDetected
self.isEyesClosed = isEyesClosed
self.userLookingAtScreen = userLookingAtScreen
self.debugLeftPupilRatio = debugLeftPupilRatio
self.debugRightPupilRatio = debugRightPupilRatio
self.debugYaw = debugYaw
self.debugPitch = debugPitch
}
} }
func startEyeTracking() async throws { func startEyeTracking() async throws {
@@ -277,6 +295,20 @@ class EyeTrackingService: NSObject, ObservableObject {
var rightPupilRatio: Double? var rightPupilRatio: Double?
var yaw: Double? var yaw: Double?
var pitch: Double? var pitch: Double?
nonisolated init(
lookingAway: Bool = false,
leftPupilRatio: Double? = nil,
rightPupilRatio: Double? = nil,
yaw: Double? = nil,
pitch: Double? = nil
) {
self.lookingAway = lookingAway
self.leftPupilRatio = leftPupilRatio
self.rightPupilRatio = rightPupilRatio
self.yaw = yaw
self.pitch = pitch
}
} }
/// Non-isolated gaze direction detection /// Non-isolated gaze direction detection

View File

@@ -71,16 +71,23 @@ final class FullscreenDetectionService: ObservableObject {
private let permissionManager: ScreenCapturePermissionManaging private let permissionManager: ScreenCapturePermissionManaging
private let environmentProvider: FullscreenEnvironmentProviding private let environmentProvider: FullscreenEnvironmentProviding
// This initializer is only for use within main actor contexts
init( init(
permissionManager: ScreenCapturePermissionManaging = ScreenCapturePermissionManager.shared, permissionManager: ScreenCapturePermissionManaging,
environmentProvider: FullscreenEnvironmentProviding = SystemFullscreenEnvironmentProvider() environmentProvider: FullscreenEnvironmentProviding
) { ) {
self.permissionManager = permissionManager self.permissionManager = permissionManager
self.environmentProvider = environmentProvider self.environmentProvider = environmentProvider
setupObservers() setupObservers()
} }
/// Convenience initializer using default services
convenience init() {
self.init(
permissionManager: ScreenCapturePermissionManager.shared,
environmentProvider: SystemFullscreenEnvironmentProvider()
)
}
// Factory method to safely create instances from non-main actor contexts // Factory method to safely create instances from non-main actor contexts
static func create( static func create(
permissionManager: ScreenCapturePermissionManaging? = nil, permissionManager: ScreenCapturePermissionManaging? = nil,
@@ -109,8 +116,10 @@ final class FullscreenDetectionService: ObservableObject {
object: workspace, object: workspace,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
Task { @MainActor in
self?.checkFullscreenState() self?.checkFullscreenState()
} }
}
observers.append(spaceObserver) observers.append(spaceObserver)
let transitionObserver = notificationCenter.addObserver( let transitionObserver = notificationCenter.addObserver(
@@ -118,8 +127,10 @@ final class FullscreenDetectionService: ObservableObject {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
Task { @MainActor in
self?.checkFullscreenState() self?.checkFullscreenState()
} }
}
observers.append(transitionObserver) observers.append(transitionObserver)
let fullscreenObserver = notificationCenter.addObserver( let fullscreenObserver = notificationCenter.addObserver(
@@ -127,8 +138,10 @@ final class FullscreenDetectionService: ObservableObject {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
Task { @MainActor in
self?.checkFullscreenState() self?.checkFullscreenState()
} }
}
observers.append(fullscreenObserver) observers.append(fullscreenObserver)
let exitFullscreenObserver = notificationCenter.addObserver( let exitFullscreenObserver = notificationCenter.addObserver(
@@ -136,8 +149,10 @@ final class FullscreenDetectionService: ObservableObject {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
Task { @MainActor in
self?.checkFullscreenState() self?.checkFullscreenState()
} }
}
observers.append(exitFullscreenObserver) observers.append(exitFullscreenObserver)
frontmostAppObserver = NotificationCenter.default.publisher( frontmostAppObserver = NotificationCenter.default.publisher(

View File

@@ -33,8 +33,9 @@ class IdleMonitoringService: ObservableObject {
private func startMonitoring() { private func startMonitoring() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
Task { @MainActor in Task { @MainActor in
self?.checkIdleState() self.checkIdleState()
} }
} }
} }

View File

@@ -38,13 +38,13 @@ final class PupilCalibration: @unchecked Sendable {
private var thresholdsLeft: [Int] = [] private var thresholdsLeft: [Int] = []
private var thresholdsRight: [Int] = [] private var thresholdsRight: [Int] = []
var isComplete: Bool { nonisolated var isComplete: Bool {
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
return thresholdsLeft.count >= targetFrames && thresholdsRight.count >= targetFrames return thresholdsLeft.count >= targetFrames && thresholdsRight.count >= targetFrames
} }
func threshold(forSide side: Int) -> Int { nonisolated func threshold(forSide side: Int) -> Int {
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
let thresholds = side == 0 ? thresholdsLeft : thresholdsRight let thresholds = side == 0 ? thresholdsLeft : thresholdsRight
@@ -52,7 +52,7 @@ final class PupilCalibration: @unchecked Sendable {
return thresholds.reduce(0, +) / thresholds.count return thresholds.reduce(0, +) / thresholds.count
} }
func evaluate(eyeData: UnsafePointer<UInt8>, width: Int, height: Int, side: Int) { nonisolated func evaluate(eyeData: UnsafePointer<UInt8>, width: Int, height: Int, side: Int) {
let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height) let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height)
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
@@ -63,7 +63,7 @@ final class PupilCalibration: @unchecked Sendable {
} }
} }
private func findBestThreshold(eyeData: UnsafePointer<UInt8>, width: Int, height: Int) -> Int { private nonisolated func findBestThreshold(eyeData: UnsafePointer<UInt8>, width: Int, height: Int) -> Int {
let averageIrisSize = 0.48 let averageIrisSize = 0.48
var bestThreshold = 50 var bestThreshold = 50
var bestDiff = Double.greatestFiniteMagnitude var bestDiff = Double.greatestFiniteMagnitude
@@ -91,7 +91,7 @@ final class PupilCalibration: @unchecked Sendable {
return bestThreshold return bestThreshold
} }
private static func irisSize(data: UnsafePointer<UInt8>, width: Int, height: Int) -> Double { private nonisolated static func irisSize(data: UnsafePointer<UInt8>, width: Int, height: Int) -> Double {
let margin = 5 let margin = 5
guard width > margin * 2, height > margin * 2 else { return 0 } guard width > margin * 2, height > margin * 2 else { return 0 }
@@ -112,7 +112,7 @@ final class PupilCalibration: @unchecked Sendable {
return totalCount > 0 ? Double(blackCount) / Double(totalCount) : 0 return totalCount > 0 ? Double(blackCount) / Double(totalCount) : 0
} }
func reset() { nonisolated func reset() {
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
thresholdsLeft.removeAll() thresholdsLeft.removeAll()
@@ -143,46 +143,46 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Configuration // MARK: - Configuration
static var enableDebugImageSaving = false nonisolated(unsafe) static var enableDebugImageSaving = false
static var enablePerformanceLogging = false nonisolated(unsafe) static var enablePerformanceLogging = false
static var frameSkipCount = 10 // Process every Nth frame nonisolated(unsafe) static var frameSkipCount = 10 // Process every Nth frame
// MARK: - State (protected by lock) // MARK: - State (protected by lock)
private static var _debugImageCounter = 0 private nonisolated(unsafe) static var _debugImageCounter = 0
private static var _frameCounter = 0 private nonisolated(unsafe) static var _frameCounter = 0
private static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = ( private nonisolated(unsafe) static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (
nil, nil nil, nil
) )
private static var _metrics = PupilDetectorMetrics() private nonisolated(unsafe) static var _metrics = PupilDetectorMetrics()
static let calibration = PupilCalibration() nonisolated(unsafe) static let calibration = PupilCalibration()
// MARK: - Convenience Properties // MARK: - Convenience Properties
private static var debugImageCounter: Int { private nonisolated static var debugImageCounter: Int {
get { _debugImageCounter } get { _debugImageCounter }
set { _debugImageCounter = newValue } set { _debugImageCounter = newValue }
} }
private static var frameCounter: Int { private nonisolated static var frameCounter: Int {
get { _frameCounter } get { _frameCounter }
set { _frameCounter = newValue } set { _frameCounter = newValue }
} }
private static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) { private nonisolated static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) {
get { _lastPupilPositions } get { _lastPupilPositions }
set { _lastPupilPositions = newValue } set { _lastPupilPositions = newValue }
} }
private static var metrics: PupilDetectorMetrics { private nonisolated static var metrics: PupilDetectorMetrics {
get { _metrics } get { _metrics }
set { _metrics = newValue } set { _metrics = newValue }
} }
// MARK: - Precomputed Tables // MARK: - Precomputed Tables
private static let spatialWeightsLUT: [[Float]] = { private nonisolated(unsafe) static let spatialWeightsLUT: [[Float]] = {
let d = 10 let d = 10
let radius = d / 2 let radius = d / 2
let sigmaSpace: Float = 15.0 let sigmaSpace: Float = 15.0
@@ -197,7 +197,7 @@ final class PupilDetector: @unchecked Sendable {
return weights return weights
}() }()
private static let colorWeightsLUT: [Float] = { private nonisolated(unsafe) static let colorWeightsLUT: [Float] = {
let sigmaColor: Float = 15.0 let sigmaColor: Float = 15.0
var lut = [Float](repeating: 0, count: 256) var lut = [Float](repeating: 0, count: 256)
for diff in 0..<256 { for diff in 0..<256 {
@@ -209,12 +209,12 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Reusable Buffers // MARK: - Reusable Buffers
private static var grayscaleBuffer: UnsafeMutablePointer<UInt8>? private nonisolated(unsafe) static var grayscaleBuffer: UnsafeMutablePointer<UInt8>?
private static var grayscaleBufferSize = 0 private nonisolated(unsafe) static var grayscaleBufferSize = 0
private static var eyeBuffer: UnsafeMutablePointer<UInt8>? private nonisolated(unsafe) static var eyeBuffer: UnsafeMutablePointer<UInt8>?
private static var eyeBufferSize = 0 private nonisolated(unsafe) static var eyeBufferSize = 0
private static var tempBuffer: UnsafeMutablePointer<UInt8>? private nonisolated(unsafe) static var tempBuffer: UnsafeMutablePointer<UInt8>?
private static var tempBufferSize = 0 private nonisolated(unsafe) static var tempBufferSize = 0
// MARK: - Public API // MARK: - Public API
@@ -369,7 +369,9 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Buffer Management // MARK: - Buffer Management
private static func ensureBufferCapacity(frameSize: Int, eyeSize: Int) { // MARK: - Buffer Management
private nonisolated static func ensureBufferCapacity(frameSize: Int, eyeSize: Int) {
if grayscaleBufferSize < frameSize { if grayscaleBufferSize < frameSize {
grayscaleBuffer?.deallocate() grayscaleBuffer?.deallocate()
grayscaleBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: frameSize) grayscaleBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: frameSize)
@@ -388,7 +390,7 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Optimized Grayscale Conversion (vImage) // MARK: - Optimized Grayscale Conversion (vImage)
private static func extractGrayscaleDataOptimized( private nonisolated static func extractGrayscaleDataOptimized(
from pixelBuffer: CVPixelBuffer, from pixelBuffer: CVPixelBuffer,
to output: UnsafeMutablePointer<UInt8>, to output: UnsafeMutablePointer<UInt8>,
width: Int, width: Int,
@@ -476,7 +478,7 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Optimized Eye Isolation // MARK: - Optimized Eye Isolation
private static func isolateEyeWithMaskOptimized( private nonisolated static func isolateEyeWithMaskOptimized(
frameData: UnsafePointer<UInt8>, frameData: UnsafePointer<UInt8>,
frameWidth: Int, frameWidth: Int,
frameHeight: Int, frameHeight: Int,
@@ -526,7 +528,7 @@ final class PupilDetector: @unchecked Sendable {
} }
@inline(__always) @inline(__always)
private static func pointInPolygonFast( private nonisolated static func pointInPolygonFast(
px: Float, py: Float, edges: [(x1: Float, y1: Float, x2: Float, y2: Float)] px: Float, py: Float, edges: [(x1: Float, y1: Float, x2: Float, y2: Float)]
) -> Bool { ) -> Bool {
var inside = false var inside = false
@@ -542,7 +544,7 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Optimized Image Processing // MARK: - Optimized Image Processing
static func imageProcessingOptimized( nonisolated static func imageProcessingOptimized(
input: UnsafePointer<UInt8>, input: UnsafePointer<UInt8>,
output: UnsafeMutablePointer<UInt8>, output: UnsafeMutablePointer<UInt8>,
width: Int, width: Int,
@@ -569,7 +571,7 @@ final class PupilDetector: @unchecked Sendable {
} }
} }
private static func gaussianBlurOptimized( private nonisolated static func gaussianBlurOptimized(
input: UnsafePointer<UInt8>, input: UnsafePointer<UInt8>,
output: UnsafeMutablePointer<UInt8>, output: UnsafeMutablePointer<UInt8>,
width: Int, width: Int,
@@ -607,7 +609,7 @@ final class PupilDetector: @unchecked Sendable {
) )
} }
private static func erodeOptimized( private nonisolated static func erodeOptimized(
input: UnsafePointer<UInt8>, input: UnsafePointer<UInt8>,
output: UnsafeMutablePointer<UInt8>, output: UnsafeMutablePointer<UInt8>,
width: Int, width: Int,
@@ -668,7 +670,7 @@ final class PupilDetector: @unchecked Sendable {
/// Optimized centroid-of-dark-pixels approach - much faster than union-find /// Optimized centroid-of-dark-pixels approach - much faster than union-find
/// Returns the centroid of the largest dark region /// Returns the centroid of the largest dark region
private static func findPupilFromContoursOptimized( private nonisolated static func findPupilFromContoursOptimized(
data: UnsafePointer<UInt8>, data: UnsafePointer<UInt8>,
width: Int, width: Int,
height: Int height: Int
@@ -722,7 +724,7 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Helper Methods // MARK: - Helper Methods
private static func landmarksToPixelCoordinates( private nonisolated static func landmarksToPixelCoordinates(
landmarks: VNFaceLandmarkRegion2D, landmarks: VNFaceLandmarkRegion2D,
faceBoundingBox: CGRect, faceBoundingBox: CGRect,
imageSize: CGSize imageSize: CGSize
@@ -736,7 +738,7 @@ final class PupilDetector: @unchecked Sendable {
} }
} }
private static func createEyeRegion(from points: [CGPoint], imageSize: CGSize) -> EyeRegion? { private nonisolated static func createEyeRegion(from points: [CGPoint], imageSize: CGSize) -> EyeRegion? {
guard !points.isEmpty else { return nil } guard !points.isEmpty else { return nil }
let margin: CGFloat = 5 let margin: CGFloat = 5
@@ -777,7 +779,7 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Debug Helpers // MARK: - Debug Helpers
private static func saveDebugImage( private nonisolated static func saveDebugImage(
data: UnsafePointer<UInt8>, width: Int, height: Int, name: String data: UnsafePointer<UInt8>, width: Int, height: Int, name: String
) { ) {
guard let cgImage = createCGImage(from: data, width: width, height: height) else { return } guard let cgImage = createCGImage(from: data, width: width, height: height) else { return }
@@ -793,7 +795,7 @@ final class PupilDetector: @unchecked Sendable {
print("💾 Saved debug image: \(url.path)") print("💾 Saved debug image: \(url.path)")
} }
private static func createCGImage(from data: UnsafePointer<UInt8>, width: Int, height: Int) private nonisolated static func createCGImage(from data: UnsafePointer<UInt8>, width: Int, height: Int)
-> CGImage? -> CGImage?
{ {
let mutableData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height) let mutableData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height)
@@ -817,7 +819,7 @@ final class PupilDetector: @unchecked Sendable {
} }
/// Clean up allocated buffers (call on app termination if needed) /// Clean up allocated buffers (call on app termination if needed)
static func cleanup() { nonisolated static func cleanup() {
grayscaleBuffer?.deallocate() grayscaleBuffer?.deallocate()
grayscaleBuffer = nil grayscaleBuffer = nil
grayscaleBufferSize = 0 grayscaleBufferSize = 0

View File

@@ -111,7 +111,12 @@ final class ServiceContainer {
} }
/// Creates a new container configured for testing with default mock settings /// Creates a new container configured for testing with default mock settings
static func forTesting(settings: AppSettings = .defaults) -> ServiceContainer { static func forTesting() -> ServiceContainer {
forTesting(settings: AppSettings())
}
/// Creates a new container configured for testing with custom settings
static func forTesting(settings: AppSettings) -> ServiceContainer {
let mockSettings = MockSettingsManager(settings: settings) let mockSettings = MockSettingsManager(settings: settings)
return ServiceContainer(settingsManager: mockSettings) return ServiceContainer(settingsManager: mockSettings)
} }
@@ -138,7 +143,11 @@ final class MockSettingsManager: SettingsProviding {
.posture: \.postureTimer, .posture: \.postureTimer,
] ]
init(settings: AppSettings = .defaults) { convenience init() {
self.init(settings: AppSettings())
}
init(settings: AppSettings) {
self.settings = settings self.settings = settings
self._settingsSubject = CurrentValueSubject(settings) self._settingsSubject = CurrentValueSubject(settings)
} }

View File

@@ -32,10 +32,21 @@ class TimerEngine: ObservableObject {
// Logging manager // Logging manager
private let logger = LoggingManager.shared.timerLogger private let logger = LoggingManager.shared.timerLogger
convenience init(
settingsManager: any SettingsProviding,
enforceModeService: EnforceModeService? = nil
) {
self.init(
settingsManager: settingsManager,
enforceModeService: enforceModeService,
timeProvider: SystemTimeProvider()
)
}
init( init(
settingsManager: any SettingsProviding, settingsManager: any SettingsProviding,
enforceModeService: EnforceModeService? = nil, enforceModeService: EnforceModeService?,
timeProvider: TimeProviding = SystemTimeProvider() timeProvider: TimeProviding
) { ) {
self.settingsProvider = settingsManager self.settingsProvider = settingsManager
self.enforceModeService = enforceModeService ?? EnforceModeService.shared self.enforceModeService = enforceModeService ?? EnforceModeService.shared

View File

@@ -75,8 +75,9 @@ class UsageTrackingService: ObservableObject {
private func startTracking() { private func startTracking() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
Task { @MainActor in Task { @MainActor in
self?.tick() self.tick()
} }
} }
} }

View File

@@ -86,8 +86,10 @@ final class OnboardingWindowPresenter {
object: window, object: window,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
Task { @MainActor in
self?.windowController = nil self?.windowController = nil
self?.removeCloseObserver() self?.removeCloseObserver()
}
NotificationCenter.default.post(name: Notification.Name("OnboardingWindowDidClose"), object: nil) NotificationCenter.default.post(name: Notification.Name("OnboardingWindowDidClose"), object: nil)
} }
} }

View File

@@ -304,7 +304,7 @@ struct UserTimerEditSheet: View {
} }
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.onChange(of: type) { newType in .onChange(of: type) { _, newType in
if newType == .subtle { if newType == .subtle {
timeOnScreen = 3 timeOnScreen = 3
} else if timeOnScreen == 3 { } else if timeOnScreen == 3 {

View File

@@ -42,6 +42,7 @@ final class ServiceContainerTests: XCTestCase {
let mockSettings = EnhancedMockSettingsManager(settings: .shortIntervals) let mockSettings = EnhancedMockSettingsManager(settings: .shortIntervals)
let customEngine = TimerEngine( let customEngine = TimerEngine(
settingsManager: mockSettings, settingsManager: mockSettings,
enforceModeService: nil,
timeProvider: MockTimeProvider() timeProvider: MockTimeProvider()
) )

View File

@@ -41,6 +41,7 @@ final class TimerEngineTests: XCTestCase {
let timeProvider = MockTimeProvider() let timeProvider = MockTimeProvider()
let engine = TimerEngine( let engine = TimerEngine(
settingsManager: testEnv.settingsManager, settingsManager: testEnv.settingsManager,
enforceModeService: nil,
timeProvider: timeProvider timeProvider: timeProvider
) )

View File

@@ -29,6 +29,7 @@ final class TimerEngineTestabilityTests: XCTestCase {
let timeProvider = MockTimeProvider() let timeProvider = MockTimeProvider()
let timerEngine = TimerEngine( let timerEngine = TimerEngine(
settingsManager: testEnv.settingsManager, settingsManager: testEnv.settingsManager,
enforceModeService: nil,
timeProvider: timeProvider timeProvider: timeProvider
) )
@@ -59,6 +60,7 @@ final class TimerEngineTestabilityTests: XCTestCase {
let timeProvider = MockTimeProvider(startTime: Date()) let timeProvider = MockTimeProvider(startTime: Date())
let timerEngine = TimerEngine( let timerEngine = TimerEngine(
settingsManager: testEnv.settingsManager, settingsManager: testEnv.settingsManager,
enforceModeService: nil,
timeProvider: timeProvider timeProvider: timeProvider
) )