feat: continued
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user