ui cleanup

This commit is contained in:
Michael Freno
2026-01-28 14:11:57 -05:00
parent 224f6d2a68
commit 8731dc84cf
13 changed files with 226 additions and 109 deletions

View File

@@ -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 }

View File

@@ -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

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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)
}
}

View 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
}
}

View File

@@ -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)")
}

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)")

View File

@@ -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)