feat: continued

This commit is contained in:
Michael Freno
2026-01-14 17:06:12 -05:00
parent 64f41f8bef
commit d96807ce79
5 changed files with 174 additions and 15 deletions

View File

@@ -22,6 +22,9 @@ class EnforceModeService: ObservableObject {
private var timerEngine: TimerEngine?
private var cancellables = Set<AnyCancellable>()
private var faceDetectionTimer: Timer?
private var lastFaceDetectionTime: Date = Date.distantPast
private let faceDetectionTimeout: TimeInterval = 5.0 // 5 seconds to consider person lost
private init() {
self.settingsManager = SettingsManager.shared
@@ -36,6 +39,13 @@ class EnforceModeService: ObservableObject {
self?.handleGazeChange(lookingAtScreen: lookingAtScreen)
}
.store(in: &cancellables)
// Observe face detection changes to track person presence
eyeTrackingService.$faceDetected
.sink { [weak self] faceDetected in
self?.handleFaceDetectionChange(faceDetected: faceDetected)
}
.store(in: &cancellables)
}
private func initializeEnforceModeState() {
@@ -126,6 +136,10 @@ class EnforceModeService: ObservableObject {
eyeTrackingService.stopEyeTracking()
isCameraActive = false
userCompliedWithBreak = false
// Invalidate the face detection timer when stopping camera
faceDetectionTimer?.invalidate()
faceDetectionTimer = nil
}
func checkUserCompliance() {
@@ -144,8 +158,42 @@ class EnforceModeService: ObservableObject {
checkUserCompliance()
}
private func handleFaceDetectionChange(faceDetected: Bool) {
// Update the last face detection time
if faceDetected {
lastFaceDetectionTime = Date()
}
// If we are in enforce mode and camera is active, start checking for person presence
if isEnforceModeEnabled && isCameraActive {
// Cancel any existing timer and restart it
faceDetectionTimer?.invalidate()
// Create a new timer to check for extended periods without face detection
faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
guard let self = self else { return }
// Dispatch to main actor to safely access main actor-isolated properties and methods
Task { @MainActor in
let timeSinceLastDetection = Date().timeIntervalSince(self.lastFaceDetectionTime)
// If person has not been detected for too long, temporarily disable enforce mode
if timeSinceLastDetection > self.faceDetectionTimeout {
print("⏰ Person not detected for \(self.faceDetectionTimeout)s. Temporarily disabling enforce mode.")
self.isEnforceModeEnabled = false
self.stopCamera()
}
}
}
}
}
func handleReminderDismissed() {
stopCamera()
// Stop camera when reminder is dismissed, but also check if we should disable enforce mode entirely
// This helps in case a user closes settings window while a reminder is active
if isCameraActive {
stopCamera()
}
}
func startTestMode() async {

View File

@@ -114,31 +114,53 @@ class EyeTrackingService: NSObject, ObservableObject {
}
private func processFaceObservations(_ observations: [VNFaceObservation]?) {
print("🔍 Processing face observations...")
guard let observations = observations, !observations.isEmpty else {
print("❌ No faces detected")
faceDetected = false
userLookingAtScreen = false
return
}
faceDetected = true
let face = observations.first!
guard let face = observations.first,
let landmarks = face.landmarks else {
print("✅ Face detected. Bounding box: \(face.boundingBox)")
guard let landmarks = face.landmarks else {
print("❌ No face landmarks detected")
return
}
// Log eye landmarks
if let leftEye = landmarks.leftEye,
let rightEye = landmarks.rightEye {
print("👁️ Left eye landmarks: \(leftEye.pointCount) points")
print("👁️ Right eye landmarks: \(rightEye.pointCount) points")
let leftEyeHeight = calculateEyeHeight(leftEye)
let rightEyeHeight = calculateEyeHeight(rightEye)
print("👁️ Left eye height: \(leftEyeHeight)")
print("👁️ Right eye height: \(rightEyeHeight)")
let eyesClosed = detectEyesClosed(leftEye: leftEye, rightEye: rightEye)
self.isEyesClosed = eyesClosed
print("👁️ Eyes closed: \(eyesClosed)")
}
// Log gaze detection
let lookingAway = detectLookingAway(face: face, landmarks: landmarks)
userLookingAtScreen = !lookingAway
print("📊 Gaze angle - Yaw: \(face.yaw?.doubleValue ?? 0.0), Roll: \(face.roll?.doubleValue ?? 0.0)")
print("🎯 Looking away: \(lookingAway)")
print("👀 User looking at screen: \(userLookingAtScreen)")
}
private func detectEyesClosed(leftEye: VNFaceLandmarkRegion2D, rightEye: VNFaceLandmarkRegion2D) -> Bool {
guard leftEye.pointCount >= 2, rightEye.pointCount >= 2 else {
print("⚠️ Eye landmarks insufficient for eye closure detection")
return false
}
@@ -147,18 +169,26 @@ class EyeTrackingService: NSObject, ObservableObject {
let closedThreshold: CGFloat = 0.02
return leftEyeHeight < closedThreshold && rightEyeHeight < closedThreshold
let isClosed = leftEyeHeight < closedThreshold && rightEyeHeight < closedThreshold
print("👁️ Eye closure detection - Left: \(leftEyeHeight) < \(closedThreshold) = \(leftEyeHeight < closedThreshold), Right: \(rightEyeHeight) < \(closedThreshold) = \(rightEyeHeight < closedThreshold)")
return isClosed
}
private func calculateEyeHeight(_ eye: VNFaceLandmarkRegion2D) -> CGFloat {
let points = eye.normalizedPoints
print("📏 Eye points count: \(points.count)")
guard points.count >= 2 else { return 0 }
let yValues = points.map { $0.y }
let maxY = yValues.max() ?? 0
let minY = yValues.min() ?? 0
return abs(maxY - minY)
let height = abs(maxY - minY)
print("📏 Eye height calculation: max(\(maxY)) - min(\(minY)) = \(height)")
return height
}
private func detectLookingAway(face: VNFaceObservation, landmarks: VNFaceLandmarks2D) -> Bool {
@@ -170,6 +200,10 @@ class EyeTrackingService: NSObject, ObservableObject {
let isLookingAway = abs(yaw) > yawThreshold || abs(roll) > rollThreshold
print("📊 Gaze detection - Yaw: \(yaw), Roll: \(roll)")
print("📉 Thresholds - Yaw: \(yawThreshold), Roll: \(rollThreshold)")
print("🎯 Looking away result: \(isLookingAway)")
return isLookingAway
}
}

View File

@@ -84,7 +84,7 @@ struct SettingsWindowView: View {
}
.tabViewStyle(.sidebarAdaptable)
} else {
// Fallback for macOS 14 and earlier
// Fallback for macOS 14 and earlier - use a consistent sidebar approach without collapse button
NavigationSplitView {
List(SettingsSection.allCases, selection: $selectedSection) { section in
NavigationLink(value: section) {
@@ -96,6 +96,8 @@ struct SettingsWindowView: View {
} detail: {
detailView(for: selectedSection)
}
// Disable the ability to collapse the sidebar by explicitly setting a fixed width
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 300)
}
Divider()

View File

@@ -17,6 +17,8 @@ struct EnforceModeSetupView: View {
@State private var isProcessingToggle = false
@State private var isTestModeActive = false
@State private var cachedPreviewLayer: AVCaptureVideoPreviewLayer?
@State private var showDebugView = false
@State private var isViewActive = false
var body: some View {
VStack(spacing: 0) {
@@ -71,6 +73,18 @@ struct EnforceModeSetupView: View {
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
#if DEBUG
HStack {
Button("Debug Info") {
showDebugView.toggle()
}
.buttonStyle(.bordered)
Spacer()
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
#endif
cameraStatusView
if enforceModeService.isEnforceModeEnabled {
@@ -82,6 +96,11 @@ struct EnforceModeSetupView: View {
} else {
if enforceModeService.isCameraActive && !isTestModeActive {
eyeTrackingStatusView
#if DEBUG
if showDebugView {
debugEyeTrackingView
}
#endif
} else if enforceModeService.isEnforceModeEnabled {
cameraPendingView
}
@@ -96,6 +115,17 @@ struct EnforceModeSetupView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.background(.clear)
.onAppear {
isViewActive = true
}
.onDisappear {
isViewActive = false
// If the view disappeared and camera is still active, stop it
if enforceModeService.isCameraActive {
print("👁️ EnforceModeSetupView disappeared, stopping camera preview")
enforceModeService.stopCamera()
}
}
}
private var testModeButton: some View {
@@ -330,9 +360,45 @@ struct EnforceModeSetupView: View {
} else {
print("🎛️ Disabling enforce mode...")
enforceModeService.disableEnforceMode()
// Clean up camera when disabling enforce mode
if enforceModeService.isCameraActive {
print("👁️ Cleaning up camera on enforce mode disable")
enforceModeService.stopCamera()
}
}
}
}
private var debugEyeTrackingView: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Debug Eye Tracking Data")
.font(.headline)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 8) {
Text("Face Detected: \(eyeTrackingService.faceDetected ? "Yes" : "No")")
.font(.caption)
Text("Looking at Screen: \(eyeTrackingService.userLookingAtScreen ? "Yes" : "No")")
.font(.caption)
Text("Eyes Closed: \(eyeTrackingService.isEyesClosed ? "Yes" : "No")")
.font(.caption)
if eyeTrackingService.faceDetected {
Text("Yaw: 0.0")
.font(.caption)
Text("Roll: 0.0")
.font(.caption)
}
}
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
}
}
#Preview {

View File

@@ -52,8 +52,9 @@ struct SmartModeSetupView: View {
"",
isOn: Binding(
get: { settingsManager.settings.smartMode.autoPauseOnFullscreen },
set: {
settingsManager.settings.smartMode.autoPauseOnFullscreen = $0
set: { newValue in
print("🔧 Smart Mode - Auto-pause on fullscreen changed to: \(newValue)")
settingsManager.settings.smartMode.autoPauseOnFullscreen = newValue
}
)
)
@@ -84,7 +85,10 @@ struct SmartModeSetupView: View {
"",
isOn: Binding(
get: { settingsManager.settings.smartMode.autoPauseOnIdle },
set: { settingsManager.settings.smartMode.autoPauseOnIdle = $0 }
set: { newValue in
print("🔧 Smart Mode - Auto-pause on idle changed to: \(newValue)")
settingsManager.settings.smartMode.autoPauseOnIdle = newValue
}
)
)
.labelsHidden()
@@ -109,9 +113,10 @@ struct SmartModeSetupView: View {
Double(
settingsManager.settings.smartMode.idleThresholdMinutes)
},
set: {
set: { newValue in
print("🔧 Smart Mode - Idle threshold changed to: \(Int(newValue))")
settingsManager.settings.smartMode.idleThresholdMinutes =
Int($0)
Int(newValue)
}
),
in: 1...30,
@@ -145,7 +150,10 @@ struct SmartModeSetupView: View {
"",
isOn: Binding(
get: { settingsManager.settings.smartMode.trackUsage },
set: { settingsManager.settings.smartMode.trackUsage = $0 }
set: { newValue in
print("🔧 Smart Mode - Track usage changed to: \(newValue)")
settingsManager.settings.smartMode.trackUsage = newValue
}
)
)
.labelsHidden()
@@ -171,9 +179,10 @@ struct SmartModeSetupView: View {
settingsManager.settings.smartMode
.usageResetAfterMinutes)
},
set: {
set: { newValue in
print("🔧 Smart Mode - Usage reset after changed to: \(Int(newValue))")
settingsManager.settings.smartMode.usageResetAfterMinutes =
Int($0)
Int(newValue)
}
),
in: 15...240,