feat: started on camera access for enforcement mode
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
||||||
|
27SPARKLE00000000003 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27SPARKLE00000000002 /* Sparkle */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
||||||
|
27SPARKLE00000000003 /* Sparkle in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -131,6 +133,7 @@
|
|||||||
name = Gaze;
|
name = Gaze;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
||||||
|
27SPARKLE00000000002 /* Sparkle */,
|
||||||
);
|
);
|
||||||
productName = Gaze;
|
productName = Gaze;
|
||||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
||||||
@@ -216,6 +219,7 @@
|
|||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||||
|
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
||||||
@@ -435,7 +439,6 @@
|
|||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.4.1;
|
||||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -472,7 +475,6 @@
|
|||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.4.1;
|
||||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -615,6 +617,14 @@
|
|||||||
minimumVersion = 4.6.0;
|
minimumVersion = 4.6.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||||
|
requirement = {
|
||||||
|
kind = exactVersion;
|
||||||
|
version = 2.8.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -623,6 +633,11 @@
|
|||||||
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
productName = Lottie;
|
productName = Lottie;
|
||||||
};
|
};
|
||||||
|
27SPARKLE00000000002 /* Sparkle */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
|
productName = Sparkle;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
var lookAwayCountdownSeconds: Int
|
var lookAwayCountdownSeconds: Int
|
||||||
var blinkTimer: TimerConfiguration
|
var blinkTimer: TimerConfiguration
|
||||||
var postureTimer: TimerConfiguration
|
var postureTimer: TimerConfiguration
|
||||||
|
var enforcementMode: Bool = false
|
||||||
|
|
||||||
var userTimers: [UserTimer]
|
var userTimers: [UserTimer]
|
||||||
|
|
||||||
var subtleReminderSize: ReminderSize
|
var subtleReminderSize: ReminderSize
|
||||||
|
|
||||||
// App state and behavior
|
|
||||||
var hasCompletedOnboarding: Bool
|
var hasCompletedOnboarding: Bool
|
||||||
var launchAtLogin: Bool
|
var launchAtLogin: Bool
|
||||||
var playSounds: 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
|
@MainActor
|
||||||
class SettingsManager: ObservableObject {
|
class SettingsManager: ObservableObject {
|
||||||
static let shared = SettingsManager()
|
static let shared = SettingsManager()
|
||||||
|
|
||||||
@Published var settings: AppSettings
|
@Published var settings: AppSettings
|
||||||
|
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let settingsKey = "gazeAppSettings"
|
private let settingsKey = "gazeAppSettings"
|
||||||
private var saveCancellable: AnyCancellable?
|
private var saveCancellable: AnyCancellable?
|
||||||
@@ -48,7 +46,7 @@ class SettingsManager: ObservableObject {
|
|||||||
guard let data = UserDefaults.standard.data(forKey: "gazeAppSettings") else {
|
guard let data = UserDefaults.standard.data(forKey: "gazeAppSettings") else {
|
||||||
return .defaults
|
return .defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let settings = try JSONDecoder().decode(AppSettings.self, from: data)
|
let settings = try JSONDecoder().decode(AppSettings.self, from: data)
|
||||||
return settings
|
return settings
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ struct GeneralSetupView: View {
|
|||||||
try LaunchAtLoginManager.disable()
|
try LaunchAtLoginManager.disable()
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 {
|
struct LookAwaySetupView: View {
|
||||||
@ObservedObject var settingsManager: SettingsManager
|
@ObservedObject var settingsManager: SettingsManager
|
||||||
@State private var previewWindowController: NSWindowController?
|
@State private var previewWindowController: NSWindowController?
|
||||||
|
@ObservedObject var cameraAccess = CameraAccessService.shared
|
||||||
|
@State private var failedCameraAccess = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -30,34 +32,15 @@ struct LookAwaySetupView: View {
|
|||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
// Vertically centered content
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 30) {
|
||||||
// InfoBox with link functionality
|
|
||||||
HStack(spacing: 12) {
|
InfoBox(
|
||||||
Button(action: {
|
text: "Suggested: 20-20-20 rule",
|
||||||
if let url = 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."
|
||||||
"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))
|
|
||||||
|
|
||||||
SliderSection(
|
SliderSection(
|
||||||
intervalSettings: Binding(
|
intervalSettings: Binding(
|
||||||
@@ -68,7 +51,8 @@ struct LookAwaySetupView: View {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
settingsManager.settings.lookAwayTimer.intervalSeconds = (newValue.val ?? 20) * 60
|
settingsManager.settings.lookAwayTimer.intervalSeconds =
|
||||||
|
(newValue.val ?? 20) * 60
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
countdownSettings: Binding(
|
countdownSettings: Binding(
|
||||||
@@ -89,8 +73,34 @@ struct LookAwaySetupView: View {
|
|||||||
type: "Look away",
|
type: "Look away",
|
||||||
previewFunc: showPreviewWindow
|
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()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
BIN
meta/Screenshot 2026-01-13 at 7.07.22 PM.png
Normal file
BIN
meta/Screenshot 2026-01-13 at 7.07.22 PM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
meta/Screenshot 2026-01-13 at 7.08.34 PM.png
Normal file
BIN
meta/Screenshot 2026-01-13 at 7.08.34 PM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
BIN
meta/Screenshot 2026-01-13 at 7.08.38 PM.png
Normal file
BIN
meta/Screenshot 2026-01-13 at 7.08.38 PM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
BIN
meta/Screenshot 2026-01-13 at 7.09.09 PM.png
Normal file
BIN
meta/Screenshot 2026-01-13 at 7.09.09 PM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
Reference in New Issue
Block a user