fix: preview fixed
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user