fix: preview fixed

This commit is contained in:
Michael Freno
2026-01-14 01:01:03 -05:00
parent 780da2951b
commit 23000589cf
4 changed files with 116 additions and 54 deletions

View File

@@ -27,6 +27,7 @@ class EnforceModeService: ObservableObject {
self.settingsManager = SettingsManager.shared
self.eyeTrackingService = EyeTrackingService.shared
setupObservers()
initializeEnforceModeState()
}
private func setupObservers() {
@@ -37,6 +38,20 @@ class EnforceModeService: ObservableObject {
.store(in: &cancellables)
}
private func initializeEnforceModeState() {
let cameraService = CameraAccessService.shared
let settingsEnabled = settingsManager.settings.enforcementMode
// If settings say it's enabled AND camera is authorized, mark as enabled
if settingsEnabled && cameraService.isCameraAuthorized {
isEnforceModeEnabled = true
print("✓ Enforce mode initialized as enabled (camera authorized)")
} else {
isEnforceModeEnabled = false
print("🔒 Enforce mode initialized as disabled")
}
}
func enableEnforceMode() async {
print("🔒 enableEnforceMode called")
guard !isEnforceModeEnabled else {

View File

@@ -21,11 +21,23 @@ class EyeTrackingService: NSObject, ObservableObject {
private var captureSession: AVCaptureSession?
private var videoOutput: AVCaptureVideoDataOutput?
private let videoDataOutputQueue = DispatchQueue(label: "com.gaze.videoDataOutput", qos: .userInitiated)
private var _previewLayer: AVCaptureVideoPreviewLayer?
var previewLayer: AVCaptureVideoPreviewLayer? {
guard let session = captureSession else { return nil }
guard let session = captureSession else {
_previewLayer = nil
return nil
}
// Reuse existing layer if session hasn't changed
if let existing = _previewLayer, existing.session === session {
return existing
}
// Create new layer only when session changes
let layer = AVCaptureVideoPreviewLayer(session: session)
layer.videoGravity = .resizeAspectFill
_previewLayer = layer
return layer
}
@@ -66,6 +78,7 @@ class EyeTrackingService: NSObject, ObservableObject {
captureSession?.stopRunning()
captureSession = nil
videoOutput = nil
_previewLayer = nil
isEyeTrackingActive = false
isEyesClosed = false
userLookingAtScreen = true

View File

@@ -12,20 +12,36 @@ struct CameraPreviewView: NSViewRepresentable {
let previewLayer: AVCaptureVideoPreviewLayer
let borderColor: NSColor
func makeNSView(context: Context) -> NSView {
func makeNSView(context: Context) -> PreviewContainerView {
let view = PreviewContainerView()
view.wantsLayer = true
previewLayer.frame = view.bounds
view.layer?.addSublayer(previewLayer)
// Add the preview layer once
if view.layer?.sublayers?.first as? AVCaptureVideoPreviewLayer !== previewLayer {
view.layer?.sublayers?.forEach { $0.removeFromSuperlayer() }
previewLayer.frame = view.bounds
view.layer?.addSublayer(previewLayer)
}
updateBorder(view: view, color: borderColor)
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
previewLayer.frame = nsView.bounds
func updateNSView(_ nsView: PreviewContainerView, context: Context) {
// Only update border color and frame, don't recreate layer
let currentLayer = nsView.layer?.sublayers?.first as? AVCaptureVideoPreviewLayer
if currentLayer !== previewLayer {
// Layer changed, need to replace
nsView.layer?.sublayers?.forEach { $0.removeFromSuperlayer() }
previewLayer.frame = nsView.bounds
nsView.layer?.addSublayer(previewLayer)
} else {
// Same layer, just update frame
previewLayer.frame = nsView.bounds
}
updateBorder(view: nsView, color: borderColor)
}

View File

@@ -5,6 +5,7 @@
// Created by Mike Freno on 1/13/26.
//
import AVFoundation
import SwiftUI
struct EnforceModeSetupView: View {
@@ -12,10 +13,11 @@ struct EnforceModeSetupView: View {
@ObservedObject var cameraService = CameraAccessService.shared
@ObservedObject var eyeTrackingService = EyeTrackingService.shared
@ObservedObject var enforceModeService = EnforceModeService.shared
@State private var isProcessingToggle = false
@State private var isTestModeActive = false
@State private var cachedPreviewLayer: AVCaptureVideoPreviewLayer?
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 16) {
@@ -27,15 +29,15 @@ struct EnforceModeSetupView: View {
}
.padding(.top, 20)
.padding(.bottom, 30)
Spacer()
VStack(spacing: 30) {
Text("Use your camera to ensure you take breaks")
.font(.title3)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
VStack(spacing: 20) {
HStack {
VStack(alignment: .leading, spacing: 4) {
@@ -49,8 +51,8 @@ struct EnforceModeSetupView: View {
Toggle(
"",
isOn: Binding(
get: {
settingsManager.settings.enforcementMode
get: {
settingsManager.settings.enforcementMode
},
set: { newValue in
print("🎛️ Toggle changed to: \(newValue)")
@@ -68,13 +70,13 @@ struct EnforceModeSetupView: View {
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
cameraStatusView
if enforceModeService.isEnforceModeEnabled {
testModeButton
}
if isTestModeActive && enforceModeService.isCameraActive {
testModePreviewView
} else {
@@ -83,28 +85,32 @@ struct EnforceModeSetupView: View {
} else if enforceModeService.isEnforceModeEnabled {
cameraPendingView
}
privacyInfoView
}
}
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.background(.clear)
}
private var testModeButton: some View {
Button(action: {
Task { @MainActor in
if isTestModeActive {
enforceModeService.stopTestMode()
isTestModeActive = false
cachedPreviewLayer = nil
} else {
await enforceModeService.startTestMode()
isTestModeActive = enforceModeService.isCameraActive
if isTestModeActive {
cachedPreviewLayer = eyeTrackingService.previewLayer
}
}
}
}) {
@@ -120,53 +126,64 @@ struct EnforceModeSetupView: View {
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
private var testModePreviewView: some View {
VStack(spacing: 16) {
if let previewLayer = eyeTrackingService.previewLayer {
let lookingAway = !eyeTrackingService.userLookingAtScreen
let borderColor: NSColor = lookingAway ? .systemGreen : .systemRed
CameraPreviewView(previewLayer: previewLayer, borderColor: borderColor)
let lookingAway = !eyeTrackingService.userLookingAtScreen
let borderColor: NSColor = lookingAway ? .systemGreen : .systemRed
// Cache the preview layer to avoid recreating it
let previewLayer = eyeTrackingService.previewLayer ?? cachedPreviewLayer
if let layer = previewLayer {
CameraPreviewView(previewLayer: layer, borderColor: borderColor)
.frame(height: 300)
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
.onAppear {
if cachedPreviewLayer == nil {
cachedPreviewLayer = eyeTrackingService.previewLayer
}
}
VStack(alignment: .leading, spacing: 12) {
Text("Live Tracking Status")
.font(.headline)
HStack(spacing: 20) {
statusIndicator(
title: "Face Detected",
isActive: eyeTrackingService.faceDetected,
icon: "person.fill"
)
statusIndicator(
title: "Looking Away",
isActive: !eyeTrackingService.userLookingAtScreen,
icon: "arrow.turn.up.right"
)
}
Text(lookingAway ? "✓ Break compliance detected" : "⚠️ Please look away from screen")
.font(.caption)
.foregroundColor(lookingAway ? .green : .orange)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 4)
Text(
lookingAway
? "✓ Break compliance detected" : "⚠️ Please look away from screen"
)
.font(.caption)
.foregroundColor(lookingAway ? .green : .orange)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 4)
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
}
}
}
private var cameraStatusView: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Camera Access")
.font(.headline)
if cameraService.isCameraAuthorized {
Label("Authorized", systemImage: "checkmark.circle.fill")
.font(.caption)
@@ -181,9 +198,9 @@ struct EnforceModeSetupView: View {
.foregroundColor(.secondary)
}
}
Spacer()
if !cameraService.isCameraAuthorized {
Button("Request Access") {
print("📷 Request Access button clicked")
@@ -202,19 +219,19 @@ struct EnforceModeSetupView: View {
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
}
private var eyeTrackingStatusView: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Eye Tracking Status")
.font(.headline)
HStack(spacing: 20) {
statusIndicator(
title: "Face Detected",
isActive: eyeTrackingService.faceDetected,
icon: "person.fill"
)
statusIndicator(
title: "Looking Away",
isActive: !eyeTrackingService.userLookingAtScreen,
@@ -225,13 +242,13 @@ struct EnforceModeSetupView: View {
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
}
private var cameraPendingView: some View {
HStack {
Image(systemName: "timer")
.font(.title2)
.foregroundColor(.orange)
VStack(alignment: .leading, spacing: 4) {
Text("Camera Ready")
.font(.headline)
@@ -239,19 +256,19 @@ struct EnforceModeSetupView: View {
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
}
private func statusIndicator(title: String, isActive: Bool, icon: String) -> some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(isActive ? .green : .secondary)
Text(title)
.font(.caption)
.foregroundColor(.secondary)
@@ -259,7 +276,7 @@ struct EnforceModeSetupView: View {
}
.frame(maxWidth: .infinity)
}
private var privacyInfoView: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
@@ -269,7 +286,7 @@ struct EnforceModeSetupView: View {
Text("Privacy Information")
.font(.headline)
}
VStack(alignment: .leading, spacing: 8) {
privacyBullet("All processing happens on-device")
privacyBullet("No images are stored or transmitted")
@@ -281,9 +298,10 @@ struct EnforceModeSetupView: View {
.foregroundColor(.secondary)
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular.tint(.blue.opacity(0.1)), in: .rect(cornerRadius: 12))
.glassEffectIfAvailable(
GlassStyle.regular.tint(.blue.opacity(0.1)), in: .rect(cornerRadius: 12))
}
private func privacyBullet(_ text: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "checkmark")
@@ -292,19 +310,19 @@ struct EnforceModeSetupView: View {
Text(text)
}
}
private func handleEnforceModeToggle(enabled: Bool) {
print("🎛️ handleEnforceModeToggle called with enabled: \(enabled)")
isProcessingToggle = true
Task { @MainActor in
defer { isProcessingToggle = false }
if enabled {
print("🎛️ Enabling enforce mode...")
await enforceModeService.enableEnforceMode()
print("🎛️ Enforce mode enabled: \(enforceModeService.isEnforceModeEnabled)")
if !enforceModeService.isEnforceModeEnabled {
print("⚠️ Failed to activate, reverting toggle")
settingsManager.settings.enforcementMode = false