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() {
|
||||
settingsManager.settingsPublisher
|
||||
.map { $0.smartMode }
|
||||
|
||||
@@ -44,6 +44,26 @@ enum AdaptiveLayout {
|
||||
static let heroTitleSmall: CGFloat = 24
|
||||
static let cardIcon: CGFloat = 32
|
||||
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
|
||||
@@ -63,6 +83,26 @@ enum AdaptiveLayout {
|
||||
static let backOffset: CGFloat = 24
|
||||
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
|
||||
|
||||
@@ -14,9 +14,11 @@ class CameraAccessService: ObservableObject {
|
||||
|
||||
@Published var isCameraAuthorized = false
|
||||
@Published var cameraError: Error?
|
||||
@Published var hasCameraHardware = false
|
||||
|
||||
private init() {
|
||||
checkCameraAuthorizationStatus()
|
||||
checkCameraHardware()
|
||||
}
|
||||
|
||||
func requestCameraAccess() async throws {
|
||||
@@ -67,6 +69,15 @@ class CameraAccessService: ObservableObject {
|
||||
cameraError = CameraAccessError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func isFaceDetectionAvailable() -> Bool {
|
||||
|
||||
@@ -50,13 +50,13 @@ final class SystemSleepManager {
|
||||
observers.removeAll()
|
||||
}
|
||||
|
||||
private func handleSystemWillSleep() {
|
||||
func handleSystemWillSleep() {
|
||||
logInfo("System will sleep")
|
||||
timerEngine?.stop()
|
||||
settingsManager.saveImmediately()
|
||||
}
|
||||
|
||||
private func handleSystemDidWake() {
|
||||
func handleSystemDidWake() {
|
||||
logInfo("System did wake")
|
||||
timerEngine?.start()
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ struct SetupHeader: View {
|
||||
Text(title)
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
}
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 30)
|
||||
.padding(.top, 15)
|
||||
.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 {
|
||||
GeometryReader { geometry in
|
||||
let availableWidth = geometry.size.width - 80 // Account for padding
|
||||
let availableHeight = geometry.size.height - 200 // Account for header and nav
|
||||
let availableWidth = geometry.size.width - 60 // Account for padding
|
||||
let availableHeight = geometry.size.height - 160 // Account for header and nav
|
||||
|
||||
let cardWidth = min(
|
||||
max(availableWidth * 0.85, AdaptiveLayout.Card.minWidth),
|
||||
@@ -39,7 +39,6 @@ struct AdditionalModifiersView: View {
|
||||
.font(isCompact ? .subheadline : .title3)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, isCompact ? 12 : 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -209,6 +208,8 @@ struct AdditionalModifiersView: View {
|
||||
.frame(width: width, height: height)
|
||||
}
|
||||
|
||||
@ObservedObject var cameraService = CameraAccessService.shared
|
||||
|
||||
private var enforceModeContent: some View {
|
||||
VStack(spacing: isCompact ? 10 : 16) {
|
||||
Image(systemName: "video.fill")
|
||||
@@ -223,10 +224,17 @@ struct AdditionalModifiersView: View {
|
||||
.font(isCompact ? .headline : .title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Use your camera to ensure you take breaks")
|
||||
.font(isCompact ? .caption : .subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
if !cameraService.hasCameraHardware {
|
||||
Text("Camera hardware not detected")
|
||||
.font(isCompact ? .caption : .subheadline)
|
||||
.foregroundStyle(.orange)
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
Text("Use your camera to ensure you take breaks")
|
||||
.font(isCompact ? .caption : .subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -235,13 +243,20 @@ struct AdditionalModifiersView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Enable Enforce Mode")
|
||||
.font(isCompact ? .subheadline : .headline)
|
||||
Text("Camera activates before lookaway reminders")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
if !cameraService.hasCameraHardware {
|
||||
Text("No camera hardware detected")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
} else {
|
||||
Text("Camera activates before lookaway reminders")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $settingsManager.settings.enforcementMode)
|
||||
.labelsHidden()
|
||||
.disabled(!cameraService.hasCameraHardware)
|
||||
.controlSize(isCompact ? .small : .regular)
|
||||
}
|
||||
.padding(isCompact ? 10 : 16)
|
||||
@@ -252,11 +267,15 @@ struct AdditionalModifiersView: View {
|
||||
Text("Camera Access")
|
||||
.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")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.green)
|
||||
} else if let error = CameraAccessService.shared.cameraError {
|
||||
} else if let error = cameraService.cameraError {
|
||||
Label(
|
||||
error.localizedDescription,
|
||||
systemImage: "exclamationmark.triangle.fill"
|
||||
@@ -272,11 +291,11 @@ struct AdditionalModifiersView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if !CameraAccessService.shared.isCameraAuthorized {
|
||||
if !cameraService.isCameraAuthorized {
|
||||
Button("Request Access") {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await CameraAccessService.shared.requestCameraAccess()
|
||||
try await cameraService.requestCameraAccess()
|
||||
} catch {
|
||||
print("Camera access failed: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
@@ -7,24 +7,6 @@
|
||||
|
||||
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
|
||||
final class OnboardingWindowPresenter {
|
||||
static let shared = OnboardingWindowPresenter()
|
||||
@@ -78,11 +60,16 @@ final class OnboardingWindowPresenter {
|
||||
}
|
||||
|
||||
private func createWindow(settingsManager: SettingsManager) {
|
||||
let responsiveWidth = AdaptiveLayout.responsiveWidth(
|
||||
baseWidth: AdaptiveLayout.Window.defaultWidth)
|
||||
let responsiveHeight = AdaptiveLayout.responsiveHeight(
|
||||
baseHeight: AdaptiveLayout.Window.defaultHeight)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(
|
||||
x: 0, y: 0,
|
||||
width: AdaptiveLayout.Window.defaultWidth,
|
||||
height: AdaptiveLayout.Window.defaultHeight
|
||||
width: responsiveWidth,
|
||||
height: responsiveHeight
|
||||
),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
@@ -178,8 +165,10 @@ struct OnboardingContainerView: View {
|
||||
.environment(\.isCompactLayout, isCompact)
|
||||
}
|
||||
.frame(
|
||||
minWidth: AdaptiveLayout.Window.minWidth,
|
||||
minHeight: AdaptiveLayout.Window.minHeight
|
||||
minWidth: AdaptiveLayout.Window.minWidth * 0.6,
|
||||
maxWidth: AdaptiveLayout.Window.defaultWidth * 1.1,
|
||||
minHeight: AdaptiveLayout.Window.minHeight,
|
||||
maxHeight: AdaptiveLayout.Window.defaultHeight * 1.1
|
||||
)
|
||||
.onAppear {
|
||||
MenuBarGuideOverlayPresenter.shared.updateVisibility(isVisible: currentPage == 1)
|
||||
@@ -239,7 +228,7 @@ struct OnboardingContainerView: View {
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, isCompact ? 24 : 40)
|
||||
.padding(.bottom, isCompact ? 12 : 20)
|
||||
.padding(.vertical, isCompact ? 12 : 20)
|
||||
}
|
||||
|
||||
private func completeOnboarding() {
|
||||
|
||||
@@ -34,7 +34,12 @@ struct SettingsWindowView: View {
|
||||
}
|
||||
.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
|
||||
if let tab = notification.object as? Int,
|
||||
let section = SettingsSection(rawValue: tab) {
|
||||
|
||||
@@ -151,47 +151,10 @@ struct MenuBarContentView: View {
|
||||
|
||||
Divider()
|
||||
} else {
|
||||
VStack(spacing: 4) {
|
||||
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)
|
||||
IncompleteOnboardingView(onOpenOnboarding: onOpenOnboarding)
|
||||
}
|
||||
|
||||
HStack {
|
||||
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)
|
||||
QuitRow(onQuit: onQuit)
|
||||
|
||||
}
|
||||
.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 {
|
||||
let identifier: TimerIdentifier
|
||||
|
||||
@@ -25,6 +25,10 @@ struct EnforceModeSetupView: View {
|
||||
@State private var showCalibrationWindow = false
|
||||
@ObservedObject var calibrationManager = CalibrationManager.shared
|
||||
|
||||
private var cameraHardwareAvailable: Bool {
|
||||
cameraService.hasCameraHardware
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
||||
@@ -42,9 +46,15 @@ struct EnforceModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Enable Enforce Mode")
|
||||
.font(isCompact ? .subheadline : .headline)
|
||||
Text("Camera activates 3 seconds before lookaway reminders")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
if !cameraHardwareAvailable {
|
||||
Text("No camera hardware detected")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
} else {
|
||||
Text("Camera activates 3 seconds before lookaway reminders")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Toggle(
|
||||
@@ -65,7 +75,7 @@ struct EnforceModeSetupView: View {
|
||||
)
|
||||
)
|
||||
.labelsHidden()
|
||||
.disabled(isProcessingToggle)
|
||||
.disabled(isProcessingToggle || !cameraHardwareAvailable)
|
||||
.controlSize(isCompact ? .small : .regular)
|
||||
}
|
||||
.padding(isCompact ? 10 : 16)
|
||||
@@ -378,6 +388,11 @@ struct EnforceModeSetupView: View {
|
||||
defer { isProcessingToggle = false }
|
||||
|
||||
if enabled {
|
||||
guard cameraHardwareAvailable else {
|
||||
print("⚠️ Cannot enable enforce mode - no camera hardware")
|
||||
settingsManager.settings.enforcementMode = false
|
||||
return
|
||||
}
|
||||
print("🎛️ Enabling enforce mode...")
|
||||
await enforceModeService.enableEnforceMode()
|
||||
print("🎛️ Enforce mode enabled: \(enforceModeService.isEnforceModeEnabled)")
|
||||
|
||||
@@ -22,7 +22,8 @@ struct LookAwaySetupView: View {
|
||||
VStack(spacing: 30) {
|
||||
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."
|
||||
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(
|
||||
@@ -34,7 +35,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(
|
||||
@@ -52,22 +54,8 @@ struct LookAwaySetupView: View {
|
||||
type: "Look away",
|
||||
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()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
Reference in New Issue
Block a user