// // LockScreenView.swift // Feels // // Lock screen shown when privacy lock is enabled and app needs authentication. // Design: "Emotional Aurora" - A sanctuary for your feelings // import SwiftUI // MARK: - Floating Mood Particle struct MoodParticle: Identifiable { let id = UUID() var x: CGFloat var y: CGFloat let size: CGFloat let color: Color let duration: Double let delay: Double } // MARK: - Aurora Gradient Background struct AuroraBackground: View { @State private var animateGradient = false private let moodColors: [Color] = [ Color(hex: "31d158"), // great - green Color(hex: "ffd709"), // good - yellow Color(hex: "0b84ff"), // average - blue Color(hex: "ff9e0b"), // bad - orange Color(hex: "ff453a"), // horrible - red ] var body: some View { ZStack { // Base dark gradient LinearGradient( colors: [ Color(hex: "0a0a0f"), Color(hex: "12121a"), Color(hex: "0d0d14") ], startPoint: .topLeading, endPoint: .bottomTrailing ) // Aurora layer 1 - green/blue EllipticalGradient( colors: [ moodColors[0].opacity(0.3), moodColors[2].opacity(0.15), .clear ], center: .topLeading, startRadiusFraction: 0, endRadiusFraction: 0.8 ) .blur(radius: 60) .offset(x: animateGradient ? 20 : -20, y: animateGradient ? -30 : 30) // Aurora layer 2 - yellow/orange EllipticalGradient( colors: [ moodColors[1].opacity(0.2), moodColors[3].opacity(0.1), .clear ], center: .bottomTrailing, startRadiusFraction: 0, endRadiusFraction: 0.7 ) .blur(radius: 80) .offset(x: animateGradient ? -30 : 30, y: animateGradient ? 20 : -20) // Aurora layer 3 - subtle red accent RadialGradient( colors: [ moodColors[4].opacity(0.15), .clear ], center: UnitPoint(x: 0.8, y: 0.3), startRadius: 0, endRadius: 200 ) .blur(radius: 40) .offset(y: animateGradient ? -10 : 10) // Noise texture overlay Rectangle() .fill(.white.opacity(0.015)) .background( Canvas { context, size in for _ in 0..<1000 { let x = CGFloat.random(in: 0...size.width) let y = CGFloat.random(in: 0...size.height) let opacity = Double.random(in: 0.01...0.04) context.fill( Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)), with: .color(.white.opacity(opacity)) ) } } ) } .ignoresSafeArea() .onAppear { withAnimation( .easeInOut(duration: 8) .repeatForever(autoreverses: true) ) { animateGradient = true } } } } // MARK: - Floating Particles Layer struct FloatingParticlesView: View { @State private var particles: [MoodParticle] = [] private let moodColors: [Color] = [ Color(hex: "31d158"), Color(hex: "ffd709"), Color(hex: "0b84ff"), Color(hex: "ff9e0b"), Color(hex: "ff453a"), ] var body: some View { GeometryReader { geo in ZStack { ForEach(particles) { particle in Circle() .fill(particle.color) .frame(width: particle.size, height: particle.size) .blur(radius: particle.size * 0.3) .position(x: particle.x, y: particle.y) .modifier(FloatingAnimation( startY: particle.y, duration: particle.duration, delay: particle.delay )) } } .onAppear { generateParticles(in: geo.size) } } } private func generateParticles(in size: CGSize) { particles = (0..<15).map { _ in MoodParticle( x: CGFloat.random(in: 0...size.width), y: CGFloat.random(in: 0...size.height), size: CGFloat.random(in: 4...12), color: moodColors.randomElement()!.opacity(Double.random(in: 0.2...0.5)), duration: Double.random(in: 6...12), delay: Double.random(in: 0...3) ) } } } struct FloatingAnimation: ViewModifier { let startY: CGFloat let duration: Double let delay: Double @State private var offset: CGFloat = 0 func body(content: Content) -> some View { content .offset(y: offset) .onAppear { withAnimation( .easeInOut(duration: duration) .repeatForever(autoreverses: true) .delay(delay) ) { offset = CGFloat.random(in: -30...30) } } } } // MARK: - Central Breathing Orb struct BreathingOrb: View { @State private var breathe = false @State private var rotate = false private let moodColors: [Color] = [ Color(hex: "31d158"), Color(hex: "ffd709"), Color(hex: "0b84ff"), Color(hex: "ff9e0b"), Color(hex: "ff453a"), ] var body: some View { ZStack { // Outer glow Circle() .fill( AngularGradient( colors: moodColors + [moodColors[0]], center: .center, startAngle: .degrees(0), endAngle: .degrees(360) ) ) .frame(width: 180, height: 180) .blur(radius: 40) .opacity(0.6) .scaleEffect(breathe ? 1.2 : 0.9) .rotationEffect(.degrees(rotate ? 360 : 0)) // Middle ring Circle() .fill( AngularGradient( colors: moodColors.reversed() + [moodColors.last!], center: .center ) ) .frame(width: 120, height: 120) .blur(radius: 20) .opacity(0.8) .scaleEffect(breathe ? 1.1 : 0.95) .rotationEffect(.degrees(rotate ? -360 : 0)) // Inner core Circle() .fill( RadialGradient( colors: [ .white.opacity(0.9), .white.opacity(0.3), .clear ], center: .center, startRadius: 0, endRadius: 40 ) ) .frame(width: 80, height: 80) .scaleEffect(breathe ? 1.05 : 0.98) // Glossy highlight Ellipse() .fill( LinearGradient( colors: [.white.opacity(0.4), .clear], startPoint: .top, endPoint: .center ) ) .frame(width: 50, height: 30) .offset(y: -15) .scaleEffect(breathe ? 1.05 : 0.98) } .onAppear { withAnimation( .easeInOut(duration: 4) .repeatForever(autoreverses: true) ) { breathe = true } withAnimation( .linear(duration: 20) .repeatForever(autoreverses: false) ) { rotate = true } } } } // MARK: - Glassmorphic Button struct GlassButton: View { let icon: String let title: String let action: () -> Void @State private var isPressed = false @State private var pulse = false var body: some View { Button(action: action) { HStack(spacing: 14) { ZStack { // Pulse ring Circle() .stroke(lineWidth: 2) .foregroundColor(.white.opacity(0.3)) .frame(width: 44, height: 44) .scaleEffect(pulse ? 1.3 : 1) .opacity(pulse ? 0 : 0.6) // Icon background Circle() .fill(.white.opacity(0.15)) .frame(width: 44, height: 44) Image(systemName: icon) .font(.system(size: 20, weight: .medium)) .foregroundColor(.white) } Text(title) .font(.system(size: 17, weight: .semibold, design: .rounded)) .foregroundColor(.white) } .frame(maxWidth: .infinity) .padding(.vertical, 18) .padding(.horizontal, 24) .background( ZStack { // Glass effect RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .opacity(0.8) // Border gradient RoundedRectangle(cornerRadius: 20) .stroke( LinearGradient( colors: [ .white.opacity(0.4), .white.opacity(0.1), .white.opacity(0.2) ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) // Inner shadow RoundedRectangle(cornerRadius: 20) .fill( LinearGradient( colors: [.white.opacity(0.1), .clear], startPoint: .top, endPoint: .bottom ) ) } ) .scaleEffect(isPressed ? 0.97 : 1) } .buttonStyle(PlainButtonStyle()) .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in withAnimation(.easeInOut(duration: 0.1)) { isPressed = true } } .onEnded { _ in withAnimation(.easeInOut(duration: 0.1)) { isPressed = false } } ) .onAppear { withAnimation( .easeInOut(duration: 2) .repeatForever(autoreverses: false) ) { pulse = true } } } } // MARK: - Main Lock Screen View struct LockScreenView: View { @ObservedObject var authManager: BiometricAuthManager @State private var showError = false @State private var showContent = false var body: some View { ZStack { // Aurora background AuroraBackground() // Floating particles FloatingParticlesView() // Main content VStack(spacing: 0) { Spacer() // Breathing orb BreathingOrb() .opacity(showContent ? 1 : 0) .scaleEffect(showContent ? 1 : 0.8) Spacer() .frame(height: 50) // Text content VStack(spacing: 12) { Text("Your Feelings") .font(.system(size: 32, weight: .light, design: .serif)) .foregroundColor(.white) Text("are safe here") .font(.system(size: 32, weight: .ultraLight, design: .serif)) .foregroundColor(.white.opacity(0.7)) } .opacity(showContent ? 1 : 0) .offset(y: showContent ? 0 : 20) Spacer() .frame(height: 16) Text("Authenticate to continue your journey") .font(.system(size: 14, weight: .regular, design: .rounded)) .foregroundColor(.white.opacity(0.5)) .opacity(showContent ? 1 : 0) Spacer() // Unlock button GlassButton( icon: authManager.biometricIcon, title: "Unlock with \(authManager.biometricName)" ) { Task { let success = await authManager.authenticate() if !success { showError = true } } } .disabled(authManager.isAuthenticating) .opacity(showContent ? 1 : 0) .offset(y: showContent ? 0 : 30) .padding(.horizontal, 32) // Passcode hint if authManager.canUseDevicePasscode { Text("Or use your device passcode") .font(.system(size: 12, weight: .regular, design: .rounded)) .foregroundColor(.white.opacity(0.35)) .padding(.top, 16) .opacity(showContent ? 1 : 0) } Spacer() .frame(height: 50) } .padding() } .alert("Authentication Failed", isPresented: $showError) { Button("Try Again") { Task { await authManager.authenticate() } } Button("Cancel", role: .cancel) { } } message: { Text("Unable to verify your identity. Please try again.") } .onAppear { // Animate content in withAnimation(.easeOut(duration: 0.8).delay(0.2)) { showContent = true } // Auto-trigger authentication if !authManager.isUnlocked && !authManager.isAuthenticating { Task { try? await Task.sleep(for: .milliseconds(800)) await authManager.authenticate() } } } } } // MARK: - Preview #Preview("Lock Screen - Face ID") { LockScreenView(authManager: BiometricAuthManager()) .preferredColorScheme(.dark) } #Preview("Lock Screen - Touch ID") { LockScreenView(authManager: BiometricAuthManager()) .preferredColorScheme(.dark) }