fix: small screen/resolution fixes
This commit is contained in:
108
Gaze/Constants/AdaptiveLayout.swift
Normal file
108
Gaze/Constants/AdaptiveLayout.swift
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// AdaptiveLayout.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Claude on 1/19/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Adaptive layout constants for responsive UI scaling on different display sizes
|
||||||
|
enum AdaptiveLayout {
|
||||||
|
/// Minimum window dimensions
|
||||||
|
enum Window {
|
||||||
|
static let minWidth: CGFloat = 700
|
||||||
|
#if APPSTORE
|
||||||
|
static let minHeight: CGFloat = 500
|
||||||
|
#else
|
||||||
|
static let minHeight: CGFloat = 600
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static let defaultWidth: CGFloat = 900
|
||||||
|
#if APPSTORE
|
||||||
|
static let defaultHeight: CGFloat = 650
|
||||||
|
#else
|
||||||
|
static let defaultHeight: CGFloat = 800
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content area constraints
|
||||||
|
enum Content {
|
||||||
|
/// Maximum width for content cards/sections
|
||||||
|
static let maxWidth: CGFloat = 560
|
||||||
|
/// Minimum width for content cards/sections
|
||||||
|
static let minWidth: CGFloat = 400
|
||||||
|
/// Ideal width for onboarding/welcome cards
|
||||||
|
static let idealCardWidth: CGFloat = 520
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Font sizes that scale based on available space
|
||||||
|
enum Font {
|
||||||
|
static let heroIcon: CGFloat = 60
|
||||||
|
static let heroIconSmall: CGFloat = 48
|
||||||
|
static let heroTitle: CGFloat = 28
|
||||||
|
static let heroTitleSmall: CGFloat = 24
|
||||||
|
static let cardIcon: CGFloat = 32
|
||||||
|
static let cardIconSmall: CGFloat = 28
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spacing values
|
||||||
|
enum Spacing {
|
||||||
|
static let standard: CGFloat = 20
|
||||||
|
static let compact: CGFloat = 12
|
||||||
|
static let section: CGFloat = 30
|
||||||
|
static let sectionCompact: CGFloat = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Card dimensions for swipeable cards
|
||||||
|
enum Card {
|
||||||
|
static let maxWidth: CGFloat = 520
|
||||||
|
static let minWidth: CGFloat = 380
|
||||||
|
static let maxHeight: CGFloat = 480
|
||||||
|
static let minHeight: CGFloat = 360
|
||||||
|
static let backOffset: CGFloat = 24
|
||||||
|
static let backScale: CGFloat = 0.92
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Environment key to determine if we're in a compact layout
|
||||||
|
struct IsCompactLayoutKey: EnvironmentKey {
|
||||||
|
static let defaultValue: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
var isCompactLayout: Bool {
|
||||||
|
get { self[IsCompactLayoutKey.self] }
|
||||||
|
set { self[IsCompactLayoutKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View modifier that adapts layout based on available size
|
||||||
|
struct AdaptiveContainerModifier: ViewModifier {
|
||||||
|
@State private var isCompact = false
|
||||||
|
let compactThreshold: CGFloat
|
||||||
|
|
||||||
|
init(compactThreshold: CGFloat = 600) {
|
||||||
|
self.compactThreshold = compactThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
content
|
||||||
|
.environment(\.isCompactLayout, geometry.size.height < compactThreshold)
|
||||||
|
.onAppear {
|
||||||
|
isCompact = geometry.size.height < compactThreshold
|
||||||
|
}
|
||||||
|
.onChange(of: geometry.size.height) { _, newHeight in
|
||||||
|
isCompact = newHeight < compactThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// Makes the view adapt its layout based on available space
|
||||||
|
func adaptiveContainer(compactThreshold: CGFloat = 600) -> some View {
|
||||||
|
modifier(AdaptiveContainerModifier(compactThreshold: compactThreshold))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,77 +12,91 @@ struct AdditionalModifiersView: View {
|
|||||||
@State private var frontCardIndex: Int = 0
|
@State private var frontCardIndex: Int = 0
|
||||||
@State private var dragOffset: CGFloat = 0
|
@State private var dragOffset: CGFloat = 0
|
||||||
@State private var isDragging: Bool = false
|
@State private var isDragging: Bool = false
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
private let cardWidth: CGFloat = 480
|
private var backCardOffset: CGFloat { isCompact ? 20 : AdaptiveLayout.Card.backOffset }
|
||||||
private let cardHeight: CGFloat = 480
|
private var backCardScale: CGFloat { AdaptiveLayout.Card.backScale }
|
||||||
private let backCardOffset: CGFloat = 30
|
|
||||||
private let backCardScale: CGFloat = 0.92
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
GeometryReader { geometry in
|
||||||
SetupHeader(icon: "slider.horizontal.3", title: "Additional Options", color: .purple)
|
let availableWidth = geometry.size.width - 80 // Account for padding
|
||||||
|
let availableHeight = geometry.size.height - 200 // Account for header and nav
|
||||||
|
|
||||||
Text("Optional features to enhance your experience")
|
let cardWidth = min(
|
||||||
.font(.title3)
|
max(availableWidth * 0.85, AdaptiveLayout.Card.minWidth),
|
||||||
.foregroundStyle(.secondary)
|
AdaptiveLayout.Card.maxWidth
|
||||||
.multilineTextAlignment(.center)
|
)
|
||||||
.padding(.bottom, 20)
|
let cardHeight = min(
|
||||||
|
max(availableHeight * 0.75, AdaptiveLayout.Card.minHeight),
|
||||||
|
AdaptiveLayout.Card.maxHeight
|
||||||
|
)
|
||||||
|
|
||||||
Spacer()
|
VStack(spacing: 0) {
|
||||||
|
SetupHeader(icon: "slider.horizontal.3", title: "Additional Options", color: .purple)
|
||||||
|
|
||||||
// Card stack
|
Text("Optional features to enhance your experience")
|
||||||
ZStack {
|
.font(isCompact ? .subheadline : .title3)
|
||||||
// Card 0 (Enforce Mode)
|
.foregroundStyle(.secondary)
|
||||||
cardView(for: 0)
|
.multilineTextAlignment(.center)
|
||||||
.zIndex(zIndex(for: 0))
|
.padding(.bottom, isCompact ? 12 : 20)
|
||||||
.scaleEffect(scale(for: 0))
|
|
||||||
.offset(x: xOffset(for: 0), y: yOffset(for: 0))
|
|
||||||
.opacity(opacity(for: 0))
|
|
||||||
|
|
||||||
// Card 1 (Smart Mode)
|
Spacer()
|
||||||
cardView(for: 1)
|
|
||||||
.zIndex(zIndex(for: 1))
|
ZStack {
|
||||||
.scaleEffect(scale(for: 1))
|
cardView(for: 0, width: cardWidth, height: cardHeight)
|
||||||
.offset(x: xOffset(for: 1), y: yOffset(for: 1))
|
.zIndex(zIndex(for: 0))
|
||||||
.opacity(opacity(for: 1))
|
.scaleEffect(scale(for: 0))
|
||||||
|
.offset(x: xOffset(for: 0), y: yOffset(for: 0))
|
||||||
|
|
||||||
|
cardView(for: 1, width: cardWidth, height: cardHeight)
|
||||||
|
.zIndex(zIndex(for: 1))
|
||||||
|
.scaleEffect(scale(for: 1))
|
||||||
|
.offset(x: xOffset(for: 1), y: yOffset(for: 1))
|
||||||
|
}
|
||||||
|
.padding(isCompact ? 12 : 20)
|
||||||
|
.gesture(dragGesture)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Navigation controls
|
||||||
|
HStack(spacing: isCompact ? 12 : 20) {
|
||||||
|
Button(action: { swapCards() }) {
|
||||||
|
Image(systemName: "chevron.left")
|
||||||
|
.font(isCompact ? .body : .title2)
|
||||||
|
.frame(width: isCompact ? 36 : 44, height: isCompact ? 36 : 44)
|
||||||
|
.contentShape(.rect)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.glassEffectIfAvailable(
|
||||||
|
GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10)
|
||||||
|
)
|
||||||
|
.opacity(frontCardIndex == 0 ? 0.3 : 1.0)
|
||||||
|
.disabled(frontCardIndex == 0)
|
||||||
|
|
||||||
|
// Page indicators with labels
|
||||||
|
HStack(spacing: isCompact ? 10 : 16) {
|
||||||
|
cardIndicator(index: 0, icon: "video.fill", label: "Enforce")
|
||||||
|
cardIndicator(index: 1, icon: "brain.fill", label: "Smart")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: { swapCards() }) {
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(isCompact ? .body : .title2)
|
||||||
|
.frame(width: isCompact ? 36 : 44, height: isCompact ? 36 : 44)
|
||||||
|
.contentShape(.rect)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.glassEffectIfAvailable(
|
||||||
|
GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10)
|
||||||
|
)
|
||||||
|
.opacity(frontCardIndex == 1 ? 0.3 : 1.0)
|
||||||
|
.disabled(frontCardIndex == 1)
|
||||||
|
}
|
||||||
|
.padding(.bottom, isCompact ? 6 : 10)
|
||||||
}
|
}
|
||||||
.gesture(dragGesture)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.padding()
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Navigation controls
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
Button(action: { swapCards() }) {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
.font(.title2)
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
|
||||||
.disabled(frontCardIndex == 0)
|
|
||||||
.opacity(frontCardIndex == 0 ? 0.4 : 1)
|
|
||||||
|
|
||||||
// Page indicators with labels
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
cardIndicator(index: 0, icon: "video.fill", label: "Enforce")
|
|
||||||
cardIndicator(index: 1, icon: "brain.fill", label: "Smart")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: { swapCards() }) {
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.title2)
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
|
||||||
.disabled(frontCardIndex == 1)
|
|
||||||
.opacity(frontCardIndex == 1 ? 0.4 : 1)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(.clear)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Card Indicator
|
// MARK: - Card Indicator
|
||||||
@@ -101,9 +115,10 @@ struct AdditionalModifiersView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, isCompact ? 10 : 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, isCompact ? 5 : 6)
|
||||||
.foregroundStyle(index == frontCardIndex ? .primary : .secondary)
|
.foregroundStyle(index == frontCardIndex ? .primary : .secondary)
|
||||||
|
.contentShape(.rect)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.glassEffectIfAvailable(
|
.glassEffectIfAvailable(
|
||||||
@@ -152,7 +167,7 @@ struct AdditionalModifiersView: View {
|
|||||||
private func yOffset(for cardIndex: Int) -> CGFloat {
|
private func yOffset(for cardIndex: Int) -> CGFloat {
|
||||||
let isFront = cardIndex == frontCardIndex
|
let isFront = cardIndex == frontCardIndex
|
||||||
let dragProgress = min(abs(dragOffset) / 150, 1.0)
|
let dragProgress = min(abs(dragOffset) / 150, 1.0)
|
||||||
let backPeekY: CGFloat = 15
|
let backPeekY: CGFloat = isCompact ? 10 : 15
|
||||||
|
|
||||||
if isFront {
|
if isFront {
|
||||||
return dragProgress * backPeekY
|
return dragProgress * backPeekY
|
||||||
@@ -175,10 +190,10 @@ struct AdditionalModifiersView: View {
|
|||||||
// MARK: - Card Views
|
// MARK: - Card Views
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func cardView(for index: Int) -> some View {
|
private func cardView(for index: Int, width: CGFloat, height: CGFloat) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(Color(NSColor.windowBackgroundColor).opacity(0.8))
|
.fill(Color(NSColor.windowBackgroundColor))
|
||||||
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
@@ -188,60 +203,64 @@ struct AdditionalModifiersView: View {
|
|||||||
smartModeContent
|
smartModeContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(isCompact ? 12 : 20)
|
||||||
}
|
}
|
||||||
.frame(width: cardWidth, height: cardHeight)
|
.frame(width: width, height: height)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var enforceModeContent: some View {
|
private var enforceModeContent: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: isCompact ? 10 : 16) {
|
||||||
Image(systemName: "video.fill")
|
Image(systemName: "video.fill")
|
||||||
.font(.system(size: 40))
|
.font(.system(size: isCompact ? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon))
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
|
|
||||||
Text("Enforce Mode")
|
Text("Enforce Mode")
|
||||||
.font(.title2)
|
.font(isCompact ? .headline : .title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text("Use your camera to ensure you take breaks")
|
Text("Use your camera to ensure you take breaks")
|
||||||
.font(.subheadline)
|
.font(isCompact ? .caption : .subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: isCompact ? 10 : 16) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Enable Enforce Mode")
|
Text("Enable Enforce Mode")
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
Text("Camera activates before lookaway reminders")
|
Text("Camera activates before lookaway reminders")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Toggle("", isOn: $settingsManager.settings.enforcementMode)
|
Toggle("", isOn: $settingsManager.settings.enforcementMode)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
.controlSize(isCompact ? .small : .regular)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 10 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Camera Access")
|
Text("Camera Access")
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
|
|
||||||
if CameraAccessService.shared.isCameraAuthorized {
|
if CameraAccessService.shared.isCameraAuthorized {
|
||||||
Label("Authorized", systemImage: "checkmark.circle.fill")
|
Label("Authorized", systemImage: "checkmark.circle.fill")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
} else if let error = CameraAccessService.shared.cameraError {
|
} else if let error = CameraAccessService.shared.cameraError {
|
||||||
Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill")
|
Label(
|
||||||
.font(.caption)
|
error.localizedDescription,
|
||||||
.foregroundStyle(.orange)
|
systemImage: "exclamationmark.triangle.fill"
|
||||||
|
)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
} else {
|
} else {
|
||||||
Label("Not authorized", systemImage: "xmark.circle.fill")
|
Label("Not authorized", systemImage: "xmark.circle.fill")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,9 +278,10 @@ struct AdditionalModifiersView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(isCompact ? .small : .regular)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 10 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,23 +290,23 @@ struct AdditionalModifiersView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var smartModeContent: some View {
|
private var smartModeContent: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: isCompact ? 10 : 16) {
|
||||||
Image(systemName: "brain.fill")
|
Image(systemName: "brain.fill")
|
||||||
.font(.system(size: 40))
|
.font(.system(size: isCompact ? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon))
|
||||||
.foregroundStyle(.purple)
|
.foregroundStyle(.purple)
|
||||||
|
|
||||||
Text("Smart Mode")
|
Text("Smart Mode")
|
||||||
.font(.title2)
|
.font(isCompact ? .headline : .title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text("Automatically manage timers based on activity")
|
Text("Automatically manage timers based on activity")
|
||||||
.font(.subheadline)
|
.font(isCompact ? .caption : .subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: isCompact ? 8 : 12) {
|
||||||
smartModeToggle(
|
smartModeToggle(
|
||||||
icon: "arrow.up.left.and.arrow.down.right",
|
icon: "arrow.up.left.and.arrow.down.right",
|
||||||
iconColor: .blue,
|
iconColor: .blue,
|
||||||
@@ -317,19 +337,22 @@ struct AdditionalModifiersView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func smartModeToggle(icon: String, iconColor: Color, title: String, subtitle: String, isOn: Binding<Bool>) -> some View {
|
private func smartModeToggle(
|
||||||
|
icon: String, iconColor: Color, title: String, subtitle: String, isOn: Binding<Bool>
|
||||||
|
) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.foregroundStyle(iconColor)
|
.foregroundStyle(iconColor)
|
||||||
.frame(width: 24)
|
.frame(width: isCompact ? 20 : 24)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.subheadline)
|
.font(isCompact ? .caption : .subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -338,8 +361,8 @@ struct AdditionalModifiersView: View {
|
|||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, isCompact ? 8 : 12)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, isCompact ? 6 : 10)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 10))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +376,9 @@ struct AdditionalModifiersView: View {
|
|||||||
}
|
}
|
||||||
.onEnded { value in
|
.onEnded { value in
|
||||||
let threshold: CGFloat = 80
|
let threshold: CGFloat = 80
|
||||||
let shouldSwap = abs(value.translation.width) > threshold ||
|
let shouldSwap =
|
||||||
abs(value.predictedEndTranslation.width) > 150
|
abs(value.translation.width) > threshold
|
||||||
|
|| abs(value.predictedEndTranslation.width) > 150
|
||||||
|
|
||||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||||
if shouldSwap {
|
if shouldSwap {
|
||||||
|
|||||||
@@ -80,15 +80,11 @@ final class OnboardingWindowPresenter {
|
|||||||
private func createWindow(settingsManager: SettingsManager) {
|
private func createWindow(settingsManager: SettingsManager) {
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
contentRect: NSRect(
|
contentRect: NSRect(
|
||||||
x: 0, y: 0, width: 1000,
|
x: 0, y: 0,
|
||||||
height: {
|
width: AdaptiveLayout.Window.defaultWidth,
|
||||||
#if APPSTORE
|
height: AdaptiveLayout.Window.defaultHeight
|
||||||
return 700
|
),
|
||||||
#else
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
return 1000
|
|
||||||
#endif
|
|
||||||
}()),
|
|
||||||
styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView],
|
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
)
|
)
|
||||||
@@ -128,60 +124,62 @@ struct OnboardingContainerView: View {
|
|||||||
private let lastPageIndex = 7
|
private let lastPageIndex = 7
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
GeometryReader { geometry in
|
||||||
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
let isCompact = geometry.size.height < 600
|
||||||
.ignoresSafeArea()
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
TabView(selection: $currentPage) {
|
|
||||||
WelcomeView()
|
|
||||||
.tag(0)
|
|
||||||
.tabItem { Image(systemName: "hand.wave.fill") }
|
|
||||||
|
|
||||||
MenuBarWelcomeView()
|
ZStack {
|
||||||
.tag(1)
|
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
||||||
.tabItem { Image(systemName: "menubar.rectangle") }
|
.ignoresSafeArea()
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
TabView(selection: $currentPage) {
|
||||||
|
WelcomeView()
|
||||||
|
.tag(0)
|
||||||
|
.tabItem { Image(systemName: "hand.wave.fill") }
|
||||||
|
|
||||||
LookAwaySetupView(settingsManager: settingsManager)
|
MenuBarWelcomeView()
|
||||||
.tag(2)
|
.tag(1)
|
||||||
.tabItem { Image(systemName: "eye.fill") }
|
.tabItem { Image(systemName: "menubar.rectangle") }
|
||||||
|
|
||||||
BlinkSetupView(settingsManager: settingsManager)
|
LookAwaySetupView(settingsManager: settingsManager)
|
||||||
.tag(3)
|
.tag(2)
|
||||||
.tabItem { Image(systemName: "eye.circle.fill") }
|
.tabItem { Image(systemName: "eye.fill") }
|
||||||
|
|
||||||
PostureSetupView(settingsManager: settingsManager)
|
BlinkSetupView(settingsManager: settingsManager)
|
||||||
.tag(4)
|
.tag(3)
|
||||||
.tabItem { Image(systemName: "figure.stand") }
|
.tabItem { Image(systemName: "eye.circle.fill") }
|
||||||
|
|
||||||
AdditionalModifiersView(settingsManager: settingsManager)
|
PostureSetupView(settingsManager: settingsManager)
|
||||||
.tag(5)
|
.tag(4)
|
||||||
.tabItem { Image(systemName: "slider.horizontal.3") }
|
.tabItem { Image(systemName: "figure.stand") }
|
||||||
|
|
||||||
GeneralSetupView(settingsManager: settingsManager, isOnboarding: true)
|
AdditionalModifiersView(settingsManager: settingsManager)
|
||||||
.tag(6)
|
.tag(5)
|
||||||
.tabItem { Image(systemName: "gearshape.fill") }
|
.tabItem { Image(systemName: "slider.horizontal.3") }
|
||||||
|
|
||||||
CompletionView()
|
ScrollView {
|
||||||
.tag(7)
|
GeneralSetupView(settingsManager: settingsManager, isOnboarding: true)
|
||||||
.tabItem { Image(systemName: "checkmark.circle.fill") }
|
|
||||||
|
}.tag(6)
|
||||||
|
.tabItem { Image(systemName: "gearshape.fill") }
|
||||||
|
|
||||||
|
CompletionView()
|
||||||
|
.tag(7)
|
||||||
|
.tabItem { Image(systemName: "checkmark.circle.fill") }
|
||||||
|
}
|
||||||
|
.tabViewStyle(.automatic)
|
||||||
|
.onChange(of: currentPage) { _, newValue in
|
||||||
|
MenuBarGuideOverlayPresenter.shared.updateVisibility(
|
||||||
|
isVisible: newValue == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationButtons(isCompact: isCompact)
|
||||||
}
|
}
|
||||||
.tabViewStyle(.automatic)
|
|
||||||
.onChange(of: currentPage) { _, newValue in
|
|
||||||
MenuBarGuideOverlayPresenter.shared.updateVisibility(isVisible: newValue == 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
navigationButtons
|
|
||||||
}
|
}
|
||||||
|
.environment(\.isCompactLayout, isCompact)
|
||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: 1000,
|
minWidth: AdaptiveLayout.Window.minWidth,
|
||||||
minHeight: {
|
minHeight: AdaptiveLayout.Window.minHeight
|
||||||
#if APPSTORE
|
|
||||||
return 700
|
|
||||||
#else
|
|
||||||
return 1000
|
|
||||||
#endif
|
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
MenuBarGuideOverlayPresenter.shared.updateVisibility(isVisible: currentPage == 1)
|
MenuBarGuideOverlayPresenter.shared.updateVisibility(isVisible: currentPage == 1)
|
||||||
@@ -192,7 +190,7 @@ struct OnboardingContainerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var navigationButtons: some View {
|
private func navigationButtons(isCompact: Bool) -> some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
if currentPage > 0 {
|
if currentPage > 0 {
|
||||||
Button(action: { currentPage -= 1 }) {
|
Button(action: { currentPage -= 1 }) {
|
||||||
@@ -200,8 +198,11 @@ struct OnboardingContainerView: View {
|
|||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
Text("Back")
|
Text("Back")
|
||||||
}
|
}
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
|
.frame(
|
||||||
|
minWidth: 80, maxWidth: .infinity, minHeight: isCompact ? 36 : 44,
|
||||||
|
maxHeight: isCompact ? 36 : 44
|
||||||
|
)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||||
}
|
}
|
||||||
@@ -222,8 +223,11 @@ struct OnboardingContainerView: View {
|
|||||||
? "Let's Get Started"
|
? "Let's Get Started"
|
||||||
: currentPage == lastPageIndex ? "Get Started" : "Continue"
|
: currentPage == lastPageIndex ? "Get Started" : "Continue"
|
||||||
)
|
)
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
|
.frame(
|
||||||
|
minWidth: 80, maxWidth: .infinity, minHeight: isCompact ? 36 : 44,
|
||||||
|
maxHeight: isCompact ? 36 : 44
|
||||||
|
)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||||
}
|
}
|
||||||
@@ -234,8 +238,8 @@ struct OnboardingContainerView: View {
|
|||||||
in: .rect(cornerRadius: 10)
|
in: .rect(cornerRadius: 10)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, isCompact ? 24 : 40)
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, isCompact ? 12 : 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func completeOnboarding() {
|
private func completeOnboarding() {
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ final class SettingsWindowPresenter {
|
|||||||
|
|
||||||
private func createWindow(settingsManager: SettingsManager, initialTab: Int) {
|
private func createWindow(settingsManager: SettingsManager, initialTab: Int) {
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 1000, height: 900),
|
contentRect: NSRect(
|
||||||
|
x: 0, y: 0,
|
||||||
|
width: AdaptiveLayout.Window.defaultWidth,
|
||||||
|
height: AdaptiveLayout.Window.defaultHeight
|
||||||
|
),
|
||||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
@@ -100,52 +104,54 @@ struct SettingsWindowView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
GeometryReader { geometry in
|
||||||
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
let isCompact = geometry.size.height < 600
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
ZStack {
|
||||||
NavigationSplitView {
|
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
||||||
List(SettingsSection.allCases, selection: $selectedSection) { section in
|
.ignoresSafeArea()
|
||||||
NavigationLink(value: section) {
|
|
||||||
Label(section.title, systemImage: section.iconName)
|
VStack(spacing: 0) {
|
||||||
|
NavigationSplitView {
|
||||||
|
List(SettingsSection.allCases, selection: $selectedSection) { section in
|
||||||
|
NavigationLink(value: section) {
|
||||||
|
Label(section.title, systemImage: section.iconName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.sidebar)
|
||||||
|
} detail: {
|
||||||
|
ScrollView {
|
||||||
|
detailView(for: selectedSection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.sidebar)
|
.onReceive(
|
||||||
} detail: {
|
NotificationCenter.default.publisher(
|
||||||
ScrollView {
|
for: Notification.Name("SwitchToSettingsTab"))
|
||||||
detailView(for: selectedSection)
|
) { notification in
|
||||||
}
|
if let tab = notification.object as? Int,
|
||||||
}
|
let section = SettingsSection(rawValue: tab)
|
||||||
.onReceive(
|
{
|
||||||
NotificationCenter.default.publisher(
|
selectedSection = section
|
||||||
for: Notification.Name("SwitchToSettingsTab"))
|
|
||||||
) { notification in
|
|
||||||
if let tab = notification.object as? Int,
|
|
||||||
let section = SettingsSection(rawValue: tab)
|
|
||||||
{
|
|
||||||
selectedSection = section
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Button("Retrigger Onboarding") {
|
|
||||||
retriggerOnboarding()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
#endif
|
#if DEBUG
|
||||||
|
Divider()
|
||||||
|
HStack {
|
||||||
|
Button("Retrigger Onboarding") {
|
||||||
|
retriggerOnboarding()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(isCompact ? .small : .regular)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(isCompact ? 8 : 16)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.environment(\.isCompactLayout, isCompact)
|
||||||
}
|
}
|
||||||
#if APPSTORE
|
.frame(minWidth: AdaptiveLayout.Window.minWidth, minHeight: AdaptiveLayout.Window.minHeight)
|
||||||
.frame(minWidth: 1000, minHeight: 700)
|
|
||||||
#else
|
|
||||||
.frame(minWidth: 1000, minHeight: 900)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@@ -8,73 +8,69 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CompletionView: View {
|
struct CompletionView: View {
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
|
private var iconSize: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Font.heroIconSmall : AdaptiveLayout.Font.heroIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleSize: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Font.heroTitleSmall : AdaptiveLayout.Font.heroTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
private var spacing: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Spacing.compact : AdaptiveLayout.Spacing.standard
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: spacing * 1.5) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 80))
|
.font(.system(size: iconSize))
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
|
|
||||||
Text("You're All Set!")
|
Text("You're All Set!")
|
||||||
.font(.system(size: 36, weight: .bold))
|
.font(.system(size: titleSize, weight: .bold))
|
||||||
|
|
||||||
Text("Gaze will now help you take care of your eyes and posture")
|
Text("Gaze will now help you take care of your eyes and posture")
|
||||||
.font(.title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, isCompact ? 20 : 40)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: isCompact ? 10 : 16) {
|
||||||
Text("What happens next:")
|
Text("What happens next:")
|
||||||
.font(.headline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
completionItem(icon: "menubar.rectangle", text: "Gaze will appear in your menu bar")
|
||||||
Image(systemName: "menubar.rectangle")
|
completionItem(icon: "clock", text: "Timers will start automatically")
|
||||||
.foregroundStyle(Color.accentColor)
|
completionItem(icon: "gearshape", text: "Adjust settings anytime from the menu bar")
|
||||||
.frame(width: 30)
|
completionItem(icon: "plus.circle", text: "Create custom timers in Settings for additional reminders")
|
||||||
Text("Gaze will appear in your menu bar")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
Image(systemName: "clock")
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("Timers will start automatically")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
Image(systemName: "gearshape")
|
|
||||||
.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")
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("Create custom timers in Settings for additional reminders")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 12 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(width: 600, height: 450)
|
.frame(maxWidth: AdaptiveLayout.Content.maxWidth)
|
||||||
.padding()
|
.padding()
|
||||||
.background(.clear)
|
.background(.clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func completionItem(icon: String, text: String) -> some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
.frame(width: 24)
|
||||||
|
Text(text)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Completion View") {
|
#Preview("Completion View") {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct EnforceModeSetupView: View {
|
|||||||
@ObservedObject var cameraService = CameraAccessService.shared
|
@ObservedObject var cameraService = CameraAccessService.shared
|
||||||
@ObservedObject var eyeTrackingService = EyeTrackingService.shared
|
@ObservedObject var eyeTrackingService = EyeTrackingService.shared
|
||||||
@ObservedObject var enforceModeService = EnforceModeService.shared
|
@ObservedObject var enforceModeService = EnforceModeService.shared
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
@State private var isProcessingToggle = false
|
@State private var isProcessingToggle = false
|
||||||
@State private var isTestModeActive = false
|
@State private var isTestModeActive = false
|
||||||
@@ -30,19 +31,19 @@ struct EnforceModeSetupView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: isCompact ? 16 : 30) {
|
||||||
Text("Use your camera to ensure you take breaks")
|
Text("Use your camera to ensure you take breaks")
|
||||||
.font(.title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: isCompact ? 12 : 20) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Enable Enforce Mode")
|
Text("Enable Enforce Mode")
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
Text("Camera activates 3 seconds before lookaway reminders")
|
Text("Camera activates 3 seconds before lookaway reminders")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -65,8 +66,9 @@ struct EnforceModeSetupView: View {
|
|||||||
)
|
)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.disabled(isProcessingToggle)
|
.disabled(isProcessingToggle)
|
||||||
|
.controlSize(isCompact ? .small : .regular)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 10 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
cameraStatusView
|
cameraStatusView
|
||||||
@@ -215,7 +217,7 @@ struct EnforceModeSetupView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 300)
|
.frame(height: isCompact ? 200 : 300)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if cachedPreviewLayer == nil {
|
if cachedPreviewLayer == nil {
|
||||||
|
|||||||
@@ -8,25 +8,40 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MenuBarWelcomeView: View {
|
struct MenuBarWelcomeView: View {
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
|
private var iconSize: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Font.heroIconSmall : AdaptiveLayout.Font.heroIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleSize: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Font.heroTitleSmall : AdaptiveLayout.Font.heroTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
private var spacing: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Spacing.compact : AdaptiveLayout.Spacing.standard
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: spacing * 1.5) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "menubar.rectangle")
|
Image(systemName: "menubar.rectangle")
|
||||||
.font(.system(size: 72))
|
.font(.system(size: iconSize))
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Text("Gaze Lives in Your Menu Bar")
|
Text("Gaze Lives in Your Menu Bar")
|
||||||
.font(.system(size: 34, weight: .bold))
|
.font(.system(size: titleSize, weight: .bold))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Text("Keep an eye on the top-right of your screen for the Gaze icon.")
|
Text("Keep an eye on the top-right of your screen for the Gaze icon.")
|
||||||
.font(.title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: isCompact ? 12 : 16) {
|
||||||
FeatureRow(
|
FeatureRow(
|
||||||
icon: "cursorarrow.click", title: "Always Within Reach",
|
icon: "cursorarrow.click", title: "Always Within Reach",
|
||||||
description: "Open settings and timers from the menu bar anytime")
|
description: "Open settings and timers from the menu bar anytime")
|
||||||
@@ -37,12 +52,12 @@ struct MenuBarWelcomeView: View {
|
|||||||
icon: "sparkles", title: "Quick Tweaks",
|
icon: "sparkles", title: "Quick Tweaks",
|
||||||
description: "Pause, resume, and adjust timers in one click")
|
description: "Pause, resume, and adjust timers in one click")
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 12 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(width: 600, height: 450)
|
.frame(maxWidth: AdaptiveLayout.Content.maxWidth)
|
||||||
.padding()
|
.padding()
|
||||||
.background(.clear)
|
.background(.clear)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,33 +11,34 @@ struct UserTimersView: View {
|
|||||||
@Binding var userTimers: [UserTimer]
|
@Binding var userTimers: [UserTimer]
|
||||||
@State private var editingTimer: UserTimer?
|
@State private var editingTimer: UserTimer?
|
||||||
@State private var showingAddTimer = false
|
@State private var showingAddTimer = false
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: isCompact ? 10 : 16) {
|
||||||
Image(systemName: "clock.badge.checkmark")
|
Image(systemName: "clock.badge.checkmark")
|
||||||
.font(.system(size: 60))
|
.font(.system(size: isCompact ? AdaptiveLayout.Font.heroIconSmall : AdaptiveLayout.Font.heroIcon))
|
||||||
.foregroundStyle(.purple)
|
.foregroundStyle(.purple)
|
||||||
Text("Custom Timers")
|
Text("Custom Timers")
|
||||||
.font(.system(size: 28, weight: .bold))
|
.font(.system(size: isCompact ? AdaptiveLayout.Font.heroTitleSmall : AdaptiveLayout.Font.heroTitle, weight: .bold))
|
||||||
}
|
}
|
||||||
.padding(.top, 20)
|
.padding(.top, isCompact ? 12 : 20)
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, isCompact ? 16 : 30)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: isCompact ? 16 : 30) {
|
||||||
Text("Create your own reminder schedules")
|
Text("Create your own reminder schedules")
|
||||||
.font(.title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
Text("Add up to 3 custom timers with your own intervals and messages")
|
Text("Add up to 3 custom timers with your own intervals and messages")
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 10 : 16)
|
||||||
.glassEffectIfAvailable(
|
.glassEffectIfAvailable(
|
||||||
GlassStyle.regular.tint(.purple), in: .rect(cornerRadius: 8))
|
GlassStyle.regular.tint(.purple), in: .rect(cornerRadius: 8))
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ struct UserTimersView: View {
|
|||||||
// for
|
// for
|
||||||
HStack {
|
HStack {
|
||||||
Text("Active Timers (\(userTimers.count)/3)")
|
Text("Active Timers (\(userTimers.count)/3)")
|
||||||
.font(.headline)
|
.font(isCompact ? .subheadline : .headline)
|
||||||
Spacer()
|
Spacer()
|
||||||
if userTimers.count < 3 {
|
if userTimers.count < 3 {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -56,6 +57,7 @@ struct UserTimersView: View {
|
|||||||
Label("Add Timer", systemImage: "plus.circle.fill")
|
Label("Add Timer", systemImage: "plus.circle.fill")
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(isCompact ? .small : .regular)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*#else*/
|
/*#else*/
|
||||||
@@ -65,7 +67,7 @@ struct UserTimersView: View {
|
|||||||
if userTimers.isEmpty {
|
if userTimers.isEmpty {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: "clock.badge.questionmark")
|
Image(systemName: "clock.badge.questionmark")
|
||||||
.font(.system(size: 40))
|
.font(.system(size: isCompact ? 28 : 40))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("No custom timers yet")
|
Text("No custom timers yet")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
@@ -75,7 +77,7 @@ struct UserTimersView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(40)
|
.padding(isCompact ? 24 : 40)
|
||||||
} else {
|
} else {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
@@ -83,6 +85,7 @@ struct UserTimersView: View {
|
|||||||
index, timer in
|
index, timer in
|
||||||
UserTimerRow(
|
UserTimerRow(
|
||||||
timer: $userTimers[index],
|
timer: $userTimers[index],
|
||||||
|
isCompact: isCompact,
|
||||||
onEdit: {
|
onEdit: {
|
||||||
editingTimer = timer
|
editingTimer = timer
|
||||||
},
|
},
|
||||||
@@ -97,10 +100,10 @@ struct UserTimersView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxHeight: 200)
|
.frame(maxHeight: isCompact ? 150 : 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 10 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -141,51 +144,53 @@ struct UserTimersView: View {
|
|||||||
|
|
||||||
struct UserTimerRow: View {
|
struct UserTimerRow: View {
|
||||||
@Binding var timer: UserTimer
|
@Binding var timer: UserTimer
|
||||||
|
var isCompact: Bool = false
|
||||||
var onEdit: () -> Void
|
var onEdit: () -> Void
|
||||||
var onDelete: () -> Void
|
var onDelete: () -> Void
|
||||||
@State private var isHovered = false
|
@State private var isHovered = false
|
||||||
@State private var showingDeleteConfirmation = false
|
@State private var showingDeleteConfirmation = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: isCompact ? 8 : 12) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(timer.color)
|
.fill(timer.color)
|
||||||
.frame(width: 12, height: 12)
|
.frame(width: isCompact ? 10 : 12, height: isCompact ? 10 : 12)
|
||||||
|
|
||||||
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
Image(systemName: timer.type == .subtle ? "eye.circle" : "rectangle.on.rectangle")
|
||||||
.foregroundStyle(timer.color)
|
.foregroundStyle(timer.color)
|
||||||
.frame(width: 24)
|
.frame(width: isCompact ? 20 : 24)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(timer.title)
|
Text(timer.title)
|
||||||
.font(.subheadline)
|
.font(isCompact ? .caption : .subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Text(
|
Text(
|
||||||
"\(timer.type.displayName) • \(timer.timeOnScreenSeconds)s on screen • \(timer.intervalMinutes) min interval"
|
"\(timer.type.displayName) • \(timer.timeOnScreenSeconds)s on screen • \(timer.intervalMinutes) min interval"
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: isCompact ? 4 : 8) {
|
||||||
Toggle("", isOn: $timer.enabled)
|
Toggle("", isOn: $timer.enabled)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.controlSize(.small)
|
.controlSize(.mini)
|
||||||
|
|
||||||
Button(action: onEdit) {
|
Button(action: onEdit) {
|
||||||
Image(systemName: "pencil.circle.fill")
|
Image(systemName: "pencil.circle.fill")
|
||||||
.font(.title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
Button(action: { showingDeleteConfirmation = true }) {
|
Button(action: { showingDeleteConfirmation = true }) {
|
||||||
Image(systemName: "trash.circle.fill")
|
Image(systemName: "trash.circle.fill")
|
||||||
.font(.title3)
|
.font(isCompact ? .subheadline : .title3)
|
||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -200,7 +205,7 @@ struct UserTimerRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 8 : 12)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.fill(Color.secondary.opacity(isHovered ? 0.1 : 0.05))
|
.fill(Color.secondary.opacity(isHovered ? 0.1 : 0.05))
|
||||||
@@ -251,29 +256,31 @@ struct UserTimerEditSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 20) {
|
||||||
Text(timer == nil ? "Add Custom Timer" : "Edit Custom Timer")
|
Text(timer == nil ? "Add Custom Timer" : "Edit Custom Timer")
|
||||||
.font(.title2)
|
.font(.title3)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Title")
|
Text("Title")
|
||||||
.font(.headline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
TextField("Timer title", text: $title)
|
TextField("Timer title", text: $title)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
Text("Example: \"Stretch Break\", \"Eye Rest\", \"Water Break\"")
|
Text("Example: \"Stretch Break\", \"Eye Rest\", \"Water Break\"")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Color")
|
Text("Color")
|
||||||
.font(.headline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
|
||||||
LazyVGrid(
|
LazyVGrid(
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 8),
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 8),
|
||||||
spacing: 12
|
spacing: 8
|
||||||
) {
|
) {
|
||||||
ForEach(UserTimer.defaultColors, id: \.self) { colorHex in
|
ForEach(UserTimer.defaultColors, id: \.self) { colorHex in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -281,23 +288,23 @@ struct UserTimerEditSheet: View {
|
|||||||
}) {
|
}) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Color(hex: colorHex) ?? .purple)
|
.fill(Color(hex: colorHex) ?? .purple)
|
||||||
.frame(width: 32, height: 32)
|
.frame(width: 28, height: 28)
|
||||||
.overlay(
|
.overlay(
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
Color.white,
|
Color.white,
|
||||||
lineWidth: selectedColorHex == colorHex ? 3 : 0)
|
lineWidth: selectedColorHex == colorHex ? 2 : 0)
|
||||||
)
|
)
|
||||||
.shadow(
|
.shadow(
|
||||||
color: selectedColorHex == colorHex ? .accentColor : .clear,
|
color: selectedColorHex == colorHex ? .accentColor : .clear,
|
||||||
radius: 4)
|
radius: 3)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Picker("Display Type", selection: $type) {
|
Picker("Display Type", selection: $type) {
|
||||||
ForEach(UserTimerType.allCases) { timerType in
|
ForEach(UserTimerType.allCases) { timerType in
|
||||||
Text(timerType.displayName).tag(timerType)
|
Text(timerType.displayName).tag(timerType)
|
||||||
@@ -317,14 +324,15 @@ struct UserTimerEditSheet: View {
|
|||||||
? "Small reminder at top of screen"
|
? "Small reminder at top of screen"
|
||||||
: "Full screen reminder with animation"
|
: "Full screen reminder with animation"
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if type == .overlay {
|
if type == .overlay {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Duration on Screen")
|
Text("Duration on Screen")
|
||||||
.font(.headline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
HStack {
|
HStack {
|
||||||
Slider(
|
Slider(
|
||||||
value: Binding(
|
value: Binding(
|
||||||
@@ -335,15 +343,17 @@ struct UserTimerEditSheet: View {
|
|||||||
step: 1
|
step: 1
|
||||||
)
|
)
|
||||||
Text("\(timeOnScreen)s")
|
Text("\(timeOnScreen)s")
|
||||||
.frame(width: 50, alignment: .trailing)
|
.frame(width: 40, alignment: .trailing)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Interval")
|
Text("Interval")
|
||||||
.font(.headline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
HStack {
|
HStack {
|
||||||
Slider(
|
Slider(
|
||||||
value: Binding(
|
value: Binding(
|
||||||
@@ -354,21 +364,23 @@ struct UserTimerEditSheet: View {
|
|||||||
step: 1
|
step: 1
|
||||||
)
|
)
|
||||||
Text("\(intervalMinutes) min")
|
Text("\(intervalMinutes) min")
|
||||||
.frame(width: 60, alignment: .trailing)
|
.frame(width: 50, alignment: .trailing)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
|
.font(.caption)
|
||||||
}
|
}
|
||||||
Text("How often this reminder will appear (in minutes)")
|
Text("How often this reminder will appear (in minutes)")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Message (Optional)")
|
Text("Message (Optional)")
|
||||||
.font(.headline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
TextField("Enter custom reminder message", text: $message)
|
TextField("Enter custom reminder message", text: $message)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
Text("Leave blank to show a default timer notification")
|
Text("Leave blank to show a default timer notification")
|
||||||
.font(.caption)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,8 +408,8 @@ struct UserTimerEditSheet: View {
|
|||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(20)
|
||||||
.frame(width: 450)
|
.frame(minWidth: 360, idealWidth: 420, maxWidth: 480)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,22 +8,36 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct WelcomeView: View {
|
struct WelcomeView: View {
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
|
private var iconSize: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Font.heroIconSmall : AdaptiveLayout.Font.heroIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleSize: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Font.heroTitleSmall : AdaptiveLayout.Font.heroTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
private var spacing: CGFloat {
|
||||||
|
isCompact ? AdaptiveLayout.Spacing.compact : AdaptiveLayout.Spacing.standard
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: spacing * 1.5) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "eye.fill")
|
Image(systemName: "eye.fill")
|
||||||
.font(.system(size: 80))
|
.font(.system(size: iconSize))
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
|
|
||||||
Text("Welcome to Gaze")
|
Text("Welcome to Gaze")
|
||||||
.font(.system(size: 36, weight: .bold))
|
.font(.system(size: titleSize, weight: .bold))
|
||||||
|
|
||||||
Text("Take care of your eyes and posture")
|
Text("Take care of your eyes and posture")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: isCompact ? 12 : 16) {
|
||||||
FeatureRow(
|
FeatureRow(
|
||||||
icon: "eye.trianglebadge.exclamationmark", title: "Reduce Eye Strain",
|
icon: "eye.trianglebadge.exclamationmark", title: "Reduce Eye Strain",
|
||||||
description: "Regular breaks help prevent digital eye strain")
|
description: "Regular breaks help prevent digital eye strain")
|
||||||
@@ -37,12 +51,12 @@ struct WelcomeView: View {
|
|||||||
icon: "plus.circle", title: "Custom Timers",
|
icon: "plus.circle", title: "Custom Timers",
|
||||||
description: "Create your own timers for specific needs")
|
description: "Create your own timers for specific needs")
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(isCompact ? 12 : 16)
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(width: 600, height: 450)
|
.frame(maxWidth: AdaptiveLayout.Content.maxWidth)
|
||||||
.padding()
|
.padding()
|
||||||
.background(.clear)
|
.background(.clear)
|
||||||
}
|
}
|
||||||
@@ -63,17 +77,18 @@ struct FeatureRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: 16) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.title2)
|
.font(.title3)
|
||||||
.foregroundStyle(iconColor)
|
.foregroundStyle(iconColor)
|
||||||
.frame(width: 30)
|
.frame(width: 24)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.headline)
|
|
||||||
Text(description)
|
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
Text(description)
|
||||||
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user