238 lines
8.3 KiB
Swift
238 lines
8.3 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
struct VisualEffectView: NSViewRepresentable {
|
|
let material: NSVisualEffectView.Material
|
|
let blendingMode: NSVisualEffectView.BlendingMode
|
|
|
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
|
let view = NSVisualEffectView()
|
|
view.material = material
|
|
view.blendingMode = blendingMode
|
|
view.state = .active
|
|
return view
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
|
nsView.material = material
|
|
nsView.blendingMode = blendingMode
|
|
}
|
|
}
|
|
|
|
struct OnboardingContainerView: View {
|
|
@ObservedObject var settingsManager: SettingsManager
|
|
@State private var currentPage = 0
|
|
@State private var lookAwayEnabled = true
|
|
@State private var lookAwayIntervalMinutes = 20
|
|
@State private var lookAwayCountdownSeconds = 20
|
|
@State private var blinkEnabled = true
|
|
@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 {
|
|
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
|
.ignoresSafeArea()
|
|
VStack(spacing: 0) {
|
|
TabView(selection: $currentPage) {
|
|
WelcomeView()
|
|
.tag(0)
|
|
.tabItem {
|
|
Image(systemName: "hand.wave.fill")
|
|
}
|
|
|
|
LookAwaySetupView(
|
|
enabled: $lookAwayEnabled,
|
|
intervalMinutes: $lookAwayIntervalMinutes,
|
|
countdownSeconds: $lookAwayCountdownSeconds
|
|
)
|
|
.tag(1)
|
|
.tabItem {
|
|
Image(systemName: "eye.fill")
|
|
}
|
|
|
|
BlinkSetupView(
|
|
enabled: $blinkEnabled,
|
|
intervalMinutes: $blinkIntervalMinutes
|
|
)
|
|
.tag(2)
|
|
.tabItem {
|
|
Image(systemName: "eye.circle.fill")
|
|
}
|
|
|
|
PostureSetupView(
|
|
enabled: $postureEnabled,
|
|
intervalMinutes: $postureIntervalMinutes
|
|
)
|
|
.tag(3)
|
|
.tabItem {
|
|
Image(systemName: "figure.stand")
|
|
}
|
|
|
|
SettingsOnboardingView(
|
|
launchAtLogin: $launchAtLogin
|
|
)
|
|
.tag(4)
|
|
.tabItem {
|
|
Image(systemName: "gearshape.fill")
|
|
}
|
|
|
|
CompletionView()
|
|
.tag(5)
|
|
.tabItem {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
}
|
|
}
|
|
.tabViewStyle(.automatic)
|
|
|
|
if currentPage >= 0 {
|
|
HStack(spacing: 12) {
|
|
if currentPage > 0 {
|
|
Button(action: { currentPage -= 1 }) {
|
|
HStack {
|
|
Image(systemName: "chevron.left")
|
|
Text("Back")
|
|
}
|
|
.font(.headline)
|
|
.frame(
|
|
minWidth: 100, maxWidth: .infinity, minHeight: 44,
|
|
maxHeight: 44, alignment: .center
|
|
)
|
|
.foregroundColor(.primary)
|
|
}
|
|
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
|
}
|
|
|
|
Button(action: {
|
|
if currentPage == 5 {
|
|
completeOnboarding()
|
|
} else {
|
|
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,
|
|
alignment: .center
|
|
)
|
|
.foregroundColor(.white)
|
|
}
|
|
.glassEffect(
|
|
.regular.tint(currentPage == 5 ? .green : .blue).interactive(),
|
|
in: .rect(cornerRadius: 10))
|
|
}
|
|
.padding(.horizontal, 40)
|
|
.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
|
|
)
|
|
settingsManager.settings.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
|
|
|
settingsManager.settings.blinkTimer = TimerConfiguration(
|
|
enabled: blinkEnabled,
|
|
intervalSeconds: blinkIntervalMinutes * 60
|
|
)
|
|
|
|
settingsManager.settings.postureTimer = TimerConfiguration(
|
|
enabled: postureEnabled,
|
|
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()
|
|
})
|
|
}
|
|
}
|
|
#Preview("Onboarding Container") {
|
|
OnboardingContainerView(settingsManager: SettingsManager.shared)
|
|
}
|