general: foregroundColor -> foregroundStyle (will change in future)
This commit is contained in:
@@ -10,15 +10,15 @@ import SwiftUI
|
||||
struct EyeTrackingCalibrationView: View {
|
||||
@StateObject private var calibrationManager = CalibrationManager.shared
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
||||
@State private var countdownValue = 3
|
||||
@State private var isCountingDown = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Full-screen black background
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
|
||||
if calibrationManager.isCalibrating {
|
||||
calibrationContentView
|
||||
} else {
|
||||
@@ -27,51 +27,55 @@ struct EyeTrackingCalibrationView: View {
|
||||
}
|
||||
.frame(minWidth: 800, minHeight: 600)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Introduction Screen
|
||||
|
||||
|
||||
private var introductionScreenView: some View {
|
||||
VStack(spacing: 30) {
|
||||
Image(systemName: "eye.circle.fill")
|
||||
.font(.system(size: 80))
|
||||
.foregroundColor(.blue)
|
||||
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
Text("Eye Tracking Calibration")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.white)
|
||||
.fontWeight(.bold)
|
||||
|
||||
|
||||
Text("This calibration will help improve eye tracking accuracy.")
|
||||
.font(.title3)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
VStack(alignment: .leading, spacing: 15) {
|
||||
InstructionRow(icon: "1.circle.fill", text: "Look at each target on the screen")
|
||||
InstructionRow(icon: "2.circle.fill", text: "Keep your head still, only move your eyes")
|
||||
InstructionRow(
|
||||
icon: "2.circle.fill", text: "Keep your head still, only move your eyes")
|
||||
InstructionRow(icon: "3.circle.fill", text: "Follow the countdown at each position")
|
||||
InstructionRow(icon: "4.circle.fill", text: "Takes about 30-45 seconds")
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
|
||||
|
||||
if calibrationManager.calibrationData.isComplete {
|
||||
VStack(spacing: 10) {
|
||||
Text("Last calibration:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.gray)
|
||||
Text(calibrationManager.getCalibrationSummary())
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
|
||||
HStack(spacing: 20) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.buttonStyle(.plain)
|
||||
.keyboardShortcut(.escape, modifiers: [])
|
||||
|
||||
|
||||
Button("Start Calibration") {
|
||||
startCalibration()
|
||||
}
|
||||
@@ -83,9 +87,9 @@ struct EyeTrackingCalibrationView: View {
|
||||
.padding(60)
|
||||
.frame(maxWidth: 600)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Calibration Content
|
||||
|
||||
|
||||
private var calibrationContentView: some View {
|
||||
ZStack {
|
||||
// Progress indicator at top
|
||||
@@ -93,12 +97,12 @@ struct EyeTrackingCalibrationView: View {
|
||||
progressBar
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
// Calibration target
|
||||
if let step = calibrationManager.currentStep {
|
||||
calibrationTarget(for: step)
|
||||
}
|
||||
|
||||
|
||||
// Skip button at bottom
|
||||
VStack {
|
||||
Spacer()
|
||||
@@ -106,19 +110,19 @@ struct EyeTrackingCalibrationView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Progress Bar
|
||||
|
||||
|
||||
private var progressBar: some View {
|
||||
VStack(spacing: 10) {
|
||||
HStack {
|
||||
Text("Calibrating...")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
Spacer()
|
||||
Text(calibrationManager.progressText)
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
|
||||
|
||||
ProgressView(value: calibrationManager.progress)
|
||||
.progressViewStyle(.linear)
|
||||
.tint(.blue)
|
||||
@@ -126,13 +130,13 @@ struct EyeTrackingCalibrationView: View {
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.5))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Calibration Target
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private func calibrationTarget(for step: CalibrationStep) -> some View {
|
||||
let position = targetPosition(for: step)
|
||||
|
||||
|
||||
VStack(spacing: 20) {
|
||||
// Target circle with countdown
|
||||
ZStack {
|
||||
@@ -141,29 +145,31 @@ struct EyeTrackingCalibrationView: View {
|
||||
.stroke(Color.blue.opacity(0.3), lineWidth: 3)
|
||||
.frame(width: 100, height: 100)
|
||||
.scaleEffect(isCountingDown ? 1.2 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true), value: isCountingDown)
|
||||
|
||||
.animation(
|
||||
.easeInOut(duration: 0.6).repeatForever(autoreverses: true),
|
||||
value: isCountingDown)
|
||||
|
||||
// Inner circle
|
||||
Circle()
|
||||
.fill(Color.blue)
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
|
||||
// Countdown number or checkmark
|
||||
if isCountingDown && countdownValue > 0 {
|
||||
Text("\(countdownValue)")
|
||||
.font(.system(size: 36, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
} else if calibrationManager.samplesCollected > 0 {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 30, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Instruction text
|
||||
Text(step.instructionText)
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 40)
|
||||
.padding(.vertical, 15)
|
||||
.background(Color.black.opacity(0.7))
|
||||
@@ -174,15 +180,15 @@ struct EyeTrackingCalibrationView: View {
|
||||
startStepCountdown()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Skip Button
|
||||
|
||||
|
||||
private var skipButton: some View {
|
||||
Button {
|
||||
calibrationManager.skipStep()
|
||||
} label: {
|
||||
Text("Skip this position")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color.white.opacity(0.2))
|
||||
@@ -190,17 +196,17 @@ struct EyeTrackingCalibrationView: View {
|
||||
}
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
|
||||
private func startCalibration() {
|
||||
calibrationManager.startCalibration()
|
||||
}
|
||||
|
||||
|
||||
private func startStepCountdown() {
|
||||
countdownValue = 3
|
||||
isCountingDown = true
|
||||
|
||||
|
||||
// Countdown 3, 2, 1
|
||||
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
|
||||
if countdownValue > 0 {
|
||||
@@ -211,16 +217,16 @@ struct EyeTrackingCalibrationView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func targetPosition(for step: CalibrationStep) -> CGPoint {
|
||||
let screenBounds = NSScreen.main?.frame ?? CGRect(x: 0, y: 0, width: 1920, height: 1080)
|
||||
let width = screenBounds.width
|
||||
let height = screenBounds.height
|
||||
|
||||
|
||||
let centerX = width / 2
|
||||
let centerY = height / 2
|
||||
let margin: CGFloat = 150
|
||||
|
||||
|
||||
switch step {
|
||||
case .center:
|
||||
return CGPoint(x: centerX, y: centerY)
|
||||
@@ -253,15 +259,16 @@ struct EyeTrackingCalibrationView: View {
|
||||
struct InstructionRow: View {
|
||||
let icon: String
|
||||
let text: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(.blue)
|
||||
.foregroundStyle(.blue)
|
||||
.frame(width: 30)
|
||||
|
||||
|
||||
Text(text)
|
||||
.foregroundStyle(.white)
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
|
||||
struct GazeOverlayView: View {
|
||||
@ObservedObject var eyeTrackingService: EyeTrackingService
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
inFrameIndicator
|
||||
@@ -19,7 +19,7 @@ struct GazeOverlayView: View {
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
|
||||
|
||||
private var inFrameIndicator: some View {
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
@@ -28,7 +28,7 @@ struct GazeOverlayView: View {
|
||||
Text(eyeTrackingService.isInFrame ? "In Frame" : "No Face")
|
||||
.font(.caption2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
@@ -37,16 +37,18 @@ struct GazeOverlayView: View {
|
||||
.fill(Color.black.opacity(0.6))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private var gazeDirectionGrid: some View {
|
||||
let currentDirection = eyeTrackingService.gazeDirection
|
||||
let currentPos = currentDirection.gridPosition
|
||||
|
||||
|
||||
return VStack(spacing: 2) {
|
||||
ForEach(0..<3, id: \.self) { row in
|
||||
HStack(spacing: 2) {
|
||||
ForEach(0..<3, id: \.self) { col in
|
||||
let isActive = currentPos.x == col && currentPos.y == row && eyeTrackingService.isInFrame
|
||||
let isActive =
|
||||
currentPos.x == col && currentPos.y == row
|
||||
&& eyeTrackingService.isInFrame
|
||||
gridCell(row: row, col: col, isActive: isActive)
|
||||
}
|
||||
}
|
||||
@@ -58,21 +60,21 @@ struct GazeOverlayView: View {
|
||||
.fill(Color.black.opacity(0.5))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func gridCell(row: Int, col: Int, isActive: Bool) -> some View {
|
||||
let direction = directionFor(row: row, col: col)
|
||||
|
||||
|
||||
return ZStack {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(isActive ? Color.green : Color.white.opacity(0.2))
|
||||
|
||||
|
||||
Text(direction.rawValue)
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(isActive ? .white : .white.opacity(0.6))
|
||||
.foregroundStyle(isActive ? .white : .white.opacity(0.6))
|
||||
}
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
|
||||
|
||||
private func directionFor(row: Int, col: Int) -> GazeDirection {
|
||||
switch (col, row) {
|
||||
case (0, 0): return .upLeft
|
||||
@@ -87,7 +89,7 @@ struct GazeOverlayView: View {
|
||||
default: return .center
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var ratioDebugView: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
// Show individual L/R ratios
|
||||
@@ -95,38 +97,39 @@ struct GazeOverlayView: View {
|
||||
if let leftH = eyeTrackingService.debugLeftPupilRatio {
|
||||
Text("L.H: \(String(format: "%.2f", leftH))")
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
if let rightH = eyeTrackingService.debugRightPupilRatio {
|
||||
Text("R.H: \(String(format: "%.2f", rightH))")
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if let leftV = eyeTrackingService.debugLeftVerticalRatio {
|
||||
Text("L.V: \(String(format: "%.2f", leftV))")
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
if let rightV = eyeTrackingService.debugRightVerticalRatio {
|
||||
Text("R.V: \(String(format: "%.2f", rightV))")
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show averaged ratios
|
||||
if let leftH = eyeTrackingService.debugLeftPupilRatio,
|
||||
let rightH = eyeTrackingService.debugRightPupilRatio,
|
||||
let leftV = eyeTrackingService.debugLeftVerticalRatio,
|
||||
let rightV = eyeTrackingService.debugRightVerticalRatio {
|
||||
let rightH = eyeTrackingService.debugRightPupilRatio,
|
||||
let leftV = eyeTrackingService.debugLeftVerticalRatio,
|
||||
let rightV = eyeTrackingService.debugRightVerticalRatio
|
||||
{
|
||||
let avgH = (leftH + rightH) / 2.0
|
||||
let avgV = (leftV + rightV) / 2.0
|
||||
Text("Avg H:\(String(format: "%.2f", avgH)) V:\(String(format: "%.2f", avgV))")
|
||||
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(.yellow)
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
@@ -136,15 +139,15 @@ struct GazeOverlayView: View {
|
||||
.fill(Color.black.opacity(0.5))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private var eyeImagesDebugView: some View {
|
||||
HStack(spacing: 12) {
|
||||
// Left eye
|
||||
VStack(spacing: 4) {
|
||||
Text("Left")
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
.foregroundStyle(.white)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
eyeImageView(
|
||||
image: eyeTrackingService.debugLeftEyeInput,
|
||||
@@ -160,13 +163,13 @@ struct GazeOverlayView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Right eye
|
||||
VStack(spacing: 4) {
|
||||
Text("Right")
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
.foregroundStyle(.white)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
eyeImageView(
|
||||
image: eyeTrackingService.debugRightEyeInput,
|
||||
@@ -189,10 +192,12 @@ struct GazeOverlayView: View {
|
||||
.fill(Color.black.opacity(0.5))
|
||||
)
|
||||
}
|
||||
|
||||
private func eyeImageView(image: NSImage?, pupilPosition: PupilPosition?, eyeSize: CGSize?, label: String) -> some View {
|
||||
|
||||
private func eyeImageView(
|
||||
image: NSImage?, pupilPosition: PupilPosition?, eyeSize: CGSize?, label: String
|
||||
) -> some View {
|
||||
let displaySize: CGFloat = 50
|
||||
|
||||
|
||||
return VStack(spacing: 2) {
|
||||
ZStack {
|
||||
if let nsImage = image {
|
||||
@@ -201,15 +206,17 @@ struct GazeOverlayView: View {
|
||||
.interpolation(.none)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: displaySize, height: displaySize)
|
||||
|
||||
|
||||
// Draw pupil position marker
|
||||
if let pupil = pupilPosition, let size = eyeSize, size.width > 0, size.height > 0 {
|
||||
if let pupil = pupilPosition, let size = eyeSize, size.width > 0,
|
||||
size.height > 0
|
||||
{
|
||||
let scaleX = displaySize / size.width
|
||||
let scaleY = displaySize / size.height
|
||||
let scale = min(scaleX, scaleY)
|
||||
let scaledWidth = size.width * scale
|
||||
let scaledHeight = size.height * scale
|
||||
|
||||
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 4, height: 4)
|
||||
@@ -224,15 +231,15 @@ struct GazeOverlayView: View {
|
||||
.frame(width: displaySize, height: displaySize)
|
||||
Text("--")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.foregroundStyle(.white.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.frame(width: displaySize, height: displaySize)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
|
||||
|
||||
Text(label)
|
||||
.font(.system(size: 7))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,27 +10,27 @@ import SwiftUI
|
||||
struct InfoBox: View {
|
||||
let text: String
|
||||
let url: String?
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
if let url = url, let urlObj = URL(string: url) {
|
||||
Button(action: {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.open(urlObj)
|
||||
UIApplication.shared.open(urlObj)
|
||||
#elseif os(macOS)
|
||||
NSWorkspace.shared.open(urlObj)
|
||||
NSWorkspace.shared.open(urlObj)
|
||||
#endif
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}.buttonStyle(.plain)
|
||||
} else {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Text(text)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
@@ -38,6 +38,10 @@ struct InfoBox: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
InfoBox(text: "This is an informational message that provides helpful context to the user.", url: "https://www.healthline.com/health/eye-health/20-20-20-rule")
|
||||
.padding()
|
||||
}
|
||||
InfoBox(
|
||||
text: "This is an informational message that provides helpful context to the user.",
|
||||
url: "https://www.healthline.com/health/eye-health/20-20-20-rule"
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,18 @@ import SwiftUI
|
||||
/// Draws pupil detection markers directly on top of the camera preview
|
||||
struct PupilOverlayView: View {
|
||||
@ObservedObject var eyeTrackingService: EyeTrackingService
|
||||
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let viewSize = geometry.size
|
||||
|
||||
|
||||
// Draw eye regions and pupil markers
|
||||
ZStack {
|
||||
// Left eye
|
||||
if let leftRegion = eyeTrackingService.debugLeftEyeRegion,
|
||||
let leftPupil = eyeTrackingService.debugLeftPupilPosition,
|
||||
let imageSize = eyeTrackingService.debugImageSize {
|
||||
let leftPupil = eyeTrackingService.debugLeftPupilPosition,
|
||||
let imageSize = eyeTrackingService.debugImageSize
|
||||
{
|
||||
EyeOverlayShape(
|
||||
eyeRegion: leftRegion,
|
||||
pupilPosition: leftPupil,
|
||||
@@ -30,11 +31,12 @@ struct PupilOverlayView: View {
|
||||
label: "L"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Right eye
|
||||
if let rightRegion = eyeTrackingService.debugRightEyeRegion,
|
||||
let rightPupil = eyeTrackingService.debugRightPupilPosition,
|
||||
let imageSize = eyeTrackingService.debugImageSize {
|
||||
let rightPupil = eyeTrackingService.debugRightPupilPosition,
|
||||
let imageSize = eyeTrackingService.debugImageSize
|
||||
{
|
||||
EyeOverlayShape(
|
||||
eyeRegion: rightRegion,
|
||||
pupilPosition: rightPupil,
|
||||
@@ -57,7 +59,7 @@ private struct EyeOverlayShape: View {
|
||||
let viewSize: CGSize
|
||||
let color: Color
|
||||
let label: String
|
||||
|
||||
|
||||
private var transformedCoordinates: (eyeRect: CGRect, pupilPoint: CGPoint) {
|
||||
// Standard macOS Camera Coordinate System (Landscape):
|
||||
// Raw Buffer:
|
||||
@@ -71,20 +73,20 @@ private struct EyeOverlayShape: View {
|
||||
// - Screen Y increases Down
|
||||
// - BUT the image content is flipped horizontally
|
||||
// (Raw Left is Screen Right, Raw Right is Screen Left)
|
||||
|
||||
|
||||
// Use dimensions directly (no rotation swap)
|
||||
let rawImageWidth = imageSize.width
|
||||
let rawImageHeight = imageSize.height
|
||||
|
||||
|
||||
// Calculate aspect-fill scaling
|
||||
// We compare the raw aspect ratio to the view aspect ratio
|
||||
let imageAspect = rawImageWidth / rawImageHeight
|
||||
let viewAspect = viewSize.width / viewSize.height
|
||||
|
||||
|
||||
let scale: CGFloat
|
||||
let offsetX: CGFloat
|
||||
let offsetY: CGFloat
|
||||
|
||||
|
||||
if imageAspect > viewAspect {
|
||||
// Image is wider than view - crop sides (pillarbox behavior in aspect fill)
|
||||
// Wait, aspect fill means we fill the view, so we crop the excess.
|
||||
@@ -98,7 +100,7 @@ private struct EyeOverlayShape: View {
|
||||
offsetX = 0
|
||||
offsetY = (viewSize.height - rawImageHeight * scale) / 2
|
||||
}
|
||||
|
||||
|
||||
// Transform Eye Region
|
||||
// Mirroring X: The 'left' of the raw image becomes the 'right' of the screen
|
||||
// Raw Rect: x, y, w, h
|
||||
@@ -107,47 +109,47 @@ private struct EyeOverlayShape: View {
|
||||
let eyeRawY = eyeRegion.frame.origin.y
|
||||
let eyeRawW = eyeRegion.frame.width
|
||||
let eyeRawH = eyeRegion.frame.height
|
||||
|
||||
|
||||
// Calculate Screen Coordinates
|
||||
let eyeScreenX = (rawImageWidth - (eyeRawX + eyeRawW)) * scale + offsetX
|
||||
let eyeScreenY = eyeRawY * scale + offsetY
|
||||
let eyeScreenW = eyeRawW * scale
|
||||
let eyeScreenH = eyeRawH * scale
|
||||
|
||||
|
||||
// Transform Pupil Position
|
||||
// Global Raw Pupil X = eyeRawX + pupilPosition.x
|
||||
// Global Raw Pupil Y = eyeRawY + pupilPosition.y
|
||||
let pupilGlobalRawX = eyeRawX + pupilPosition.x
|
||||
let pupilGlobalRawY = eyeRawY + pupilPosition.y
|
||||
|
||||
|
||||
// Mirror X for Pupil
|
||||
let pupilScreenX = (rawImageWidth - pupilGlobalRawX) * scale + offsetX
|
||||
let pupilScreenY = pupilGlobalRawY * scale + offsetY
|
||||
|
||||
|
||||
return (
|
||||
eyeRect: CGRect(x: eyeScreenX, y: eyeScreenY, width: eyeScreenW, height: eyeScreenH),
|
||||
pupilPoint: CGPoint(x: pupilScreenX, y: pupilScreenY)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
let coords = transformedCoordinates
|
||||
let eyeRect = coords.eyeRect
|
||||
let pupilPoint = coords.pupilPoint
|
||||
|
||||
|
||||
ZStack {
|
||||
// Eye region rectangle
|
||||
Rectangle()
|
||||
.stroke(color, lineWidth: 2)
|
||||
.frame(width: eyeRect.width, height: eyeRect.height)
|
||||
.position(x: eyeRect.midX, y: eyeRect.midY)
|
||||
|
||||
|
||||
// Pupil marker (red dot)
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
.position(x: pupilPoint.x, y: pupilPoint.y)
|
||||
|
||||
|
||||
// Crosshair at pupil position
|
||||
Path { path in
|
||||
path.move(to: CGPoint(x: pupilPoint.x - 6, y: pupilPoint.y))
|
||||
@@ -156,18 +158,18 @@ private struct EyeOverlayShape: View {
|
||||
path.addLine(to: CGPoint(x: pupilPoint.x, y: pupilPoint.y + 6))
|
||||
}
|
||||
.stroke(Color.red, lineWidth: 1)
|
||||
|
||||
|
||||
// Label
|
||||
Text(label)
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundColor(color)
|
||||
.foregroundStyle(color)
|
||||
.position(x: eyeRect.minX + 8, y: eyeRect.minY - 8)
|
||||
|
||||
|
||||
// Debug: Show raw coordinates
|
||||
Text("\(label): (\(Int(pupilPosition.x)), \(Int(pupilPosition.y)))")
|
||||
.font(.system(size: 8, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.7))
|
||||
.foregroundStyle(.white)
|
||||
.background(.black.opacity(0.7))
|
||||
.position(x: eyeRect.midX, y: eyeRect.maxY + 10)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct SetupHeader: View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(color)
|
||||
.foregroundStyle(color)
|
||||
Text(title)
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ struct SliderSection: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Remind me every:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
@@ -62,7 +62,7 @@ struct SliderSection: View {
|
||||
if let range = countdownSettings.range {
|
||||
Text("Look away for:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
@@ -89,14 +89,14 @@ struct SliderSection: View {
|
||||
reminderText
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
Text(
|
||||
"\(type) reminders are currently disabled."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -104,10 +104,10 @@ struct SliderSection: View {
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "eye")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
Text("Preview Reminder")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
@@ -90,7 +90,8 @@ final class OnboardingWindowPresenter {
|
||||
self?.windowController = nil
|
||||
self?.removeCloseObserver()
|
||||
}
|
||||
NotificationCenter.default.post(name: Notification.Name("OnboardingWindowDidClose"), object: nil)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name("OnboardingWindowDidClose"), object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +143,9 @@ struct OnboardingContainerView: View {
|
||||
}
|
||||
}
|
||||
#if APPSTORE
|
||||
.frame(minWidth: 1000, minHeight: 700)
|
||||
.frame(minWidth: 1000, minHeight: 700)
|
||||
#else
|
||||
.frame(minWidth: 1000, minHeight: 900)
|
||||
.frame(minWidth: 1000, minHeight: 900)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -159,11 +160,12 @@ struct OnboardingContainerView: View {
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffectIfAvailable(GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -173,11 +175,14 @@ struct OnboardingContainerView: View {
|
||||
currentPage += 1
|
||||
}
|
||||
}) {
|
||||
Text(currentPage == 0 ? "Let's Get Started" : currentPage == 5 ? "Get Started" : "Continue")
|
||||
.font(.headline)
|
||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
|
||||
.foregroundColor(.white)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||
Text(
|
||||
currentPage == 0
|
||||
? "Let's Get Started" : currentPage == 5 ? "Get Started" : "Continue"
|
||||
)
|
||||
.font(.headline)
|
||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
|
||||
.foregroundStyle(.white)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffectIfAvailable(
|
||||
|
||||
@@ -50,7 +50,7 @@ struct MenuBarHoverButtonStyle: ButtonStyle {
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundColor(isHovered ? .white : .primary)
|
||||
.foregroundStyle(isHovered ? .white : .primary)
|
||||
.glassEffectIfAvailable(
|
||||
isHovered
|
||||
? GlassStyle.regular.tint(.accentColor).interactive()
|
||||
@@ -83,7 +83,7 @@ struct MenuBarContentView: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Active Timers")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
|
||||
@@ -176,7 +176,7 @@ struct MenuBarContentView: View {
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
Text("Complete Onboarding")
|
||||
Spacer()
|
||||
}
|
||||
@@ -193,7 +193,7 @@ struct MenuBarContentView: View {
|
||||
Button(action: onQuit) {
|
||||
HStack {
|
||||
Image(systemName: "power")
|
||||
.foregroundColor(.red)
|
||||
.foregroundStyle(.red)
|
||||
Text("Quit Gaze")
|
||||
Spacer()
|
||||
}
|
||||
@@ -207,7 +207,7 @@ struct MenuBarContentView: View {
|
||||
"v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0")"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
@@ -335,20 +335,20 @@ struct TimerStatusRowWithIndividualControls: View {
|
||||
}
|
||||
|
||||
Image(systemName: iconName)
|
||||
.foregroundColor(isHoveredBody ? .white : color)
|
||||
.foregroundStyle(isHoveredBody ? .white : color)
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(displayName)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(isHoveredBody ? .white : .primary)
|
||||
.foregroundStyle(isHoveredBody ? .white : .primary)
|
||||
.lineLimit(1)
|
||||
|
||||
if let state = state {
|
||||
Text(state.remainingSeconds.asTimerDuration)
|
||||
.font(.caption)
|
||||
.foregroundColor(isHoveredBody ? .white.opacity(0.8) : .secondary)
|
||||
.foregroundStyle(isHoveredBody ? .white.opacity(0.8) : .secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
@@ -365,7 +365,7 @@ struct TimerStatusRowWithIndividualControls: View {
|
||||
Button(action: onDevTrigger) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(isHoveredDevTrigger ? .white : .yellow)
|
||||
.foregroundStyle(isHoveredDevTrigger ? .white : .yellow)
|
||||
.padding(6)
|
||||
.contentShape(Circle())
|
||||
}
|
||||
@@ -394,7 +394,7 @@ struct TimerStatusRowWithIndividualControls: View {
|
||||
systemName: isPaused ? "play.circle" : "pause.circle"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(isHoveredPauseButton ? .white : .accentColor)
|
||||
.foregroundStyle(isHoveredPauseButton ? .white : .accentColor)
|
||||
.padding(6)
|
||||
.contentShape(Circle())
|
||||
}
|
||||
@@ -416,7 +416,7 @@ struct TimerStatusRowWithIndividualControls: View {
|
||||
Button(action: onSkip) {
|
||||
Image(systemName: "forward.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(isHoveredSkip ? .white : .accentColor)
|
||||
.foregroundStyle(isHoveredSkip ? .white : .accentColor)
|
||||
.padding(6)
|
||||
.contentShape(Circle())
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ struct LookAwayReminderView: View {
|
||||
VStack(spacing: 40) {
|
||||
Text("Look Away")
|
||||
.font(.system(size: 64, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text("Look at something 20 feet away")
|
||||
.font(.system(size: 28))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
|
||||
GazeLottieView(
|
||||
animationName: AnimationAsset.lookAway.fileName,
|
||||
@@ -62,14 +62,14 @@ struct LookAwayReminderView: View {
|
||||
|
||||
Text("\(remainingSeconds)")
|
||||
.font(.system(size: 48, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.monospacedDigit()
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Reminders.countdownLabel)
|
||||
}
|
||||
|
||||
Text("Press ESC or Space to skip")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
|
||||
// Skip button in corner
|
||||
@@ -79,7 +79,7 @@ struct LookAwayReminderView: View {
|
||||
Button(action: dismiss) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Reminders.dismissButton)
|
||||
|
||||
@@ -22,7 +22,7 @@ struct PostureReminderView: View {
|
||||
VStack {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.system(size: scale))
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
.opacity(opacity)
|
||||
.offset(y: yOffset)
|
||||
|
||||
@@ -32,19 +32,19 @@ struct UserTimerOverlayReminderView: View {
|
||||
VStack(spacing: 40) {
|
||||
Text(timer.title)
|
||||
.font(.system(size: 64, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
if let message = timer.message, !message.isEmpty {
|
||||
Text(message)
|
||||
.font(.system(size: 28))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
|
||||
Image(systemName: "clock.fill")
|
||||
.font(.system(size: 120))
|
||||
.foregroundColor(timer.color)
|
||||
.foregroundStyle(timer.color)
|
||||
.padding(.vertical, 30)
|
||||
|
||||
// Countdown display
|
||||
@@ -62,13 +62,13 @@ struct UserTimerOverlayReminderView: View {
|
||||
|
||||
Text("\(remainingSeconds)")
|
||||
.font(.system(size: 48, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
Text("Press ESC or Space to dismiss")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
|
||||
// Dismiss button in corner
|
||||
@@ -78,7 +78,7 @@ struct UserTimerOverlayReminderView: View {
|
||||
Button(action: dismiss) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(30)
|
||||
|
||||
@@ -27,12 +27,12 @@ struct UserTimerReminderView: View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "clock.fill")
|
||||
.font(.system(size: baseSize * 0.4))
|
||||
.foregroundColor(timer.color)
|
||||
.foregroundStyle(timer.color)
|
||||
|
||||
if let message = timer.message, !message.isEmpty {
|
||||
Text(message)
|
||||
.font(.system(size: baseSize * 0.24))
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
@@ -21,45 +21,62 @@ struct BlinkSetupView: View {
|
||||
VStack(spacing: 30) {
|
||||
HStack(spacing: 12) {
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://www.aao.org/eye-health/tips-prevention/computer-usage#:~:text=Humans normally blink about 15 times in one minute. However, studies show that we only blink about 5 to 7 times in a minute while using computers and other digital screen devices.") {
|
||||
if let url = URL(
|
||||
string:
|
||||
"https://www.aao.org/eye-health/tips-prevention/computer-usage#:~:text=Humans normally blink about 15 times in one minute. However, studies show that we only blink about 5 to 7 times in a minute while using computers and other digital screen devices."
|
||||
) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text("We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes.")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(
|
||||
"We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes."
|
||||
)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Toggle("Enable Blink Reminders", isOn: $settingsManager.settings.blinkTimer.enabled)
|
||||
.font(.headline)
|
||||
Toggle(
|
||||
"Enable Blink Reminders", isOn: $settingsManager.settings.blinkTimer.enabled
|
||||
)
|
||||
.font(.headline)
|
||||
|
||||
if settingsManager.settings.blinkTimer.enabled {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Remind me every:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(settingsManager.settings.blinkTimer.intervalSeconds / 60) },
|
||||
set: { settingsManager.settings.blinkTimer.intervalSeconds = Int($0) * 60 }
|
||||
get: {
|
||||
Double(
|
||||
settingsManager.settings.blinkTimer.intervalSeconds
|
||||
/ 60)
|
||||
},
|
||||
set: {
|
||||
settingsManager.settings.blinkTimer.intervalSeconds =
|
||||
Int($0) * 60
|
||||
}
|
||||
),
|
||||
in: 1...20,
|
||||
step: 1
|
||||
)
|
||||
|
||||
Text("\(settingsManager.settings.blinkTimer.intervalSeconds / 60) min")
|
||||
.frame(width: 60, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
Text(
|
||||
"\(settingsManager.settings.blinkTimer.intervalSeconds / 60) min"
|
||||
)
|
||||
.frame(width: 60, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,29 +85,33 @@ struct BlinkSetupView: View {
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
if settingsManager.settings.blinkTimer.enabled {
|
||||
Text("You will be subtly reminded every \(settingsManager.settings.blinkTimer.intervalSeconds / 60) minutes to blink")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text(
|
||||
"You will be subtly reminded every \(settingsManager.settings.blinkTimer.intervalSeconds / 60) minutes to blink"
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Blink reminders are currently disabled.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Button(action: showPreviewWindow) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "eye")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
Text("Preview Reminder")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10))
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -104,7 +125,9 @@ struct BlinkSetupView: View {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
previewWindowController = PreviewWindowHelper.showPreview(
|
||||
on: screen,
|
||||
content: BlinkReminderView(sizePercentage: settingsManager.settings.subtleReminderSize.percentage) { [weak previewWindowController] in
|
||||
content: BlinkReminderView(
|
||||
sizePercentage: settingsManager.settings.subtleReminderSize.percentage
|
||||
) { [weak previewWindowController] in
|
||||
previewWindowController?.window?.close()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,55 +11,55 @@ struct CompletionView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
Spacer()
|
||||
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 80))
|
||||
.foregroundColor(.green)
|
||||
|
||||
.foregroundStyle(.green)
|
||||
|
||||
Text("You're All Set!")
|
||||
.font(.system(size: 36, weight: .bold))
|
||||
|
||||
|
||||
Text("Gaze will now help you take care of your eyes and posture")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("What happens next:")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: "menubar.rectangle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.frame(width: 30)
|
||||
Text("Gaze will appear in your menu bar")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: "clock")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.frame(width: 30)
|
||||
Text("Timers will start automatically")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: "gearshape")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.frame(width: 30)
|
||||
Text("Adjust settings anytime from the menu bar")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: "plus.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.frame(width: 30)
|
||||
Text("Create custom timers in Settings for additional reminders")
|
||||
.font(.subheadline)
|
||||
@@ -68,7 +68,7 @@ struct CompletionView: View {
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 600, height: 450)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct EnforceModeSetupView: View {
|
||||
@Bindable var settingsManager: SettingsManager
|
||||
@@ -33,7 +33,7 @@ struct EnforceModeSetupView: View {
|
||||
VStack(spacing: 30) {
|
||||
Text("Use your camera to ensure you take breaks")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
@@ -43,7 +43,7 @@ struct EnforceModeSetupView: View {
|
||||
.font(.headline)
|
||||
Text("Camera activates 3 seconds before lookaway reminders")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle(
|
||||
@@ -149,7 +149,7 @@ struct EnforceModeSetupView: View {
|
||||
HStack {
|
||||
Image(systemName: "target")
|
||||
.font(.title3)
|
||||
.foregroundColor(.blue)
|
||||
.foregroundStyle(.blue)
|
||||
Text("Eye Tracking Calibration")
|
||||
.font(.headline)
|
||||
}
|
||||
@@ -158,7 +158,7 @@ struct EnforceModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(calibrationManager.getCalibrationSummary())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if calibrationManager.needsRecalibration() {
|
||||
Label(
|
||||
@@ -166,17 +166,17 @@ struct EnforceModeSetupView: View {
|
||||
systemImage: "exclamationmark.triangle.fill"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
} else {
|
||||
Label("Calibration active and valid", systemImage: "checkmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Not calibrated - using default thresholds")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -214,10 +214,10 @@ struct EnforceModeSetupView: View {
|
||||
if let layer = previewLayer {
|
||||
ZStack {
|
||||
CameraPreviewView(previewLayer: layer, borderColor: borderColor)
|
||||
|
||||
|
||||
// Pupil detection overlay (drawn on video)
|
||||
PupilOverlayView(eyeTrackingService: eyeTrackingService)
|
||||
|
||||
|
||||
// Debug info overlay (top-right corner)
|
||||
VStack {
|
||||
HStack {
|
||||
@@ -258,7 +258,7 @@ struct EnforceModeSetupView: View {
|
||||
/*? "✓ Break compliance detected" : "⚠️ Please look away from screen"*/
|
||||
/*)*/
|
||||
/*.font(.caption)*/
|
||||
/*.foregroundColor(lookingAway ? .green : .orange)*/
|
||||
/*.foregroundStyle(lookingAway ? .green : .orange)*/
|
||||
/*.frame(maxWidth: .infinity, alignment: .center)*/
|
||||
/*.padding(.top, 4)*/
|
||||
/*}*/
|
||||
@@ -277,15 +277,15 @@ struct EnforceModeSetupView: View {
|
||||
if cameraService.isCameraAuthorized {
|
||||
Label("Authorized", systemImage: "checkmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
.foregroundStyle(.green)
|
||||
} else if let error = cameraService.cameraError {
|
||||
Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
} else {
|
||||
Label("Not authorized", systemImage: "xmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,14 +337,14 @@ struct EnforceModeSetupView: View {
|
||||
HStack {
|
||||
Image(systemName: "timer")
|
||||
.font(.title2)
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Camera Ready")
|
||||
.font(.headline)
|
||||
Text("Will activate 3 seconds before lookaway reminder")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -357,11 +357,11 @@ struct EnforceModeSetupView: View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(isActive ? .green : .secondary)
|
||||
.foregroundStyle(isActive ? .green : .secondary)
|
||||
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -372,7 +372,7 @@ struct EnforceModeSetupView: View {
|
||||
HStack {
|
||||
Image(systemName: "lock.shield.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.blue)
|
||||
.foregroundStyle(.blue)
|
||||
Text("Privacy Information")
|
||||
.font(.headline)
|
||||
}
|
||||
@@ -381,11 +381,10 @@ struct EnforceModeSetupView: View {
|
||||
privacyBullet("All processing happens on-device")
|
||||
privacyBullet("No images are stored or transmitted")
|
||||
privacyBullet("Camera only active during lookaway reminders (3 second window)")
|
||||
privacyBullet("Eyes closed does not affect countdown")
|
||||
privacyBullet("You can disable at any time")
|
||||
privacyBullet("You can always force quit with cmd+q")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(
|
||||
@@ -396,7 +395,7 @@ struct EnforceModeSetupView: View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.blue)
|
||||
.foregroundStyle(.blue)
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
@@ -442,7 +441,7 @@ struct EnforceModeSetupView: View {
|
||||
systemName: eyeTrackingService.enableDebugLogging
|
||||
? "ant.circle.fill" : "ant.circle"
|
||||
)
|
||||
.foregroundColor(eyeTrackingService.enableDebugLogging ? .orange : .secondary)
|
||||
.foregroundStyle(eyeTrackingService.enableDebugLogging ? .orange : .secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Toggle console debug logging")
|
||||
@@ -461,7 +460,7 @@ struct EnforceModeSetupView: View {
|
||||
Text("Live Values:")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if let leftRatio = eyeTrackingService.debugLeftPupilRatio,
|
||||
let rightRatio = eyeTrackingService.debugRightPupilRatio
|
||||
@@ -470,23 +469,23 @@ struct EnforceModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Left Pupil: \(String(format: "%.3f", leftRatio))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(
|
||||
.foregroundStyle(
|
||||
!EyeTrackingConstants.minPupilEnabled
|
||||
&& !EyeTrackingConstants.maxPupilEnabled
|
||||
? .secondary
|
||||
: (leftRatio < EyeTrackingConstants.minPupilRatio
|
||||
|| leftRatio > EyeTrackingConstants.maxPupilRatio)
|
||||
? .orange : .green
|
||||
? Color.orange : Color.green
|
||||
)
|
||||
Text("Right Pupil: \(String(format: "%.3f", rightRatio))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(
|
||||
.foregroundStyle(
|
||||
!EyeTrackingConstants.minPupilEnabled
|
||||
&& !EyeTrackingConstants.maxPupilEnabled
|
||||
? .secondary
|
||||
: (rightRatio < EyeTrackingConstants.minPupilRatio
|
||||
|| rightRatio > EyeTrackingConstants.maxPupilRatio)
|
||||
? .orange : .green
|
||||
? Color.orange : Color.green
|
||||
)
|
||||
}
|
||||
|
||||
@@ -497,7 +496,7 @@ struct EnforceModeSetupView: View {
|
||||
"Range: \(String(format: "%.2f", EyeTrackingConstants.minPupilRatio)) - \(String(format: "%.2f", EyeTrackingConstants.maxPupilRatio))"
|
||||
)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
let bothEyesOut =
|
||||
(leftRatio < EyeTrackingConstants.minPupilRatio
|
||||
|| leftRatio > EyeTrackingConstants.maxPupilRatio)
|
||||
@@ -505,13 +504,13 @@ struct EnforceModeSetupView: View {
|
||||
|| rightRatio > EyeTrackingConstants.maxPupilRatio)
|
||||
Text(bothEyesOut ? "Both Out ⚠️" : "In Range ✓")
|
||||
.font(.caption2)
|
||||
.foregroundColor(bothEyesOut ? .orange : .green)
|
||||
.foregroundStyle(bothEyesOut ? .orange : .green)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Pupil data unavailable")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let yaw = eyeTrackingService.debugYaw,
|
||||
@@ -521,21 +520,21 @@ struct EnforceModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Yaw: \(String(format: "%.3f", yaw))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(
|
||||
.foregroundStyle(
|
||||
!EyeTrackingConstants.yawEnabled
|
||||
? .secondary
|
||||
: abs(yaw) > EyeTrackingConstants.yawThreshold
|
||||
? .orange : .green
|
||||
? Color.orange : Color.green
|
||||
)
|
||||
Text("Pitch: \(String(format: "%.3f", pitch))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(
|
||||
.foregroundStyle(
|
||||
!EyeTrackingConstants.pitchUpEnabled
|
||||
&& !EyeTrackingConstants.pitchDownEnabled
|
||||
? .secondary
|
||||
: (pitch > EyeTrackingConstants.pitchUpThreshold
|
||||
|| pitch < EyeTrackingConstants.pitchDownThreshold)
|
||||
? .orange : .green
|
||||
? Color.orange : Color.green
|
||||
)
|
||||
}
|
||||
|
||||
@@ -546,12 +545,12 @@ struct EnforceModeSetupView: View {
|
||||
"Yaw Max: \(String(format: "%.2f", EyeTrackingConstants.yawThreshold))"
|
||||
)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(
|
||||
"Pitch: \(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) to \(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold))"
|
||||
)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,48 +564,54 @@ struct EnforceModeSetupView: View {
|
||||
Text("Current Threshold Values:")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack {
|
||||
Text("Yaw Threshold:")
|
||||
Spacer()
|
||||
Text("\(String(format: "%.2f", EyeTrackingConstants.yawThreshold)) rad")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Text("Pitch Up Threshold:")
|
||||
Spacer()
|
||||
Text("\(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold)) rad")
|
||||
.foregroundColor(.secondary)
|
||||
Text(
|
||||
"\(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold)) rad"
|
||||
)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Text("Pitch Down Threshold:")
|
||||
Spacer()
|
||||
Text("\(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) rad")
|
||||
.foregroundColor(.secondary)
|
||||
Text(
|
||||
"\(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) rad"
|
||||
)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Text("Min Pupil Ratio:")
|
||||
Spacer()
|
||||
Text("\(String(format: "%.2f", EyeTrackingConstants.minPupilRatio))")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Text("Max Pupil Ratio:")
|
||||
Spacer()
|
||||
Text("\(String(format: "%.2f", EyeTrackingConstants.maxPupilRatio))")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Text("Eye Closed Threshold:")
|
||||
Spacer()
|
||||
Text("\(String(format: "%.3f", EyeTrackingConstants.eyeClosedThreshold))")
|
||||
.foregroundColor(.secondary)
|
||||
Text(
|
||||
"\(String(format: "%.3f", EyeTrackingConstants.eyeClosedThreshold))"
|
||||
)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
@@ -622,7 +627,7 @@ struct EnforceModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Debug Eye Tracking Data")
|
||||
.font(.headline)
|
||||
.foregroundColor(.blue)
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Face Detected: \(eyeTrackingService.faceDetected ? "Yes" : "No")")
|
||||
@@ -643,7 +648,7 @@ struct EnforceModeSetupView: View {
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
@@ -14,26 +14,28 @@ struct GeneralSetupView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SetupHeader(icon: "gearshape.fill", title: isOnboarding ? "Final Settings" : "General Settings", color: .accentColor)
|
||||
SetupHeader(
|
||||
icon: "gearshape.fill", title: isOnboarding ? "Final Settings" : "General Settings",
|
||||
color: .accentColor)
|
||||
|
||||
Spacer()
|
||||
VStack(spacing: 30) {
|
||||
Text("Configure app preferences and support the project")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
launchAtLoginToggle
|
||||
|
||||
|
||||
#if !APPSTORE
|
||||
softwareUpdatesSection
|
||||
softwareUpdatesSection
|
||||
#endif
|
||||
|
||||
subtleReminderSizeSection
|
||||
|
||||
#if !APPSTORE
|
||||
supportSection
|
||||
supportSection
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -51,7 +53,7 @@ struct GeneralSetupView: View {
|
||||
.font(.headline)
|
||||
Text("Start Gaze automatically when you log in")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $settingsManager.settings.launchAtLogin)
|
||||
@@ -65,42 +67,45 @@ struct GeneralSetupView: View {
|
||||
}
|
||||
|
||||
#if !APPSTORE
|
||||
private var softwareUpdatesSection: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Software Updates")
|
||||
.font(.headline)
|
||||
private var softwareUpdatesSection: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Software Updates")
|
||||
.font(.headline)
|
||||
|
||||
if let lastCheck = updateManager.lastUpdateCheckDate {
|
||||
Text("Last checked: \(lastCheck, style: .relative)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
} else {
|
||||
Text("Never checked for updates")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
if let lastCheck = updateManager.lastUpdateCheckDate {
|
||||
Text("Last checked: \(lastCheck, style: .relative)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.italic()
|
||||
} else {
|
||||
Text("Never checked for updates")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.italic()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Check for Updates Now") {
|
||||
updateManager.checkForUpdates()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Toggle(
|
||||
"Automatically check for updates",
|
||||
isOn: Binding(
|
||||
get: { updateManager.automaticallyChecksForUpdates },
|
||||
set: { updateManager.automaticallyChecksForUpdates = $0 }
|
||||
)
|
||||
)
|
||||
.labelsHidden()
|
||||
.help("Check for new versions of Gaze in the background")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Check for Updates Now") {
|
||||
updateManager.checkForUpdates()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Toggle("Automatically check for updates", isOn: Binding(
|
||||
get: { updateManager.automaticallyChecksForUpdates },
|
||||
set: { updateManager.automaticallyChecksForUpdates = $0 }
|
||||
))
|
||||
.labelsHidden()
|
||||
.help("Check for new versions of Gaze in the background")
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
}
|
||||
#endif
|
||||
|
||||
private var subtleReminderSizeSection: some View {
|
||||
@@ -110,20 +115,28 @@ struct GeneralSetupView: View {
|
||||
|
||||
Text("Adjust the size of blink and posture reminders")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
ForEach(ReminderSize.allCases, id: \.self) { size in
|
||||
Button(action: { settingsManager.settings.subtleReminderSize = size }) {
|
||||
VStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(settingsManager.settings.subtleReminderSize == size ? Color.accentColor : Color.secondary.opacity(0.3))
|
||||
.fill(
|
||||
settingsManager.settings.subtleReminderSize == size
|
||||
? Color.accentColor : Color.secondary.opacity(0.3)
|
||||
)
|
||||
.frame(width: iconSize(for: size), height: iconSize(for: size))
|
||||
|
||||
Text(size.displayName)
|
||||
.font(.caption)
|
||||
.fontWeight(settingsManager.settings.subtleReminderSize == size ? .semibold : .regular)
|
||||
.foregroundColor(settingsManager.settings.subtleReminderSize == size ? .primary : .secondary)
|
||||
.fontWeight(
|
||||
settingsManager.settings.subtleReminderSize == size
|
||||
? .semibold : .regular
|
||||
)
|
||||
.foregroundStyle(
|
||||
settingsManager.settings.subtleReminderSize == size
|
||||
? .primary : .secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 60)
|
||||
.padding(.vertical, 12)
|
||||
@@ -142,31 +155,31 @@ struct GeneralSetupView: View {
|
||||
}
|
||||
|
||||
#if !APPSTORE
|
||||
private var supportSection: some View {
|
||||
VStack(spacing: 12) {
|
||||
Text("Support & Contribute")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
private var supportSection: some View {
|
||||
VStack(spacing: 12) {
|
||||
Text("Support & Contribute")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
ExternalLinkButton(
|
||||
icon: "chevron.left.forwardslash.chevron.right",
|
||||
title: "View on GitHub",
|
||||
subtitle: "Star the repo, report issues, contribute",
|
||||
url: "https://github.com/mikefreno/Gaze",
|
||||
tint: nil
|
||||
)
|
||||
ExternalLinkButton(
|
||||
icon: "chevron.left.forwardslash.chevron.right",
|
||||
title: "View on GitHub",
|
||||
subtitle: "Star the repo, report issues, contribute",
|
||||
url: "https://github.com/mikefreno/Gaze",
|
||||
tint: nil
|
||||
)
|
||||
|
||||
ExternalLinkButton(
|
||||
icon: "cup.and.saucer.fill",
|
||||
iconColor: .brown,
|
||||
title: "Buy Me a Coffee",
|
||||
subtitle: "Support development of Gaze",
|
||||
url: "https://buymeacoffee.com/mikefreno",
|
||||
tint: .orange
|
||||
)
|
||||
ExternalLinkButton(
|
||||
icon: "cup.and.saucer.fill",
|
||||
iconColor: .brown,
|
||||
title: "Buy Me a Coffee",
|
||||
subtitle: "Support development of Gaze",
|
||||
url: "https://buymeacoffee.com/mikefreno",
|
||||
tint: .orange
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
#endif
|
||||
|
||||
private func applyLaunchAtLoginSetting(enabled: Bool) {
|
||||
@@ -205,14 +218,14 @@ struct ExternalLinkButton: View {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.font(.title3)
|
||||
.foregroundColor(iconColor)
|
||||
.foregroundStyle(iconColor)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
@@ -224,7 +237,8 @@ struct ExternalLinkButton: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffectIfAvailable(
|
||||
tint != nil ? GlassStyle.regular.tint(tint!).interactive() : GlassStyle.regular.interactive(),
|
||||
tint != nil
|
||||
? GlassStyle.regular.tint(tint!).interactive() : GlassStyle.regular.interactive(),
|
||||
in: .rect(cornerRadius: 10)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,21 +21,27 @@ struct PostureSetupView: View {
|
||||
VStack(spacing: 30) {
|
||||
HStack(spacing: 12) {
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://pubmed.ncbi.nlm.nih.gov/40111906/#:~:text=For%20studies%20exploring%20sitting%20posture%2C%20seven%20found%20a%20relationship%20with%20LBP.%20Regarding%20studies%20on%20sitting%20behavior%2C%20only%20one%20showed%20no%20relationship%20between%20LBP%20prevalence") {
|
||||
if let url = URL(
|
||||
string:
|
||||
"https://pubmed.ncbi.nlm.nih.gov/40111906/#:~:text=For%20studies%20exploring%20sitting%20posture%2C%20seven%20found%20a%20relationship%20with%20LBP.%20Regarding%20studies%20on%20sitting%20behavior%2C%20only%20one%20showed%20no%20relationship%20between%20LBP%20prevalence"
|
||||
) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text("Regular posture checks help prevent back and neck pain from prolonged sitting")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(
|
||||
"Regular posture checks help prevent back and neck pain from prolonged sitting"
|
||||
)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
|
||||
SliderSection(
|
||||
intervalSettings: Binding(
|
||||
@@ -46,7 +52,8 @@ struct PostureSetupView: View {
|
||||
)
|
||||
},
|
||||
set: { newValue in
|
||||
settingsManager.settings.postureTimer.intervalSeconds = (newValue.val ?? 30) * 60
|
||||
settingsManager.settings.postureTimer.intervalSeconds =
|
||||
(newValue.val ?? 30) * 60
|
||||
}
|
||||
),
|
||||
countdownSettings: nil,
|
||||
@@ -67,7 +74,9 @@ struct PostureSetupView: View {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
previewWindowController = PreviewWindowHelper.showPreview(
|
||||
on: screen,
|
||||
content: PostureReminderView(sizePercentage: settingsManager.settings.subtleReminderSize.percentage) { [weak previewWindowController] in
|
||||
content: PostureReminderView(
|
||||
sizePercentage: settingsManager.settings.subtleReminderSize.percentage
|
||||
) { [weak previewWindowController] in
|
||||
previewWindowController?.window?.close()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -14,10 +14,10 @@ struct SmartModeSetupView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SetupHeader(icon: "brain.fill", title: "Smart Mode", color: .purple)
|
||||
|
||||
|
||||
Text("Automatically manage timers based on your activity")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
|
||||
Spacer()
|
||||
@@ -42,18 +42,21 @@ struct SmartModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||
.foregroundColor(.blue)
|
||||
.foregroundStyle(.blue)
|
||||
Text("Auto-pause on Fullscreen")
|
||||
.font(.headline)
|
||||
}
|
||||
Text("Timers will automatically pause when you enter fullscreen mode (videos, games, presentations)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(
|
||||
"Timers will automatically pause when you enter fullscreen mode (videos, games, presentations)"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $settingsManager.settings.smartMode.autoPauseOnFullscreen)
|
||||
.labelsHidden()
|
||||
.onChange(of: settingsManager.settings.smartMode.autoPauseOnFullscreen) { _, newValue in
|
||||
.onChange(of: settingsManager.settings.smartMode.autoPauseOnFullscreen) {
|
||||
_, newValue in
|
||||
if newValue {
|
||||
permissionManager.requestAuthorizationIfNeeded()
|
||||
}
|
||||
@@ -61,7 +64,8 @@ struct SmartModeSetupView: View {
|
||||
}
|
||||
|
||||
if settingsManager.settings.smartMode.autoPauseOnFullscreen,
|
||||
permissionManager.authorizationStatus != .authorized {
|
||||
permissionManager.authorizationStatus != .authorized
|
||||
{
|
||||
permissionWarningView
|
||||
}
|
||||
}
|
||||
@@ -81,7 +85,7 @@ struct SmartModeSetupView: View {
|
||||
|
||||
Text("macOS requires Screen Recording permission to detect other apps in fullscreen.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack {
|
||||
Button("Grant Access") {
|
||||
@@ -107,13 +111,13 @@ struct SmartModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Image(systemName: "moon.zzz.fill")
|
||||
.foregroundColor(.indigo)
|
||||
.foregroundStyle(.indigo)
|
||||
Text("Auto-pause on Idle")
|
||||
.font(.headline)
|
||||
}
|
||||
Text("Timers will pause when you're inactive for more than the threshold below")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $settingsManager.settings.smartMode.autoPauseOnIdle)
|
||||
@@ -139,13 +143,15 @@ struct SmartModeSetupView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Image(systemName: "chart.line.uptrend.xyaxis")
|
||||
.foregroundColor(.green)
|
||||
.foregroundStyle(.green)
|
||||
Text("Track Usage Statistics")
|
||||
.font(.headline)
|
||||
}
|
||||
Text("Monitor active and idle time, with automatic reset after the specified duration")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(
|
||||
"Monitor active and idle time, with automatic reset after the specified duration"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $settingsManager.settings.smartMode.trackUsage)
|
||||
@@ -182,7 +188,7 @@ struct ThresholdSlider: View {
|
||||
Spacer()
|
||||
Text("\(value) \(unit)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Slider(
|
||||
|
||||
@@ -17,7 +17,7 @@ struct UserTimersView: View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.purple)
|
||||
.foregroundStyle(.purple)
|
||||
Text("Custom Timers")
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
}
|
||||
@@ -28,14 +28,14 @@ struct UserTimersView: View {
|
||||
VStack(spacing: 30) {
|
||||
Text("Create your own reminder schedules")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
Text("Add up to 3 custom timers with your own intervals and messages")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(
|
||||
@@ -66,13 +66,13 @@ struct UserTimersView: View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "clock.badge.questionmark")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("No custom timers yet")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Click 'Add Timer' to create your first custom reminder")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(40)
|
||||
@@ -153,7 +153,7 @@ struct UserTimerRow: View {
|
||||
.frame(width: 12, height: 12)
|
||||
|
||||
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
||||
.foregroundColor(timer.color)
|
||||
.foregroundStyle(timer.color)
|
||||
.frame(width: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -165,7 +165,7 @@ struct UserTimerRow: View {
|
||||
"\(timer.type.displayName) • \(timer.timeOnScreenSeconds)s on screen • \(timer.intervalMinutes) min interval"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -179,14 +179,14 @@ struct UserTimerRow: View {
|
||||
Button(action: onEdit) {
|
||||
Image(systemName: "pencil.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button(action: { showingDeleteConfirmation = true }) {
|
||||
Image(systemName: "trash.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.red)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.confirmationDialog("Delete Timer", isPresented: $showingDeleteConfirmation) {
|
||||
@@ -264,7 +264,7 @@ struct UserTimerEditSheet: View {
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("Example: \"Stretch Break\", \"Eye Rest\", \"Water Break\"")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
@@ -318,7 +318,7 @@ struct UserTimerEditSheet: View {
|
||||
: "Full screen reminder with animation"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if type == .overlay {
|
||||
@@ -359,7 +359,7 @@ struct UserTimerEditSheet: View {
|
||||
}
|
||||
Text("How often this reminder will appear (in minutes)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
@@ -369,7 +369,7 @@ struct UserTimerEditSheet: View {
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("Leave blank to show a default timer notification")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -14,14 +14,14 @@ struct WelcomeView: View {
|
||||
|
||||
Image(systemName: "eye.fill")
|
||||
.font(.system(size: 80))
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
|
||||
Text("Welcome to Gaze")
|
||||
.font(.system(size: 36, weight: .bold))
|
||||
|
||||
Text("Take care of your eyes and posture")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
FeatureRow(
|
||||
@@ -66,7 +66,7 @@ struct FeatureRow: View {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(iconColor)
|
||||
.foregroundStyle(iconColor)
|
||||
.frame(width: 30)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -74,7 +74,7 @@ struct FeatureRow: View {
|
||||
.font(.headline)
|
||||
Text(description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user