ui cleanup
This commit is contained in:
@@ -76,8 +76,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Smart mode setup is now handled by ServiceContainer
|
|
||||||
// Keeping this method for settings change observation
|
|
||||||
private func observeSmartModeSettings() {
|
private func observeSmartModeSettings() {
|
||||||
settingsManager.settingsPublisher
|
settingsManager.settingsPublisher
|
||||||
.map { $0.smartMode }
|
.map { $0.smartMode }
|
||||||
|
|||||||
@@ -44,6 +44,26 @@ enum AdaptiveLayout {
|
|||||||
static let heroTitleSmall: CGFloat = 24
|
static let heroTitleSmall: CGFloat = 24
|
||||||
static let cardIcon: CGFloat = 32
|
static let cardIcon: CGFloat = 32
|
||||||
static let cardIconSmall: CGFloat = 28
|
static let cardIconSmall: CGFloat = 28
|
||||||
|
|
||||||
|
/// Returns a responsive font size based on available space
|
||||||
|
static func responsiveHeroIcon(for size: CGFloat) -> CGFloat {
|
||||||
|
size < 600 ? heroIconSmall : heroIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a responsive font size based on available space
|
||||||
|
static func responsiveHeroTitle(for size: CGFloat) -> CGFloat {
|
||||||
|
size < 600 ? heroTitleSmall : heroTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a responsive font size based on available space
|
||||||
|
static func responsiveCardIcon(for size: CGFloat) -> CGFloat {
|
||||||
|
size < 600 ? cardIconSmall : cardIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a responsive spacing value based on available space
|
||||||
|
static func responsiveSpacing(for size: CGFloat) -> CGFloat {
|
||||||
|
size < 600 ? AdaptiveLayout.Spacing.compact : AdaptiveLayout.Spacing.standard
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spacing values
|
/// Spacing values
|
||||||
@@ -63,6 +83,26 @@ enum AdaptiveLayout {
|
|||||||
static let backOffset: CGFloat = 24
|
static let backOffset: CGFloat = 24
|
||||||
static let backScale: CGFloat = 0.92
|
static let backScale: CGFloat = 0.92
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a width that scales based on available screen size
|
||||||
|
static func responsiveWidth(
|
||||||
|
baseWidth: CGFloat,
|
||||||
|
scaleFactor: CGFloat = 1.0,
|
||||||
|
minScale: CGFloat = 0.6
|
||||||
|
) -> CGFloat {
|
||||||
|
let scaleFactor = min(max(scaleFactor, minScale), 1.0)
|
||||||
|
return baseWidth * scaleFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a height that scales based on available screen size
|
||||||
|
static func responsiveHeight(
|
||||||
|
baseHeight: CGFloat,
|
||||||
|
scaleFactor: CGFloat = 1.0,
|
||||||
|
minScale: CGFloat = 0.6
|
||||||
|
) -> CGFloat {
|
||||||
|
let scaleFactor = min(max(scaleFactor, minScale), 1.0)
|
||||||
|
return baseHeight * scaleFactor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Environment key to determine if we're in a compact layout
|
/// Environment key to determine if we're in a compact layout
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ class CameraAccessService: ObservableObject {
|
|||||||
|
|
||||||
@Published var isCameraAuthorized = false
|
@Published var isCameraAuthorized = false
|
||||||
@Published var cameraError: Error?
|
@Published var cameraError: Error?
|
||||||
|
@Published var hasCameraHardware = false
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
checkCameraAuthorizationStatus()
|
checkCameraAuthorizationStatus()
|
||||||
|
checkCameraHardware()
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestCameraAccess() async throws {
|
func requestCameraAccess() async throws {
|
||||||
@@ -68,6 +70,15 @@ class CameraAccessService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkCameraHardware() {
|
||||||
|
let devices = AVCaptureDevice.DiscoverySession(
|
||||||
|
deviceTypes: [.builtInWideAngleCamera],
|
||||||
|
mediaType: .video,
|
||||||
|
position: .unspecified
|
||||||
|
).devices
|
||||||
|
hasCameraHardware = !devices.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
// New method to check if face detection is supported and available
|
// New method to check if face detection is supported and available
|
||||||
func isFaceDetectionAvailable() -> Bool {
|
func isFaceDetectionAvailable() -> Bool {
|
||||||
// On macOS, face detection requires specific Vision framework support
|
// On macOS, face detection requires specific Vision framework support
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ final class SystemSleepManager {
|
|||||||
observers.removeAll()
|
observers.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSystemWillSleep() {
|
func handleSystemWillSleep() {
|
||||||
logInfo("System will sleep")
|
logInfo("System will sleep")
|
||||||
timerEngine?.stop()
|
timerEngine?.stop()
|
||||||
settingsManager.saveImmediately()
|
settingsManager.saveImmediately()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSystemDidWake() {
|
func handleSystemDidWake() {
|
||||||
logInfo("System did wake")
|
logInfo("System did wake")
|
||||||
timerEngine?.start()
|
timerEngine?.start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ struct SetupHeader: View {
|
|||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 28, weight: .bold))
|
.font(.system(size: 28, weight: .bold))
|
||||||
}
|
}
|
||||||
.padding(.top, 20)
|
.padding(.top, 15)
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
Gaze/Views/Components/VisualEffectView.swift
Normal file
19
Gaze/Views/Components/VisualEffectView.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct VisualEffectView: NSViewRepresentable {
|
||||||
|
let material: NSVisualEffectView.Material
|
||||||
|
let blendingMode: NSVisualEffectView.BlendingMode
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||||
|
let view = NSVisualEffectView()
|
||||||
|
view.material = material
|
||||||
|
view.blendingMode = blendingMode
|
||||||
|
view.state = .active
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
||||||
|
nsView.material = material
|
||||||
|
nsView.blendingMode = blendingMode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ struct AdditionalModifiersView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let availableWidth = geometry.size.width - 80 // Account for padding
|
let availableWidth = geometry.size.width - 60 // Account for padding
|
||||||
let availableHeight = geometry.size.height - 200 // Account for header and nav
|
let availableHeight = geometry.size.height - 160 // Account for header and nav
|
||||||
|
|
||||||
let cardWidth = min(
|
let cardWidth = min(
|
||||||
max(availableWidth * 0.85, AdaptiveLayout.Card.minWidth),
|
max(availableWidth * 0.85, AdaptiveLayout.Card.minWidth),
|
||||||
@@ -39,7 +39,6 @@ struct AdditionalModifiersView: View {
|
|||||||
.font(isCompact ? .subheadline : .title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.bottom, isCompact ? 12 : 20)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -209,6 +208,8 @@ struct AdditionalModifiersView: View {
|
|||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObservedObject var cameraService = CameraAccessService.shared
|
||||||
|
|
||||||
private var enforceModeContent: some View {
|
private var enforceModeContent: some View {
|
||||||
VStack(spacing: isCompact ? 10 : 16) {
|
VStack(spacing: isCompact ? 10 : 16) {
|
||||||
Image(systemName: "video.fill")
|
Image(systemName: "video.fill")
|
||||||
@@ -223,10 +224,17 @@ struct AdditionalModifiersView: View {
|
|||||||
.font(isCompact ? .headline : .title2)
|
.font(isCompact ? .headline : .title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text("Use your camera to ensure you take breaks")
|
if !cameraService.hasCameraHardware {
|
||||||
.font(isCompact ? .caption : .subheadline)
|
Text("Camera hardware not detected")
|
||||||
.foregroundStyle(.secondary)
|
.font(isCompact ? .caption : .subheadline)
|
||||||
.multilineTextAlignment(.center)
|
.foregroundStyle(.orange)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
} else {
|
||||||
|
Text("Use your camera to ensure you take breaks")
|
||||||
|
.font(isCompact ? .caption : .subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -235,13 +243,20 @@ struct AdditionalModifiersView: View {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Enable Enforce Mode")
|
Text("Enable Enforce Mode")
|
||||||
.font(isCompact ? .subheadline : .headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
Text("Camera activates before lookaway reminders")
|
if !cameraService.hasCameraHardware {
|
||||||
.font(.caption2)
|
Text("No camera hardware detected")
|
||||||
.foregroundStyle(.secondary)
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
} else {
|
||||||
|
Text("Camera activates before lookaway reminders")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Toggle("", isOn: $settingsManager.settings.enforcementMode)
|
Toggle("", isOn: $settingsManager.settings.enforcementMode)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
.disabled(!cameraService.hasCameraHardware)
|
||||||
.controlSize(isCompact ? .small : .regular)
|
.controlSize(isCompact ? .small : .regular)
|
||||||
}
|
}
|
||||||
.padding(isCompact ? 10 : 16)
|
.padding(isCompact ? 10 : 16)
|
||||||
@@ -252,11 +267,15 @@ struct AdditionalModifiersView: View {
|
|||||||
Text("Camera Access")
|
Text("Camera Access")
|
||||||
.font(isCompact ? .subheadline : .headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
|
|
||||||
if CameraAccessService.shared.isCameraAuthorized {
|
if !cameraService.hasCameraHardware {
|
||||||
|
Label("No camera", systemImage: "xmark.circle.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
} else if cameraService.isCameraAuthorized {
|
||||||
Label("Authorized", systemImage: "checkmark.circle.fill")
|
Label("Authorized", systemImage: "checkmark.circle.fill")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
} else if let error = CameraAccessService.shared.cameraError {
|
} else if let error = cameraService.cameraError {
|
||||||
Label(
|
Label(
|
||||||
error.localizedDescription,
|
error.localizedDescription,
|
||||||
systemImage: "exclamationmark.triangle.fill"
|
systemImage: "exclamationmark.triangle.fill"
|
||||||
@@ -272,11 +291,11 @@ struct AdditionalModifiersView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if !CameraAccessService.shared.isCameraAuthorized {
|
if !cameraService.isCameraAuthorized {
|
||||||
Button("Request Access") {
|
Button("Request Access") {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await CameraAccessService.shared.requestCameraAccess()
|
try await cameraService.requestCameraAccess()
|
||||||
} catch {
|
} catch {
|
||||||
print("Camera access failed: \(error.localizedDescription)")
|
print("Camera access failed: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,24 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VisualEffectView: NSViewRepresentable {
|
|
||||||
let material: NSVisualEffectView.Material
|
|
||||||
let blendingMode: NSVisualEffectView.BlendingMode
|
|
||||||
|
|
||||||
func makeNSView(context: Context) -> NSVisualEffectView {
|
|
||||||
let view = NSVisualEffectView()
|
|
||||||
view.material = material
|
|
||||||
view.blendingMode = blendingMode
|
|
||||||
view.state = .active
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
|
||||||
nsView.material = material
|
|
||||||
nsView.blendingMode = blendingMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class OnboardingWindowPresenter {
|
final class OnboardingWindowPresenter {
|
||||||
static let shared = OnboardingWindowPresenter()
|
static let shared = OnboardingWindowPresenter()
|
||||||
@@ -78,11 +60,16 @@ final class OnboardingWindowPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func createWindow(settingsManager: SettingsManager) {
|
private func createWindow(settingsManager: SettingsManager) {
|
||||||
|
let responsiveWidth = AdaptiveLayout.responsiveWidth(
|
||||||
|
baseWidth: AdaptiveLayout.Window.defaultWidth)
|
||||||
|
let responsiveHeight = AdaptiveLayout.responsiveHeight(
|
||||||
|
baseHeight: AdaptiveLayout.Window.defaultHeight)
|
||||||
|
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
contentRect: NSRect(
|
contentRect: NSRect(
|
||||||
x: 0, y: 0,
|
x: 0, y: 0,
|
||||||
width: AdaptiveLayout.Window.defaultWidth,
|
width: responsiveWidth,
|
||||||
height: AdaptiveLayout.Window.defaultHeight
|
height: responsiveHeight
|
||||||
),
|
),
|
||||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
@@ -178,8 +165,10 @@ struct OnboardingContainerView: View {
|
|||||||
.environment(\.isCompactLayout, isCompact)
|
.environment(\.isCompactLayout, isCompact)
|
||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: AdaptiveLayout.Window.minWidth,
|
minWidth: AdaptiveLayout.Window.minWidth * 0.6,
|
||||||
minHeight: AdaptiveLayout.Window.minHeight
|
maxWidth: AdaptiveLayout.Window.defaultWidth * 1.1,
|
||||||
|
minHeight: AdaptiveLayout.Window.minHeight,
|
||||||
|
maxHeight: AdaptiveLayout.Window.defaultHeight * 1.1
|
||||||
)
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
MenuBarGuideOverlayPresenter.shared.updateVisibility(isVisible: currentPage == 1)
|
MenuBarGuideOverlayPresenter.shared.updateVisibility(isVisible: currentPage == 1)
|
||||||
@@ -239,7 +228,7 @@ struct OnboardingContainerView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, isCompact ? 24 : 40)
|
.padding(.horizontal, isCompact ? 24 : 40)
|
||||||
.padding(.bottom, isCompact ? 12 : 20)
|
.padding(.vertical, isCompact ? 12 : 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func completeOnboarding() {
|
private func completeOnboarding() {
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ struct SettingsWindowView: View {
|
|||||||
}
|
}
|
||||||
.environment(\.isCompactLayout, isCompact)
|
.environment(\.isCompactLayout, isCompact)
|
||||||
}
|
}
|
||||||
.frame(minWidth: AdaptiveLayout.Window.minWidth, minHeight: AdaptiveLayout.Window.minHeight)
|
.frame(
|
||||||
|
minWidth: AdaptiveLayout.Window.minWidth * 0.7,
|
||||||
|
maxWidth: AdaptiveLayout.Window.defaultWidth * 1.2,
|
||||||
|
minHeight: AdaptiveLayout.Window.minHeight,
|
||||||
|
maxHeight: AdaptiveLayout.Window.defaultHeight * 1.2
|
||||||
|
)
|
||||||
.onReceive(tabSwitchPublisher) { notification in
|
.onReceive(tabSwitchPublisher) { notification in
|
||||||
if let tab = notification.object as? Int,
|
if let tab = notification.object as? Int,
|
||||||
let section = SettingsSection(rawValue: tab) {
|
let section = SettingsSection(rawValue: tab) {
|
||||||
|
|||||||
@@ -151,47 +151,10 @@ struct MenuBarContentView: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 4) {
|
IncompleteOnboardingView(onOpenOnboarding: onOpenOnboarding)
|
||||||
Button(action: {
|
|
||||||
onOpenOnboarding()
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
Text("Complete Onboarding")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
}
|
|
||||||
.buttonStyle(MenuBarHoverButtonStyle())
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
QuitRow(onQuit: onQuit)
|
||||||
Button(action: onQuit) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "power")
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
Text("Quit Gaze")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
}
|
|
||||||
.buttonStyle(MenuBarHoverButtonStyle())
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
Spacer()
|
|
||||||
Text(
|
|
||||||
"v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0")"
|
|
||||||
)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.frame(width: 300)
|
.frame(width: 300)
|
||||||
@@ -226,6 +189,69 @@ struct MenuBarContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
struct IncompleteOnboardingView: View {
|
||||||
|
@State private var isHovering = false
|
||||||
|
|
||||||
|
let onOpenOnboarding: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Button(action: {
|
||||||
|
onOpenOnboarding()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundStyle(isHovering ? .white : .accentColor)
|
||||||
|
Text("Complete Onboarding")
|
||||||
|
.foregroundStyle(isHovering ? .white : .primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.onHover { hovering in
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(MenuBarHoverButtonStyle())
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct QuitRow: View {
|
||||||
|
@State private var isHovering = false
|
||||||
|
|
||||||
|
let onQuit: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Button(action: onQuit) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "power")
|
||||||
|
.foregroundStyle(isHovering ? .white : .red)
|
||||||
|
Text("Quit Gaze")
|
||||||
|
.foregroundStyle(isHovering ? .white : .primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.onHover { hovering in
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(MenuBarHoverButtonStyle())
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
Spacer()
|
||||||
|
Text(
|
||||||
|
"v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0")"
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct TimerStatusRowWithIndividualControls: View {
|
struct TimerStatusRowWithIndividualControls: View {
|
||||||
let identifier: TimerIdentifier
|
let identifier: TimerIdentifier
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ struct EnforceModeSetupView: View {
|
|||||||
@State private var showCalibrationWindow = false
|
@State private var showCalibrationWindow = false
|
||||||
@ObservedObject var calibrationManager = CalibrationManager.shared
|
@ObservedObject var calibrationManager = CalibrationManager.shared
|
||||||
|
|
||||||
|
private var cameraHardwareAvailable: Bool {
|
||||||
|
cameraService.hasCameraHardware
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
||||||
@@ -42,9 +46,15 @@ struct EnforceModeSetupView: View {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Enable Enforce Mode")
|
Text("Enable Enforce Mode")
|
||||||
.font(isCompact ? .subheadline : .headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
Text("Camera activates 3 seconds before lookaway reminders")
|
if !cameraHardwareAvailable {
|
||||||
.font(.caption2)
|
Text("No camera hardware detected")
|
||||||
.foregroundStyle(.secondary)
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
} else {
|
||||||
|
Text("Camera activates 3 seconds before lookaway reminders")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Toggle(
|
Toggle(
|
||||||
@@ -65,7 +75,7 @@ struct EnforceModeSetupView: View {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.disabled(isProcessingToggle)
|
.disabled(isProcessingToggle || !cameraHardwareAvailable)
|
||||||
.controlSize(isCompact ? .small : .regular)
|
.controlSize(isCompact ? .small : .regular)
|
||||||
}
|
}
|
||||||
.padding(isCompact ? 10 : 16)
|
.padding(isCompact ? 10 : 16)
|
||||||
@@ -378,6 +388,11 @@ struct EnforceModeSetupView: View {
|
|||||||
defer { isProcessingToggle = false }
|
defer { isProcessingToggle = false }
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
|
guard cameraHardwareAvailable else {
|
||||||
|
print("⚠️ Cannot enable enforce mode - no camera hardware")
|
||||||
|
settingsManager.settings.enforcementMode = false
|
||||||
|
return
|
||||||
|
}
|
||||||
print("🎛️ Enabling enforce mode...")
|
print("🎛️ Enabling enforce mode...")
|
||||||
await enforceModeService.enableEnforceMode()
|
await enforceModeService.enableEnforceMode()
|
||||||
print("🎛️ Enforce mode enabled: \(enforceModeService.isEnforceModeEnabled)")
|
print("🎛️ Enforce mode enabled: \(enforceModeService.isEnforceModeEnabled)")
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ struct LookAwaySetupView: View {
|
|||||||
VStack(spacing: 30) {
|
VStack(spacing: 30) {
|
||||||
InfoBox(
|
InfoBox(
|
||||||
text: "Suggested: 20-20-20 rule",
|
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."
|
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(
|
SliderSection(
|
||||||
@@ -34,7 +35,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(
|
||||||
@@ -52,20 +54,6 @@ struct LookAwaySetupView: View {
|
|||||||
type: "Look away",
|
type: "Look away",
|
||||||
previewFunc: showPreviewWindow
|
previewFunc: showPreviewWindow
|
||||||
)
|
)
|
||||||
|
|
||||||
Toggle("Enable enforcement mode", isOn: $settingsManager.settings.enforcementMode)
|
|
||||||
.onChange(of: settingsManager.settings.enforcementMode) { _, newMode in
|
|
||||||
if newMode && !cameraAccess.isCameraAuthorized {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await cameraAccess.requestCameraAccess()
|
|
||||||
} catch {
|
|
||||||
failedCameraAccess = true
|
|
||||||
settingsManager.settings.enforcementMode = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -15,18 +15,25 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
|
|
||||||
var testEnv: TestEnvironment!
|
var testEnv: TestEnvironment!
|
||||||
var timerEngine: TimerEngine!
|
var timerEngine: TimerEngine!
|
||||||
|
var systemSleepManager: SystemSleepManager!
|
||||||
var cancellables: Set<AnyCancellable>!
|
var cancellables: Set<AnyCancellable>!
|
||||||
|
|
||||||
override func setUp() async throws {
|
override func setUp() async throws {
|
||||||
testEnv = TestEnvironment(settings: .defaults)
|
testEnv = TestEnvironment(settings: .defaults)
|
||||||
timerEngine = testEnv.container.timerEngine
|
timerEngine = testEnv.container.timerEngine
|
||||||
|
systemSleepManager = SystemSleepManager(
|
||||||
|
timerEngine: timerEngine,
|
||||||
|
settingsManager: testEnv.settingsManager
|
||||||
|
)
|
||||||
cancellables = []
|
cancellables = []
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() async throws {
|
override func tearDown() async throws {
|
||||||
timerEngine?.stop()
|
timerEngine?.stop()
|
||||||
|
systemSleepManager?.stopObserving()
|
||||||
cancellables = nil
|
cancellables = nil
|
||||||
timerEngine = nil
|
timerEngine = nil
|
||||||
|
systemSleepManager = nil
|
||||||
testEnv = nil
|
testEnv = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,20 +259,20 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
|
|
||||||
// MARK: - System Sleep/Wake Tests
|
// MARK: - System Sleep/Wake Tests
|
||||||
|
|
||||||
func testHandleSystemSleep() {
|
func testSystemSleepManagerHandlesSleep() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
let statesBefore = timerEngine.timerStates.count
|
let statesBefore = timerEngine.timerStates.count
|
||||||
|
|
||||||
timerEngine.handleSystemSleep()
|
systemSleepManager.handleSystemWillSleep()
|
||||||
|
|
||||||
// States should still exist
|
// States should still exist
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
|
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHandleSystemWake() {
|
func testSystemSleepManagerHandlesWake() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
timerEngine.handleSystemSleep()
|
systemSleepManager.handleSystemWillSleep()
|
||||||
timerEngine.handleSystemWake()
|
systemSleepManager.handleSystemDidWake()
|
||||||
|
|
||||||
// Should handle wake event without crashing
|
// Should handle wake event without crashing
|
||||||
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
|
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user