feat: onboarding flow styled
This commit is contained in:
@@ -191,4 +191,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
reminderWindowController?.close()
|
||||
reminderWindowController = nil
|
||||
}
|
||||
|
||||
// Public method to get menubar icon position for animations
|
||||
func getMenuBarIconPosition() -> NSRect? {
|
||||
return statusItem?.button?.window?.frame
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ class SettingsManager: ObservableObject {
|
||||
private let settingsKey = "gazeAppSettings"
|
||||
|
||||
private init() {
|
||||
#if DEBUG
|
||||
// Clear settings on every development build
|
||||
UserDefaults.standard.removeObject(forKey: "gazeAppSettings")
|
||||
#endif
|
||||
self.settings = Self.loadSettings()
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class TimerEngine: ObservableObject {
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let settingsManager: SettingsManager
|
||||
|
||||
nonisolated init(settingsManager: SettingsManager = .shared) {
|
||||
init(settingsManager: SettingsManager) {
|
||||
self.settingsManager = settingsManager
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,39 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Hover button style for menubar items
|
||||
struct MenuBarButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(configuration.isPressed ? Color.blue.opacity(0.2) : Color.gray.opacity(0.1))
|
||||
.opacity(configuration.isPressed ? 1 : 0)
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct MenuBarHoverButtonStyle: ButtonStyle {
|
||||
@State private var isHovered = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(isHovered ? Color.gray.opacity(0.15) : Color.clear)
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onHover { hovering in
|
||||
isHovered = hovering
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.1), value: isHovered)
|
||||
.animation(.easeInOut(duration: 0.05), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct MenuBarContentView: View {
|
||||
@ObservedObject var timerEngine: TimerEngine
|
||||
@ObservedObject var settingsManager: SettingsManager
|
||||
@@ -54,7 +87,7 @@ struct MenuBarContentView: View {
|
||||
}
|
||||
|
||||
// Controls
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: 4) {
|
||||
Button(action: {
|
||||
if timerEngine.timerStates.values.first?.isPaused == true {
|
||||
timerEngine.resume()
|
||||
@@ -67,10 +100,10 @@ struct MenuBarContentView: View {
|
||||
Text(isPaused ? "Resume All Timers" : "Pause All Timers")
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal)
|
||||
.buttonStyle(MenuBarHoverButtonStyle())
|
||||
|
||||
Button(action: {
|
||||
// TODO: Open settings window
|
||||
@@ -80,12 +113,13 @@ struct MenuBarContentView: View {
|
||||
Text("Settings...")
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal)
|
||||
.buttonStyle(MenuBarHoverButtonStyle())
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -93,13 +127,16 @@ struct MenuBarContentView: View {
|
||||
Button(action: onQuit) {
|
||||
HStack {
|
||||
Image(systemName: "power")
|
||||
.foregroundColor(.red)
|
||||
Text("Quit Gaze")
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding()
|
||||
.buttonStyle(MenuBarHoverButtonStyle())
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.frame(width: 300)
|
||||
}
|
||||
@@ -113,6 +150,7 @@ struct TimerStatusRow: View {
|
||||
let type: TimerType
|
||||
let state: TimerState
|
||||
var onSkip: () -> Void
|
||||
@State private var isHovered = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -136,9 +174,17 @@ struct TimerStatusRow: View {
|
||||
Image(systemName: "forward.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
.padding(6)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(isHovered ? Color.blue.opacity(0.1) : Color.clear)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Skip to next \(type.displayName) reminder")
|
||||
.onHover { hovering in
|
||||
isHovered = hovering
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 4)
|
||||
@@ -170,9 +216,11 @@ struct TimerStatusRow: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let settingsManager = SettingsManager.shared
|
||||
let timerEngine = TimerEngine(settingsManager: settingsManager)
|
||||
MenuBarContentView(
|
||||
timerEngine: TimerEngine(settingsManager: .shared),
|
||||
settingsManager: .shared,
|
||||
timerEngine: timerEngine,
|
||||
settingsManager: settingsManager,
|
||||
onQuit: {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct BlinkSetupView: View {
|
||||
@Binding var enabled: Bool
|
||||
@Binding var intervalMinutes: Int
|
||||
var onContinue: () -> Void
|
||||
var onBack: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
@@ -49,27 +50,41 @@ struct BlinkSetupView: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.glassEffect(in: .rect(cornerRadius: 12))
|
||||
|
||||
InfoBox(text: "We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes")
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let onBack = onBack {
|
||||
Button(action: onBack) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive())
|
||||
}
|
||||
|
||||
Button(action: onContinue) {
|
||||
Text("Continue")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.blue).interactive())
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +92,7 @@ struct BlinkSetupView: View {
|
||||
BlinkSetupView(
|
||||
enabled: .constant(true),
|
||||
intervalMinutes: .constant(5),
|
||||
onContinue: {}
|
||||
onContinue: {},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct CompletionView: View {
|
||||
var onComplete: () -> Void
|
||||
var onBack: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
@@ -60,28 +61,42 @@ struct CompletionView: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.glassEffect(in: .rect(cornerRadius: 12))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let onBack = onBack {
|
||||
Button(action: onBack) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive())
|
||||
}
|
||||
|
||||
Button(action: onComplete) {
|
||||
Text("Get Started")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.green)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.green).interactive())
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompletionView(onComplete: {})
|
||||
CompletionView(onComplete: {}, onBack: {})
|
||||
}
|
||||
|
||||
@@ -66,27 +66,41 @@ struct LookAwaySetupView: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.glassEffect(in: .rect(cornerRadius: 12))
|
||||
|
||||
InfoBox(text: "Every 20 minutes, look at something 20 feet away for 20 seconds to reduce eye strain")
|
||||
InfoBox(text: "Every \(intervalMinutes) minutes, look in the distance for \(countdownSeconds) seconds to reduce eye strain")
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let onBack = onBack {
|
||||
Button(action: onBack) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive())
|
||||
}
|
||||
|
||||
Button(action: onContinue) {
|
||||
Text("Continue")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.blue).interactive())
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +116,7 @@ struct InfoBox: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.glassEffect(.regular.tint(.blue), in: .rect(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +125,7 @@ struct InfoBox: View {
|
||||
enabled: .constant(true),
|
||||
intervalMinutes: .constant(20),
|
||||
countdownSeconds: .constant(20),
|
||||
onContinue: {}
|
||||
onContinue: {},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
// NSVisualEffectView wrapper for SwiftUI
|
||||
struct VisualEffectView: NSViewRepresentable {
|
||||
let material: NSVisualEffectView.Material
|
||||
let blendingMode: NSVisualEffectView.BlendingMode
|
||||
|
||||
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||
let view = NSVisualEffectView()
|
||||
view.material = material
|
||||
view.blendingMode = blendingMode
|
||||
view.state = .active
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
||||
nsView.material = material
|
||||
nsView.blendingMode = blendingMode
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingContainerView: View {
|
||||
@ObservedObject var settingsManager: SettingsManager
|
||||
@@ -17,54 +37,90 @@ struct OnboardingContainerView: View {
|
||||
@State private var blinkIntervalMinutes = 5
|
||||
@State private var postureEnabled = true
|
||||
@State private var postureIntervalMinutes = 30
|
||||
@State private var launchAtLogin = false
|
||||
@State private var isAnimatingOut = false
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Semi-transparent background with blur
|
||||
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
TabView(selection: $currentPage) {
|
||||
WelcomeView(onContinue: { currentPage = 1 })
|
||||
WelcomeView(
|
||||
onContinue: { currentPage = 1 }
|
||||
)
|
||||
.tag(0)
|
||||
.tabItem {
|
||||
Image(systemName: "hand.wave.fill")
|
||||
}
|
||||
|
||||
LookAwaySetupView(
|
||||
enabled: $lookAwayEnabled,
|
||||
intervalMinutes: $lookAwayIntervalMinutes,
|
||||
countdownSeconds: $lookAwayCountdownSeconds,
|
||||
onContinue: { currentPage = 2 }
|
||||
onContinue: { currentPage = 2 },
|
||||
onBack: { currentPage = 0 }
|
||||
)
|
||||
.tag(1)
|
||||
.tabItem {
|
||||
Image(systemName: "eye.fill")
|
||||
}
|
||||
|
||||
BlinkSetupView(
|
||||
enabled: $blinkEnabled,
|
||||
intervalMinutes: $blinkIntervalMinutes,
|
||||
onContinue: { currentPage = 3 }
|
||||
onContinue: { currentPage = 3 },
|
||||
onBack: { currentPage = 1 }
|
||||
)
|
||||
.tag(2)
|
||||
.tabItem {
|
||||
Image(systemName: "eye.circle.fill")
|
||||
}
|
||||
|
||||
PostureSetupView(
|
||||
enabled: $postureEnabled,
|
||||
intervalMinutes: $postureIntervalMinutes,
|
||||
onContinue: { currentPage = 4 }
|
||||
onContinue: { currentPage = 4 },
|
||||
onBack: { currentPage = 2 }
|
||||
)
|
||||
.tag(3)
|
||||
.tabItem {
|
||||
Image(systemName: "figure.stand")
|
||||
}
|
||||
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: $launchAtLogin,
|
||||
onContinue: { currentPage = 5 },
|
||||
onBack: { currentPage = 3 }
|
||||
)
|
||||
.tag(4)
|
||||
.tabItem {
|
||||
Image(systemName: "gearshape.fill")
|
||||
}
|
||||
|
||||
CompletionView(
|
||||
onComplete: {
|
||||
completeOnboarding()
|
||||
}
|
||||
},
|
||||
onBack: { currentPage = 4 }
|
||||
)
|
||||
.tag(4)
|
||||
.tag(5)
|
||||
.tabItem {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.automatic)
|
||||
|
||||
// Page indicator
|
||||
Text("\(currentPage + 1)/5")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
.opacity(isAnimatingOut ? 0 : 1)
|
||||
.scaleEffect(isAnimatingOut ? 0.3 : 1.0)
|
||||
}
|
||||
|
||||
private func completeOnboarding() {
|
||||
// Save settings
|
||||
settingsManager.settings.lookAwayTimer = TimerConfiguration(
|
||||
enabled: lookAwayEnabled,
|
||||
intervalSeconds: lookAwayIntervalMinutes * 60
|
||||
@@ -81,7 +137,73 @@ struct OnboardingContainerView: View {
|
||||
intervalSeconds: postureIntervalMinutes * 60
|
||||
)
|
||||
|
||||
settingsManager.settings.launchAtLogin = launchAtLogin
|
||||
settingsManager.settings.hasCompletedOnboarding = true
|
||||
|
||||
// Apply launch at login setting
|
||||
do {
|
||||
if launchAtLogin {
|
||||
try LaunchAtLoginManager.enable()
|
||||
} else {
|
||||
try LaunchAtLoginManager.disable()
|
||||
}
|
||||
} catch {
|
||||
print("Failed to set launch at login: \(error)")
|
||||
}
|
||||
|
||||
// Perform vacuum animation
|
||||
performVacuumAnimation()
|
||||
}
|
||||
|
||||
private func performVacuumAnimation() {
|
||||
// Get the NSWindow reference
|
||||
guard let window = NSApplication.shared.windows.first(where: { $0.isVisible && $0.contentView != nil }) else {
|
||||
// Fallback: just dismiss without animation
|
||||
dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
// Get menubar icon position from AppDelegate
|
||||
let appDelegate = NSApplication.shared.delegate as? AppDelegate
|
||||
let targetFrame = appDelegate?.getMenuBarIconPosition()
|
||||
|
||||
// Calculate target position (menubar icon or top-center as fallback)
|
||||
let targetRect: NSRect
|
||||
if let menuBarFrame = targetFrame {
|
||||
// Use menubar icon position
|
||||
targetRect = NSRect(
|
||||
x: menuBarFrame.midX,
|
||||
y: menuBarFrame.midY,
|
||||
width: 0,
|
||||
height: 0
|
||||
)
|
||||
} else {
|
||||
// Fallback to top-center of screen
|
||||
let screen = NSScreen.main?.frame ?? .zero
|
||||
targetRect = NSRect(
|
||||
x: screen.midX,
|
||||
y: screen.maxY,
|
||||
width: 0,
|
||||
height: 0
|
||||
)
|
||||
}
|
||||
|
||||
// Start SwiftUI animation for visual effects
|
||||
withAnimation(.easeInOut(duration: 0.7)) {
|
||||
isAnimatingOut = true
|
||||
}
|
||||
|
||||
// Animate window frame using AppKit
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0.7
|
||||
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
||||
window.animator().setFrame(targetRect, display: true)
|
||||
window.animator().alphaValue = 0
|
||||
}, completionHandler: {
|
||||
// Close window after animation completes
|
||||
self.dismiss()
|
||||
window.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ struct PostureSetupView: View {
|
||||
@Binding var enabled: Bool
|
||||
@Binding var intervalMinutes: Int
|
||||
var onContinue: () -> Void
|
||||
var onBack: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
@@ -49,27 +50,41 @@ struct PostureSetupView: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.glassEffect(in: .rect(cornerRadius: 12))
|
||||
|
||||
InfoBox(text: "Regular posture checks help prevent back and neck pain from prolonged sitting")
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let onBack = onBack {
|
||||
Button(action: onBack) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive())
|
||||
}
|
||||
|
||||
Button(action: onContinue) {
|
||||
Text("Continue")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.blue).interactive())
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +92,7 @@ struct PostureSetupView: View {
|
||||
PostureSetupView(
|
||||
enabled: .constant(true),
|
||||
intervalMinutes: .constant(30),
|
||||
onContinue: {}
|
||||
onContinue: {},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
|
||||
170
Gaze/Views/Onboarding/SettingsOnboardingView.swift
Normal file
170
Gaze/Views/Onboarding/SettingsOnboardingView.swift
Normal file
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// SettingsOnboardingView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsOnboardingView: View {
|
||||
@Binding var launchAtLogin: Bool
|
||||
var onContinue: () -> Void
|
||||
var onBack: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "gearshape.fill")
|
||||
.font(.system(size: 80))
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Text("Final Settings")
|
||||
.font(.system(size: 36, weight: .bold))
|
||||
|
||||
Text("Configure app preferences and support the project")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
// Launch at Login Toggle
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Launch at Login")
|
||||
.font(.headline)
|
||||
Text("Start Gaze automatically when you log in")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $launchAtLogin)
|
||||
.labelsHidden()
|
||||
.onChange(of: launchAtLogin) { oldValue, newValue in
|
||||
applyLaunchAtLoginSetting(enabled: newValue)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.glassEffect(in: .rect(cornerRadius: 12))
|
||||
|
||||
// Links Section
|
||||
VStack(spacing: 12) {
|
||||
Text("Support & Contribute")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
// GitHub Link
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/mikefreno/Gaze") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left.forwardslash.chevron.right")
|
||||
.font(.title3)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("View on GitHub")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
Text("Star the repo, report issues, contribute")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||
|
||||
// Buy Me a Coffee
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://buymeacoffee.com/placeholder") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "cup.and.saucer.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.orange)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Buy Me a Coffee")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
Text("Support development of Gaze")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.orange.opacity(0.1))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let onBack = onBack {
|
||||
Button(action: onBack) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive())
|
||||
}
|
||||
|
||||
Button(action: onContinue) {
|
||||
Text("Continue")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.blue).interactive())
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
|
||||
private func applyLaunchAtLoginSetting(enabled: Bool) {
|
||||
do {
|
||||
if enabled {
|
||||
try LaunchAtLoginManager.enable()
|
||||
} else {
|
||||
try LaunchAtLoginManager.disable()
|
||||
}
|
||||
} catch {
|
||||
print("Failed to set launch at login: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsOnboardingView(
|
||||
launchAtLogin: .constant(false),
|
||||
onContinue: {},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ struct WelcomeView: View {
|
||||
FeatureRow(icon: "figure.stand", title: "Maintain Good Posture", description: "Gentle reminders to sit up straight")
|
||||
}
|
||||
.padding()
|
||||
.glassEffect(in: .rect(cornerRadius: 16))
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -39,15 +40,14 @@ struct WelcomeView: View {
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.tint(.blue).interactive())
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user