vOOice/VoiceInk/Views/Onboarding/OnboardingTutorialView.swift

205 lines
9.6 KiB
Swift

import SwiftUI
import KeyboardShortcuts
struct OnboardingTutorialView: View {
@Binding var hasCompletedOnboarding: Bool
@EnvironmentObject private var hotkeyManager: HotkeyManager
@EnvironmentObject private var whisperState: WhisperState
@State private var scale: CGFloat = 0.8
@State private var opacity: CGFloat = 0
@State private var transcribedText: String = ""
@State private var isTextFieldFocused: Bool = false
@State private var showingShortcutHint: Bool = true
@FocusState private var isFocused: Bool
var body: some View {
GeometryReader { geometry in
ZStack {
// Reusable background
OnboardingBackgroundView()
HStack(spacing: 0) {
// Left side - Tutorial instructions
VStack(alignment: .leading, spacing: 40) {
// Title and description
VStack(alignment: .leading, spacing: 16) {
Text("Try It Out!")
.font(.system(size: 44, weight: .bold, design: .rounded))
.foregroundColor(.white)
Text("Let's test your VoiceInk setup.")
.font(.system(size: 24, weight: .medium, design: .rounded))
.foregroundColor(.white.opacity(0.7))
.lineSpacing(4)
}
// Keyboard shortcut display
VStack(alignment: .leading, spacing: 20) {
HStack {
Text("Your Shortcut")
.font(.system(size: 28, weight: .semibold, design: .rounded))
.foregroundColor(.white)
}
if hotkeyManager.selectedHotkey1 == .custom,
let shortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) {
KeyboardShortcutView(shortcut: shortcut)
.scaleEffect(1.2)
} else if hotkeyManager.selectedHotkey1 != .none && hotkeyManager.selectedHotkey1 != .custom {
Text(hotkeyManager.selectedHotkey1.displayName)
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundColor(.accentColor)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
}
}
// Instructions
VStack(alignment: .leading, spacing: 24) {
ForEach(1...4, id: \.self) { step in
instructionStep(number: step, text: getInstructionText(for: step))
}
}
Spacer()
// Continue button
Button(action: {
hasCompletedOnboarding = true
}) {
Text("Complete Setup")
.font(.system(size: 18, weight: .semibold, design: .rounded))
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.accentColor)
.cornerRadius(25)
}
.buttonStyle(ScaleButtonStyle())
.opacity(transcribedText.isEmpty ? 0.5 : 1)
.disabled(transcribedText.isEmpty)
SkipButton(text: "Skip for now") {
hasCompletedOnboarding = true
}
}
.padding(60)
.frame(width: geometry.size.width * 0.5)
// Right side - Interactive area
VStack {
// Magical text editor area
ZStack {
// Glowing background
RoundedRectangle(cornerRadius: 20)
.fill(Color.black.opacity(0.4))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.white.opacity(0.1), lineWidth: 1)
)
.overlay(
// Subtle gradient overlay
LinearGradient(
colors: [
Color.accentColor.opacity(0.05),
Color.black.opacity(0.1)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.shadow(color: Color.accentColor.opacity(0.1), radius: 15, x: 0, y: 0)
// Text editor with custom styling
TextEditor(text: $transcribedText)
.font(.system(size: 32, weight: .bold, design: .rounded))
.focused($isFocused)
.scrollContentBackground(.hidden)
.background(Color.clear)
.foregroundColor(.white)
.padding(20)
// Placeholder text with magical appearance
if transcribedText.isEmpty {
VStack(spacing: 16) {
Image(systemName: "wand.and.stars")
.font(.system(size: 36))
.foregroundColor(.white.opacity(0.3))
Text("Click here and start speaking...")
.font(.system(size: 28, weight: .semibold, design: .rounded))
.foregroundColor(.white.opacity(0.5))
.multilineTextAlignment(.center)
}
.padding()
.allowsHitTesting(false)
}
// Subtle animated border
RoundedRectangle(cornerRadius: 20)
.strokeBorder(
LinearGradient(
colors: [
Color.accentColor.opacity(isFocused ? 0.4 : 0.1),
Color.accentColor.opacity(isFocused ? 0.2 : 0.05)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
.animation(.easeInOut(duration: 0.3), value: isFocused)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(60)
.frame(width: geometry.size.width * 0.5)
}
}
}
.onAppear {
hotkeyManager.startHotkeyMonitoring()
animateIn()
isFocused = true
}
}
private func getInstructionText(for step: Int) -> String {
switch step {
case 1: return "Click the text area on the right"
case 2: return "Press your shortcut key"
case 3: return "Speak something"
case 4: return "Press your shortcut key again"
default: return ""
}
}
private func instructionStep(number: Int, text: String) -> some View {
HStack(spacing: 20) {
Text("\(number)")
.font(.system(size: 20, weight: .bold, design: .rounded))
.foregroundColor(.white)
.frame(width: 40, height: 40)
.background(Circle().fill(Color.accentColor.opacity(0.2)))
.overlay(
Circle()
.stroke(Color.accentColor.opacity(0.3), lineWidth: 1)
)
Text(text)
.font(.system(size: 18, weight: .medium, design: .rounded))
.foregroundColor(.white.opacity(0.9))
.lineSpacing(4)
}
}
private func animateIn() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.7)) {
scale = 1
opacity = 1
}
}
}