diff --git a/Shared/Views/LockScreenView.swift b/Shared/Views/LockScreenView.swift index 4f7441d..eda0faa 100644 --- a/Shared/Views/LockScreenView.swift +++ b/Shared/Views/LockScreenView.swift @@ -3,83 +3,461 @@ // 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 { - // Background gradient - LinearGradient( - colors: [ - Color(.systemBackground), - Color(.systemGray6) - ], - startPoint: .top, - endPoint: .bottom - ) - .ignoresSafeArea() + // Aurora background + AuroraBackground() - VStack(spacing: 40) { + // Floating particles + FloatingParticlesView() + + // Main content + VStack(spacing: 0) { Spacer() - // App icon / lock icon - VStack(spacing: 20) { - Image(systemName: "lock.fill") - .font(.largeTitle) - .foregroundStyle(.secondary) + // Breathing orb + BreathingOrb() + .opacity(showContent ? 1 : 0) + .scaleEffect(showContent ? 1 : 0.8) - Text("Feels is Locked") - .font(.title2) - .fontWeight(.semibold) + Spacer() + .frame(height: 50) - Text("Authenticate to access your mood data") - .font(.subheadline) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) + // 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 - Button { + GlassButton( + icon: authManager.biometricIcon, + title: "Unlock with \(authManager.biometricName)" + ) { Task { let success = await authManager.authenticate() if !success { showError = true } } - } label: { - HStack(spacing: 12) { - Image(systemName: authManager.biometricIcon) - .font(.title2) - - Text("Unlock with \(authManager.biometricName)") - .fontWeight(.medium) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - .background(Color.accentColor) - .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 14)) } .disabled(authManager.isAuthenticating) - .padding(.horizontal, 40) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 30) + .padding(.horizontal, 32) - // Passcode fallback hint + // Passcode hint if authManager.canUseDevicePasscode { Text("Or use your device passcode") - .font(.caption) - .foregroundStyle(.tertiary) + .font(.system(size: 12, weight: .regular, design: .rounded)) + .foregroundColor(.white.opacity(0.35)) + .padding(.top, 16) + .opacity(showContent ? 1 : 0) } Spacer() - .frame(height: 60) + .frame(height: 50) } .padding() } @@ -94,12 +472,30 @@ struct LockScreenView: View { Text("Unable to verify your identity. Please try again.") } .onAppear { - // Auto-trigger authentication on appear + // 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) +}