feat: started on camera access for enforcement mode
This commit is contained in:
@@ -36,12 +36,12 @@ struct AppSettings: Codable, Equatable, Hashable {
|
||||
var lookAwayCountdownSeconds: Int
|
||||
var blinkTimer: TimerConfiguration
|
||||
var postureTimer: TimerConfiguration
|
||||
var enforcementMode: Bool = false
|
||||
|
||||
var userTimers: [UserTimer]
|
||||
|
||||
var subtleReminderSize: ReminderSize
|
||||
|
||||
// App state and behavior
|
||||
var hasCompletedOnboarding: Bool
|
||||
var launchAtLogin: Bool
|
||||
var playSounds: Bool
|
||||
|
||||
82
Gaze/Services/CameraAccessService.swift
Normal file
82
Gaze/Services/CameraAccessService.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// CameraAccessService.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Mike Freno on 1/13/26.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class CameraAccessService: ObservableObject {
|
||||
static let shared = CameraAccessService()
|
||||
|
||||
@Published var isCameraAuthorized = false
|
||||
@Published var cameraError: Error?
|
||||
|
||||
private init() {
|
||||
checkCameraAuthorizationStatus()
|
||||
}
|
||||
|
||||
func requestCameraAccess() async throws {
|
||||
guard #available(macOS 12.0, *) else {
|
||||
throw CameraAccessError.unsupportedOS
|
||||
}
|
||||
|
||||
if isCameraAuthorized {
|
||||
return
|
||||
}
|
||||
|
||||
let status = await AVCaptureDevice.requestAccess(for: .video)
|
||||
if !status {
|
||||
throw CameraAccessError.accessDenied
|
||||
}
|
||||
|
||||
checkCameraAuthorizationStatus()
|
||||
}
|
||||
|
||||
func checkCameraAuthorizationStatus() {
|
||||
guard #available(macOS 12.0, *) else {
|
||||
isCameraAuthorized = false
|
||||
return
|
||||
}
|
||||
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
|
||||
switch status {
|
||||
case .authorized:
|
||||
isCameraAuthorized = true
|
||||
cameraError = nil
|
||||
case .notDetermined:
|
||||
isCameraAuthorized = false
|
||||
cameraError = nil
|
||||
case .denied, .restricted:
|
||||
isCameraAuthorized = false
|
||||
cameraError = CameraAccessError.accessDenied
|
||||
default:
|
||||
isCameraAuthorized = false
|
||||
cameraError = CameraAccessError.unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Handling
|
||||
|
||||
enum CameraAccessError: Error, LocalizedError {
|
||||
case accessDenied
|
||||
case unsupportedOS
|
||||
case unknown
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .accessDenied:
|
||||
return
|
||||
"Camera access was denied. Please enable camera permissions in System Preferences."
|
||||
case .unsupportedOS:
|
||||
return "This feature requires macOS 12 or later."
|
||||
case .unknown:
|
||||
return "An unknown error occurred with camera access."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,7 @@ import Foundation
|
||||
@MainActor
|
||||
class SettingsManager: ObservableObject {
|
||||
static let shared = SettingsManager()
|
||||
|
||||
@Published var settings: AppSettings
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let settingsKey = "gazeAppSettings"
|
||||
private var saveCancellable: AnyCancellable?
|
||||
@@ -48,7 +46,7 @@ class SettingsManager: ObservableObject {
|
||||
guard let data = UserDefaults.standard.data(forKey: "gazeAppSettings") else {
|
||||
return .defaults
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let settings = try JSONDecoder().decode(AppSettings.self, from: data)
|
||||
return settings
|
||||
|
||||
@@ -228,7 +228,7 @@ struct GeneralSetupView: View {
|
||||
try LaunchAtLoginManager.disable()
|
||||
}
|
||||
} catch {
|
||||
// Failed to set launch at login - handled silently in production
|
||||
//TODO: see what can be done here
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import SwiftUI
|
||||
struct LookAwaySetupView: View {
|
||||
@ObservedObject var settingsManager: SettingsManager
|
||||
@State private var previewWindowController: NSWindowController?
|
||||
@ObservedObject var cameraAccess = CameraAccessService.shared
|
||||
@State private var failedCameraAccess = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -30,34 +32,15 @@ struct LookAwaySetupView: View {
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// Vertically centered content
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 30) {
|
||||
// InfoBox with link functionality
|
||||
HStack(spacing: 12) {
|
||||
Button(action: {
|
||||
if let url = URL(
|
||||
string:
|
||||
"https://journals.co.za/doi/abs/10.4102/aveh.v79i1.554#:~:text=the 20/20/20 rule induces significant changes in dry eye symptoms and tear film and some limited changes for ocular surface integrity."
|
||||
) {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.open(url)
|
||||
#elseif os(macOS)
|
||||
NSWorkspace.shared.open(url)
|
||||
#endif
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.white)
|
||||
}.buttonStyle(.plain)
|
||||
Text("Suggested: 20-20-20 rule")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
|
||||
InfoBox(
|
||||
text: "Suggested: 20-20-20 rule",
|
||||
url:
|
||||
"https://journals.co.za/doi/abs/10.4102/aveh.v79i1.554#:~:text=the 20/20/20 rule induces significant changes in dry eye symptoms and tear film and some limited changes for ocular surface integrity."
|
||||
)
|
||||
|
||||
SliderSection(
|
||||
intervalSettings: Binding(
|
||||
@@ -68,7 +51,8 @@ struct LookAwaySetupView: View {
|
||||
)
|
||||
},
|
||||
set: { newValue in
|
||||
settingsManager.settings.lookAwayTimer.intervalSeconds = (newValue.val ?? 20) * 60
|
||||
settingsManager.settings.lookAwayTimer.intervalSeconds =
|
||||
(newValue.val ?? 20) * 60
|
||||
}
|
||||
),
|
||||
countdownSettings: Binding(
|
||||
@@ -89,8 +73,34 @@ struct LookAwaySetupView: View {
|
||||
type: "Look away",
|
||||
previewFunc: showPreviewWindow
|
||||
)
|
||||
}
|
||||
Toggle(
|
||||
"Enable enforcement mode",
|
||||
isOn: Binding(
|
||||
get: { settingsManager.settings.enforcementMode },
|
||||
set: { settingsManager.settings.enforcementMode = $0 }
|
||||
)
|
||||
)
|
||||
.onChange(
|
||||
of: settingsManager.settings.enforcementMode,
|
||||
) { newMode in
|
||||
if newMode && !cameraAccess.isCameraAuthorized {
|
||||
Task {
|
||||
do {
|
||||
try await cameraAccess.requestCameraAccess()
|
||||
} catch {
|
||||
failedCameraAccess = true
|
||||
settingsManager.settings.enforcementMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
#if failedCameraAccess
|
||||
Text(
|
||||
"Camera access denied. Please enable camera access in System Settings if you want to use enforcement mode."
|
||||
)
|
||||
#endif
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
Reference in New Issue
Block a user