- Wrap 30+ production print() statements in #if DEBUG guards across 18 files - Add VoiceOver labels, hints, and traits to Watch app, Live Activities, widgets - Add .accessibilityAddTraits(.isButton) to 15+ onTapGesture views - Add text alternatives for color-only indicators (progress dots, mood circles) - Localize raw string literals in NoteEditorView, EntryDetailView, widgets - Replace 25+ silent try? with do/catch + AppLogger error logging - Replace hardcoded font sizes with semantic Dynamic Type fonts - Fix FIXME in IconPickerView (log icon change errors) - Extract magic animation delays to named constants across 8 files - Add widget empty state "Log your first mood!" messaging - Hide decorative images from VoiceOver, add labels to ColorPickers - Remove stale TODO in Color+Codable (alpha change deferred for migration) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2053 lines
66 KiB
Swift
2053 lines
66 KiB
Swift
//
|
|
// LockScreenView.swift
|
|
// Reflect
|
|
//
|
|
// Lock screen shown when privacy lock is enabled and app needs authentication.
|
|
// Supports multiple themed styles that match app themes.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
// MARK: - Lock Screen Style Protocol
|
|
|
|
protocol LockScreenTheme {
|
|
var backgroundColor: AnyView { get }
|
|
var centralElement: AnyView { get }
|
|
var titleText: String { get }
|
|
var subtitleText: String { get }
|
|
var taglineText: String { get }
|
|
var titleFont: Font { get }
|
|
var subtitleFont: Font { get }
|
|
var taglineFont: Font { get }
|
|
func titleColor(isDark: Bool) -> Color
|
|
func subtitleColor(isDark: Bool) -> Color
|
|
func taglineColor(isDark: Bool) -> Color
|
|
func buttonStyle(isDark: Bool) -> LockButtonStyle
|
|
}
|
|
|
|
struct LockButtonStyle {
|
|
let backgroundColor: Color
|
|
let foregroundColor: Color
|
|
let borderColor: Color
|
|
let useMaterial: Bool
|
|
}
|
|
|
|
// 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 {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@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
|
|
]
|
|
|
|
private var isDark: Bool { colorScheme == .dark }
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Base gradient - adapts to color scheme
|
|
LinearGradient(
|
|
colors: isDark ? [
|
|
Color(hex: "0a0a0f"),
|
|
Color(hex: "12121a"),
|
|
Color(hex: "0d0d14")
|
|
] : [
|
|
Color(hex: "f8f9fa"),
|
|
Color(hex: "e9ecef"),
|
|
Color(hex: "f1f3f5")
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
|
|
// Aurora layer 1 - green/blue
|
|
EllipticalGradient(
|
|
colors: [
|
|
moodColors[0].opacity(isDark ? 0.3 : 0.2),
|
|
moodColors[2].opacity(isDark ? 0.15 : 0.1),
|
|
.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(isDark ? 0.2 : 0.15),
|
|
moodColors[3].opacity(isDark ? 0.1 : 0.08),
|
|
.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(isDark ? 0.15 : 0.1),
|
|
.clear
|
|
],
|
|
center: UnitPoint(x: 0.8, y: 0.3),
|
|
startRadius: 0,
|
|
endRadius: 200
|
|
)
|
|
.blur(radius: 40)
|
|
.offset(y: animateGradient ? -10 : 10)
|
|
|
|
}
|
|
.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 {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@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"),
|
|
]
|
|
|
|
private var isDark: Bool { colorScheme == .dark }
|
|
|
|
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(isDark ? 0.6 : 0.5)
|
|
.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(isDark ? 0.8 : 0.6)
|
|
.scaleEffect(breathe ? 1.1 : 0.95)
|
|
.rotationEffect(.degrees(rotate ? -360 : 0))
|
|
|
|
// Inner core
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: isDark ? [
|
|
.white.opacity(0.9),
|
|
.white.opacity(0.3),
|
|
.clear
|
|
] : [
|
|
.white,
|
|
.white.opacity(0.6),
|
|
.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 40
|
|
)
|
|
)
|
|
.frame(width: 80, height: 80)
|
|
.scaleEffect(breathe ? 1.05 : 0.98)
|
|
.shadow(color: .black.opacity(isDark ? 0 : 0.1), radius: 10)
|
|
|
|
// Glossy highlight
|
|
Ellipse()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [.white.opacity(isDark ? 0.4 : 0.8), .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: - Zen Lock Screen Theme
|
|
|
|
struct ZenLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var breathe = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm paper background
|
|
LinearGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.12, green: 0.11, blue: 0.10),
|
|
Color(red: 0.08, green: 0.07, blue: 0.06)
|
|
] : [
|
|
Color(red: 0.96, green: 0.94, blue: 0.90),
|
|
Color(red: 0.92, green: 0.90, blue: 0.86)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Subtle ink wash effect
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.4, green: 0.45, blue: 0.4).opacity(0.15),
|
|
.clear
|
|
] : [
|
|
Color(red: 0.3, green: 0.35, blue: 0.3).opacity(0.08),
|
|
.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 300
|
|
)
|
|
)
|
|
.scaleEffect(breathe ? 1.1 : 1.0)
|
|
.blur(radius: 80)
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 6).repeatForever(autoreverses: true)) {
|
|
breathe = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
breathe = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ZenEnsoOrb: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var drawProgress: CGFloat = 0
|
|
@State private var breathe = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Outer glow
|
|
Circle()
|
|
.stroke(
|
|
Color(red: 0.35, green: 0.4, blue: 0.35).opacity(colorScheme == .dark ? 0.3 : 0.15),
|
|
lineWidth: 4
|
|
)
|
|
.frame(width: 140, height: 140)
|
|
.blur(radius: 15)
|
|
.scaleEffect(breathe ? 1.1 : 0.95)
|
|
|
|
// Enso circle - incomplete for zen aesthetics
|
|
Circle()
|
|
.trim(from: 0, to: 0.85)
|
|
.stroke(
|
|
Color(red: 0.3, green: 0.35, blue: 0.3),
|
|
style: StrokeStyle(lineWidth: 5, lineCap: .round)
|
|
)
|
|
.frame(width: 100, height: 100)
|
|
.rotationEffect(.degrees(-90))
|
|
.scaleEffect(breathe ? 1.02 : 0.98)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) {
|
|
breathe = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
breathe = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Neon Lock Screen Theme
|
|
|
|
struct NeonLockBackground: View {
|
|
@State private var pulse = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep black base
|
|
Color(red: 0.02, green: 0.02, blue: 0.05)
|
|
|
|
// Grid lines
|
|
Canvas { context, size in
|
|
let spacing: CGFloat = 30
|
|
for y in stride(from: 0, to: size.height, by: spacing) {
|
|
var path = Path()
|
|
path.move(to: CGPoint(x: 0, y: y))
|
|
path.addLine(to: CGPoint(x: size.width, y: y))
|
|
context.stroke(path, with: .color(Color.cyan.opacity(0.08)), lineWidth: 0.5)
|
|
}
|
|
for x in stride(from: 0, to: size.width, by: spacing) {
|
|
var path = Path()
|
|
path.move(to: CGPoint(x: x, y: 0))
|
|
path.addLine(to: CGPoint(x: x, y: size.height))
|
|
context.stroke(path, with: .color(Color.cyan.opacity(0.08)), lineWidth: 0.5)
|
|
}
|
|
}
|
|
|
|
// Neon glow spots
|
|
Circle()
|
|
.fill(Color(red: 0, green: 1, blue: 0.82).opacity(pulse ? 0.3 : 0.15))
|
|
.frame(width: 300, height: 300)
|
|
.blur(radius: 80)
|
|
.offset(y: -100)
|
|
|
|
Circle()
|
|
.fill(Color(red: 1, green: 0, blue: 0.8).opacity(pulse ? 0.2 : 0.1))
|
|
.frame(width: 250, height: 250)
|
|
.blur(radius: 70)
|
|
.offset(x: 50, y: 150)
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
pulse = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pulse = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NeonRingOrb: View {
|
|
@State private var rotate = false
|
|
@State private var pulse = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Outer glow ring
|
|
Circle()
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 4
|
|
)
|
|
.frame(width: 140, height: 140)
|
|
.blur(radius: 10)
|
|
.opacity(pulse ? 0.9 : 0.5)
|
|
.rotationEffect(.degrees(rotate ? 360 : 0))
|
|
|
|
// Inner ring
|
|
Circle()
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 3
|
|
)
|
|
.frame(width: 100, height: 100)
|
|
.shadow(color: Color(red: 0, green: 1, blue: 0.82).opacity(0.6), radius: pulse ? 20 : 10)
|
|
|
|
// Center core
|
|
Circle()
|
|
.fill(Color.white)
|
|
.frame(width: 30, height: 30)
|
|
.shadow(color: .white.opacity(0.8), radius: 15)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 10).repeatForever(autoreverses: false)) {
|
|
rotate = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
|
pulse = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
rotate = false
|
|
pulse = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Celestial Lock Screen Theme
|
|
|
|
struct CelestialLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var twinkle = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep space gradient
|
|
LinearGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.05, green: 0.05, blue: 0.12),
|
|
Color(red: 0.08, green: 0.06, blue: 0.15)
|
|
] : [
|
|
Color(red: 0.95, green: 0.94, blue: 0.98),
|
|
Color(red: 0.92, green: 0.9, blue: 0.96)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Star field (dark mode only)
|
|
if colorScheme == .dark {
|
|
Canvas { context, size in
|
|
for _ in 0..<50 {
|
|
let x = CGFloat.random(in: 0...size.width)
|
|
let y = CGFloat.random(in: 0...size.height)
|
|
let starSize = CGFloat.random(in: 1...3)
|
|
context.fill(
|
|
Path(ellipseIn: CGRect(x: x, y: y, width: starSize, height: starSize)),
|
|
with: .color(.white.opacity(Double.random(in: 0.3...0.8)))
|
|
)
|
|
}
|
|
}
|
|
.opacity(twinkle ? 0.8 : 1.0)
|
|
}
|
|
|
|
// Nebula glow
|
|
Circle()
|
|
.fill(Color(red: 1.0, green: 0.4, blue: 0.5).opacity(colorScheme == .dark ? 0.2 : 0.1))
|
|
.frame(width: 300, height: 300)
|
|
.blur(radius: 80)
|
|
.offset(y: -50)
|
|
|
|
Circle()
|
|
.fill(Color(red: 0.6, green: 0.4, blue: 0.9).opacity(colorScheme == .dark ? 0.15 : 0.08))
|
|
.frame(width: 250, height: 250)
|
|
.blur(radius: 60)
|
|
.offset(x: 80, y: 100)
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
twinkle = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
twinkle = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CelestialOrbsElement: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var float = false
|
|
@State private var rotate = false
|
|
|
|
private let orbColors: [Color] = [
|
|
Color(red: 1.0, green: 0.8, blue: 0.3), // Gold
|
|
Color(red: 1.0, green: 0.5, blue: 0.5), // Coral
|
|
Color(red: 0.6, green: 0.5, blue: 0.9) // Lavender
|
|
]
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Orbit ring
|
|
Circle()
|
|
.stroke(Color.white.opacity(colorScheme == .dark ? 0.15 : 0.2), lineWidth: 1)
|
|
.frame(width: 140, height: 140)
|
|
.rotationEffect(.degrees(rotate ? 360 : 0))
|
|
|
|
// Orbiting orbs
|
|
ForEach(0..<3, id: \.self) { i in
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [orbColors[i], orbColors[i].opacity(0.6)],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 20
|
|
)
|
|
)
|
|
.frame(width: 28, height: 28)
|
|
.shadow(color: orbColors[i].opacity(0.6), radius: 10)
|
|
.offset(y: -70)
|
|
.rotationEffect(.degrees(Double(i) * 120 + (rotate ? 360 : 0)))
|
|
}
|
|
|
|
// Center star
|
|
Image(systemName: "sparkle")
|
|
.font(.system(size: 40, weight: .light))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color(red: 1.0, green: 0.9, blue: 0.7), Color(red: 1.0, green: 0.7, blue: 0.6)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 1.0, green: 0.8, blue: 0.5).opacity(0.5), radius: 15)
|
|
.scaleEffect(float ? 1.1 : 0.95)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 20).repeatForever(autoreverses: false)) {
|
|
rotate = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
float = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
rotate = false
|
|
float = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Editorial Lock Screen Theme
|
|
|
|
struct EditorialLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Solid elegant background
|
|
Color(colorScheme == .dark ? .black : .white)
|
|
|
|
// Subtle texture lines
|
|
VStack(spacing: 0) {
|
|
ForEach(0..<20, id: \.self) { _ in
|
|
Rectangle()
|
|
.fill(Color.primary.opacity(0.02))
|
|
.frame(height: 1)
|
|
Spacer()
|
|
}
|
|
}
|
|
.padding(.horizontal, 40)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct EditorialFrameElement: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var appear = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Elegant frame
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.stroke(Color.primary.opacity(0.3), lineWidth: 1)
|
|
.frame(width: 120, height: 150)
|
|
|
|
// Inner accent line
|
|
VStack {
|
|
Rectangle()
|
|
.fill(Color.primary)
|
|
.frame(width: 40, height: 2)
|
|
Spacer()
|
|
Rectangle()
|
|
.fill(Color.primary)
|
|
.frame(width: 40, height: 2)
|
|
}
|
|
.frame(height: 130)
|
|
.opacity(appear ? 1 : 0)
|
|
|
|
// Center diamond
|
|
Image(systemName: "diamond")
|
|
.font(.system(size: 28, weight: .ultraLight))
|
|
.foregroundColor(.primary)
|
|
.opacity(appear ? 1 : 0.5)
|
|
.scaleEffect(appear ? 1 : 0.9)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeOut(duration: 1)) {
|
|
appear = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Mixtape Lock Screen Theme
|
|
|
|
struct MixtapeLockBackground: View {
|
|
@State private var shift = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm retro gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.45, blue: 0.35),
|
|
Color(red: 0.95, green: 0.65, blue: 0.25)
|
|
],
|
|
startPoint: shift ? .topLeading : .topTrailing,
|
|
endPoint: shift ? .bottomTrailing : .bottomLeading
|
|
)
|
|
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) {
|
|
shift = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
shift = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CassetteElement: View {
|
|
@State private var spin = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Cassette body
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color.black.opacity(0.85))
|
|
.frame(width: 140, height: 90)
|
|
.shadow(color: .black.opacity(0.3), radius: 10)
|
|
|
|
// Label area
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.white.opacity(0.9))
|
|
.frame(width: 100, height: 30)
|
|
.offset(y: -15)
|
|
|
|
// Reels
|
|
HStack(spacing: 40) {
|
|
Circle()
|
|
.fill(Color.white.opacity(0.8))
|
|
.frame(width: 30, height: 30)
|
|
.overlay(
|
|
Circle()
|
|
.fill(Color.black.opacity(0.6))
|
|
.frame(width: 10, height: 10)
|
|
)
|
|
.rotationEffect(.degrees(spin ? 360 : 0))
|
|
|
|
Circle()
|
|
.fill(Color.white.opacity(0.8))
|
|
.frame(width: 30, height: 30)
|
|
.overlay(
|
|
Circle()
|
|
.fill(Color.black.opacity(0.6))
|
|
.frame(width: 10, height: 10)
|
|
)
|
|
.rotationEffect(.degrees(spin ? 360 : 0))
|
|
}
|
|
.offset(y: 18)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
|
|
spin = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
spin = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Bloom Lock Screen Theme
|
|
|
|
struct BloomLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var bloom = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Garden gradient
|
|
LinearGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.05, green: 0.12, blue: 0.08),
|
|
Color(red: 0.08, green: 0.18, blue: 0.1)
|
|
] : [
|
|
Color(red: 0.95, green: 0.98, blue: 0.95),
|
|
Color(red: 0.9, green: 0.96, blue: 0.92)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Soft glows
|
|
Circle()
|
|
.fill(Color(red: 0.3, green: 0.7, blue: 0.4).opacity(colorScheme == .dark ? 0.2 : 0.1))
|
|
.frame(width: 300, height: 300)
|
|
.blur(radius: 80)
|
|
.offset(y: bloom ? -20 : 20)
|
|
|
|
Circle()
|
|
.fill(Color(red: 1.0, green: 0.6, blue: 0.7).opacity(colorScheme == .dark ? 0.15 : 0.08))
|
|
.frame(width: 200, height: 200)
|
|
.blur(radius: 60)
|
|
.offset(x: -50, y: bloom ? 80 : 120)
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 6).repeatForever(autoreverses: true)) {
|
|
bloom = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
bloom = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FlowerElement: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var bloom = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Petals
|
|
ForEach(0..<6, id: \.self) { i in
|
|
Ellipse()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.6, blue: 0.7),
|
|
Color(red: 1.0, green: 0.5, blue: 0.6)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.frame(width: 30, height: bloom ? 60 : 45)
|
|
.offset(y: bloom ? -45 : -35)
|
|
.rotationEffect(.degrees(Double(i) * 60))
|
|
.shadow(color: Color(red: 1.0, green: 0.5, blue: 0.6).opacity(0.3), radius: 8)
|
|
}
|
|
|
|
// Center
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.9, blue: 0.6),
|
|
Color(red: 1.0, green: 0.85, blue: 0.4)
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 25
|
|
)
|
|
)
|
|
.frame(width: 40, height: 40)
|
|
.shadow(color: Color(red: 1.0, green: 0.9, blue: 0.6).opacity(0.5), radius: 10)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
bloom = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
bloom = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Heartfelt Lock Screen Theme
|
|
|
|
struct HeartfeltLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var pulse = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Soft pink gradient
|
|
LinearGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.15, green: 0.08, blue: 0.1),
|
|
Color(red: 0.1, green: 0.05, blue: 0.08)
|
|
] : [
|
|
Color(red: 1.0, green: 0.95, blue: 0.96),
|
|
Color(red: 0.98, green: 0.92, blue: 0.94)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Heart glow
|
|
Circle()
|
|
.fill(Color(red: 0.9, green: 0.45, blue: 0.55).opacity(colorScheme == .dark ? 0.2 : 0.1))
|
|
.frame(width: 300, height: 300)
|
|
.blur(radius: 80)
|
|
.scaleEffect(pulse ? 1.1 : 0.95)
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
|
pulse = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pulse = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HeartElement: View {
|
|
@State private var beat = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Glow
|
|
Image(systemName: "heart.fill")
|
|
.font(.system(size: 80))
|
|
.foregroundColor(Color(red: 0.9, green: 0.45, blue: 0.55))
|
|
.blur(radius: 20)
|
|
.scaleEffect(beat ? 1.15 : 0.9)
|
|
|
|
// Main heart
|
|
Image(systemName: "heart.fill")
|
|
.font(.system(size: 70))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.55, blue: 0.6),
|
|
Color(red: 0.85, green: 0.35, blue: 0.45)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.9, green: 0.45, blue: 0.55).opacity(0.4), radius: 15)
|
|
.scaleEffect(beat ? 1.08 : 0.95)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
|
|
beat = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
beat = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Minimal Lock Screen Theme
|
|
|
|
struct MinimalLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Clean gradient
|
|
LinearGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.1, green: 0.1, blue: 0.1),
|
|
Color(red: 0.08, green: 0.08, blue: 0.08)
|
|
] : [
|
|
Color(red: 0.98, green: 0.96, blue: 0.94),
|
|
Color(red: 0.95, green: 0.93, blue: 0.9)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Subtle warm accent
|
|
Circle()
|
|
.fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(colorScheme == .dark ? 0.08 : 0.05))
|
|
.frame(width: 400, height: 400)
|
|
.blur(radius: 100)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct MinimalCircleElement: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var breathe = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Outer ring
|
|
Circle()
|
|
.stroke(Color.primary.opacity(0.15), lineWidth: 1)
|
|
.frame(width: breathe ? 130 : 120, height: breathe ? 130 : 120)
|
|
|
|
// Middle ring
|
|
Circle()
|
|
.stroke(Color.primary.opacity(0.25), lineWidth: 1)
|
|
.frame(width: breathe ? 90 : 85, height: breathe ? 90 : 85)
|
|
|
|
// Inner circle
|
|
Circle()
|
|
.fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(colorScheme == .dark ? 0.6 : 0.4))
|
|
.frame(width: 50, height: 50)
|
|
.scaleEffect(breathe ? 1.05 : 0.95)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
breathe = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
breathe = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Luxe Lock Screen Theme
|
|
|
|
struct LuxeLockBackground: View {
|
|
@State private var shimmer = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Rich dark background
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.1, green: 0.08, blue: 0.06),
|
|
Color(red: 0.06, green: 0.04, blue: 0.02)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Gold shimmer
|
|
LinearGradient(
|
|
colors: [
|
|
.clear,
|
|
Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.1),
|
|
.clear
|
|
],
|
|
startPoint: shimmer ? .topLeading : .bottomTrailing,
|
|
endPoint: shimmer ? .bottomTrailing : .topLeading
|
|
)
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
shimmer = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
shimmer = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DiamondElement: View {
|
|
@State private var rotate = false
|
|
@State private var shimmer = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Glow
|
|
Image(systemName: "diamond.fill")
|
|
.font(.system(size: 70))
|
|
.foregroundColor(Color(red: 0.85, green: 0.7, blue: 0.45))
|
|
.blur(radius: 25)
|
|
.opacity(shimmer ? 0.6 : 0.3)
|
|
|
|
// Diamond
|
|
Image(systemName: "diamond.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.85, blue: 0.6),
|
|
Color(red: 0.75, green: 0.6, blue: 0.35),
|
|
Color(red: 0.55, green: 0.45, blue: 0.25)
|
|
],
|
|
startPoint: shimmer ? .topLeading : .bottomTrailing,
|
|
endPoint: shimmer ? .bottomTrailing : .topLeading
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.5), radius: 20)
|
|
.rotationEffect(.degrees(rotate ? 5 : -5))
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
rotate = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
shimmer = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
rotate = false
|
|
shimmer = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Forecast Lock Screen Theme
|
|
|
|
struct ForecastLockBackground: View {
|
|
@State private var drift = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Sky gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.55, green: 0.75, blue: 0.95),
|
|
Color(red: 0.4, green: 0.6, blue: 0.85),
|
|
Color(red: 0.3, green: 0.5, blue: 0.75)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Floating clouds
|
|
ForEach(0..<4, id: \.self) { i in
|
|
Image(systemName: "cloud.fill")
|
|
.font(.system(size: CGFloat.random(in: 40...80)))
|
|
.foregroundColor(.white.opacity(Double.random(in: 0.3...0.6)))
|
|
.offset(
|
|
x: CGFloat(i * 100 - 150) + (drift ? 20 : -20),
|
|
y: CGFloat(i * 80 - 200)
|
|
)
|
|
.blur(radius: CGFloat.random(in: 2...5))
|
|
}
|
|
}
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 8).repeatForever(autoreverses: true)) {
|
|
drift = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
drift = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WeatherElement: View {
|
|
@State private var shine = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Sun rays
|
|
ForEach(0..<8, id: \.self) { i in
|
|
Rectangle()
|
|
.fill(Color(red: 1.0, green: 0.9, blue: 0.5))
|
|
.frame(width: 3, height: 30)
|
|
.offset(y: -60)
|
|
.rotationEffect(.degrees(Double(i) * 45))
|
|
.opacity(shine ? 0.8 : 0.4)
|
|
}
|
|
|
|
// Sun glow
|
|
Circle()
|
|
.fill(Color(red: 1.0, green: 0.9, blue: 0.5).opacity(0.4))
|
|
.frame(width: 100, height: 100)
|
|
.blur(radius: 25)
|
|
|
|
// Sun
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.95, blue: 0.7),
|
|
Color(red: 1.0, green: 0.85, blue: 0.4)
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 35
|
|
)
|
|
)
|
|
.frame(width: 70, height: 70)
|
|
.shadow(color: Color(red: 1.0, green: 0.85, blue: 0.4).opacity(0.6), radius: 20)
|
|
|
|
// Cloud accent
|
|
Image(systemName: "cloud.fill")
|
|
.font(.system(size: 35))
|
|
.foregroundColor(.white)
|
|
.offset(x: 40, y: 25)
|
|
.shadow(color: .black.opacity(0.1), radius: 5)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
shine = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
shine = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Playful Lock Screen Theme
|
|
|
|
struct PlayfulLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm cream gradient
|
|
LinearGradient(
|
|
colors: colorScheme == .dark ? [
|
|
Color(red: 0.15, green: 0.12, blue: 0.1),
|
|
Color(red: 0.1, green: 0.08, blue: 0.06)
|
|
] : [
|
|
Color(red: 1.0, green: 0.98, blue: 0.95),
|
|
Color(red: 0.98, green: 0.96, blue: 0.92)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Colorful accents
|
|
Circle()
|
|
.fill(Color(red: 0.95, green: 0.55, blue: 0.35).opacity(colorScheme == .dark ? 0.15 : 0.08))
|
|
.frame(width: 200, height: 200)
|
|
.blur(radius: 60)
|
|
.offset(x: -80, y: -150)
|
|
|
|
Circle()
|
|
.fill(Color(red: 0.95, green: 0.75, blue: 0.35).opacity(colorScheme == .dark ? 0.12 : 0.06))
|
|
.frame(width: 180, height: 180)
|
|
.blur(radius: 50)
|
|
.offset(x: 100, y: 150)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct PlayfulEmojiElement: View {
|
|
@State private var bounce = false
|
|
@State private var wiggle = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Background circle
|
|
Circle()
|
|
.fill(Color(red: 0.95, green: 0.55, blue: 0.35).opacity(0.15))
|
|
.frame(width: 140, height: 140)
|
|
.scaleEffect(bounce ? 1.05 : 0.98)
|
|
|
|
// Main emoji
|
|
Text("😊")
|
|
.font(.system(size: 80))
|
|
.rotationEffect(.degrees(wiggle ? 8 : -8))
|
|
.scaleEffect(bounce ? 1.1 : 0.95)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) {
|
|
wiggle = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true)) {
|
|
bounce = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
wiggle = false
|
|
bounce = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Journal Lock Screen Theme
|
|
|
|
struct JournalLockBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm paper background
|
|
Color(colorScheme == .dark ?
|
|
Color(red: 0.12, green: 0.1, blue: 0.08) :
|
|
Color(red: 0.95, green: 0.92, blue: 0.88)
|
|
)
|
|
|
|
// Paper texture lines
|
|
VStack(spacing: 28) {
|
|
ForEach(0..<25, id: \.self) { _ in
|
|
Rectangle()
|
|
.fill(Color.primary.opacity(colorScheme == .dark ? 0.08 : 0.06))
|
|
.frame(height: 1)
|
|
}
|
|
}
|
|
.padding(.horizontal, 50)
|
|
|
|
// Margin line
|
|
Rectangle()
|
|
.fill(Color(red: 0.85, green: 0.55, blue: 0.55).opacity(colorScheme == .dark ? 0.3 : 0.2))
|
|
.frame(width: 1)
|
|
.offset(x: -120)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct JournalBookElement: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var open = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Book shadow
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color.black.opacity(0.2))
|
|
.frame(width: 110, height: 140)
|
|
.offset(x: 5, y: 5)
|
|
.blur(radius: 8)
|
|
|
|
// Book cover
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.55, green: 0.45, blue: 0.35),
|
|
Color(red: 0.45, green: 0.35, blue: 0.28)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.frame(width: 100, height: 130)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(Color(red: 0.65, green: 0.55, blue: 0.45), lineWidth: 2)
|
|
)
|
|
|
|
// Spine
|
|
Rectangle()
|
|
.fill(Color(red: 0.4, green: 0.32, blue: 0.25))
|
|
.frame(width: 8, height: 130)
|
|
.offset(x: -46)
|
|
|
|
// Title area
|
|
VStack(spacing: 8) {
|
|
Rectangle()
|
|
.fill(Color(red: 0.95, green: 0.9, blue: 0.82))
|
|
.frame(width: 60, height: 30)
|
|
.cornerRadius(2)
|
|
|
|
Image(systemName: "heart.fill")
|
|
.font(.system(size: 20))
|
|
.foregroundColor(Color(red: 0.85, green: 0.55, blue: 0.55))
|
|
}
|
|
.scaleEffect(open ? 1.02 : 1.0)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
open = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
open = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Glassmorphic Button
|
|
|
|
struct GlassButton: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let icon: String
|
|
let title: String
|
|
let action: () -> Void
|
|
|
|
@State private var isPressed = false
|
|
@State private var pulse = false
|
|
|
|
private var isDark: Bool { colorScheme == .dark }
|
|
private var foregroundColor: Color { isDark ? .white : .primary }
|
|
private var accentOpacity: Double { isDark ? 0.15 : 0.1 }
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 14) {
|
|
ZStack {
|
|
// Pulse ring
|
|
Circle()
|
|
.stroke(lineWidth: 2)
|
|
.foregroundColor(foregroundColor.opacity(0.3))
|
|
.frame(width: 44, height: 44)
|
|
.scaleEffect(pulse ? 1.3 : 1)
|
|
.opacity(pulse ? 0 : 0.6)
|
|
|
|
// Icon background
|
|
Circle()
|
|
.fill(foregroundColor.opacity(accentOpacity))
|
|
.frame(width: 44, height: 44)
|
|
|
|
Image(systemName: icon)
|
|
.font(.system(size: 20, weight: .medium))
|
|
.foregroundColor(foregroundColor)
|
|
}
|
|
|
|
Text(title)
|
|
.font(.system(size: 17, weight: .semibold, design: .rounded))
|
|
.foregroundColor(foregroundColor)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 18)
|
|
.padding(.horizontal, 24)
|
|
.background(
|
|
ZStack {
|
|
// Glass effect
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.fill(.ultraThinMaterial)
|
|
|
|
// Border gradient
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [
|
|
foregroundColor.opacity(0.3),
|
|
foregroundColor.opacity(0.1),
|
|
foregroundColor.opacity(0.15)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 1
|
|
)
|
|
|
|
// Inner highlight
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [foregroundColor.opacity(0.08), .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 {
|
|
private enum AnimationConstants {
|
|
static let contentAppearDuration: TimeInterval = 0.8
|
|
static let contentAppearDelay: TimeInterval = 0.2
|
|
static let authenticationDelay: Int = 800 // milliseconds
|
|
}
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@ObservedObject var authManager: BiometricAuthManager
|
|
@State private var showError = false
|
|
@State private var showContent = false
|
|
|
|
// Read from AppStorage to match current theme, with optional override for previews
|
|
@AppStorage(UserDefaultsStore.Keys.lockScreenStyle.rawValue, store: GroupUserDefaults.groupDefaults)
|
|
private var lockScreenStyleRaw: Int = 0
|
|
|
|
var style: LockScreenStyle?
|
|
|
|
private var lockScreenStyle: LockScreenStyle {
|
|
if let override = style {
|
|
return override
|
|
}
|
|
return LockScreenStyle(rawValue: lockScreenStyleRaw) ?? .aurora
|
|
}
|
|
|
|
private var isDark: Bool { colorScheme == .dark }
|
|
|
|
// Style-dependent properties
|
|
private var primaryTextColor: Color {
|
|
switch lockScreenStyle {
|
|
case .neon:
|
|
return Color(red: 0, green: 1, blue: 0.82)
|
|
case .editorial:
|
|
return isDark ? .white : .black
|
|
case .mixtape:
|
|
return .white
|
|
case .luxe:
|
|
return Color(red: 0.95, green: 0.85, blue: 0.6)
|
|
case .forecast:
|
|
return .white
|
|
default:
|
|
return isDark ? .white : .primary
|
|
}
|
|
}
|
|
|
|
private var secondaryTextColor: Color {
|
|
switch lockScreenStyle {
|
|
case .neon:
|
|
return Color(red: 1, green: 0, blue: 0.8)
|
|
case .editorial:
|
|
return isDark ? .white.opacity(0.7) : .black.opacity(0.7)
|
|
case .mixtape:
|
|
return .white.opacity(0.9)
|
|
case .luxe:
|
|
return Color(red: 0.75, green: 0.6, blue: 0.35)
|
|
case .forecast:
|
|
return .white.opacity(0.9)
|
|
default:
|
|
return isDark ? .white.opacity(0.7) : .secondary
|
|
}
|
|
}
|
|
|
|
private var tertiaryTextColor: Color {
|
|
switch lockScreenStyle {
|
|
case .neon:
|
|
return Color.white.opacity(0.6)
|
|
case .editorial:
|
|
return isDark ? .white.opacity(0.5) : .black.opacity(0.5)
|
|
case .mixtape:
|
|
return .white.opacity(0.7)
|
|
case .luxe:
|
|
return Color(red: 0.65, green: 0.55, blue: 0.4).opacity(0.8)
|
|
case .forecast:
|
|
return .white.opacity(0.7)
|
|
default:
|
|
return isDark ? .white.opacity(0.5) : .secondary.opacity(0.8)
|
|
}
|
|
}
|
|
|
|
private var titleFont: Font {
|
|
switch lockScreenStyle {
|
|
case .neon:
|
|
return .system(size: 28, weight: .black, design: .monospaced)
|
|
case .editorial:
|
|
return .system(size: 30, weight: .ultraLight, design: .serif)
|
|
case .mixtape:
|
|
return .system(size: 28, weight: .black, design: .rounded)
|
|
case .zen:
|
|
return .system(size: 30, weight: .thin, design: .serif)
|
|
case .luxe:
|
|
return .system(size: 28, weight: .light, design: .serif)
|
|
case .playful:
|
|
return .system(size: 28, weight: .bold, design: .rounded)
|
|
case .journal:
|
|
return .system(size: 26, weight: .medium, design: .serif)
|
|
default:
|
|
return .system(size: 32, weight: .light, design: .serif)
|
|
}
|
|
}
|
|
|
|
private var titleText: String {
|
|
switch lockScreenStyle {
|
|
case .neon: return "UNLOCK YOUR"
|
|
case .editorial: return "Your Story"
|
|
case .mixtape: return "PRESS PLAY"
|
|
case .zen: return "Find Your"
|
|
case .heartfelt: return "Feel With"
|
|
case .luxe: return "Your Sanctuary"
|
|
case .forecast: return "Your Forecast"
|
|
case .playful: return "Hey There!"
|
|
case .journal: return "Your Journal"
|
|
case .bloom: return "Time to"
|
|
case .celestial: return "Your Feelings"
|
|
case .minimal: return "Simply"
|
|
default: return "Your Feelings"
|
|
}
|
|
}
|
|
|
|
private var subtitleText: String {
|
|
switch lockScreenStyle {
|
|
case .neon: return "FULL SIGNAL"
|
|
case .editorial: return "Awaits"
|
|
case .mixtape: return "ON YOUR MOODS"
|
|
case .zen: return "Inner Peace"
|
|
case .heartfelt: return "All Your Heart"
|
|
case .luxe: return "Awaits"
|
|
case .forecast: return "Is Ready"
|
|
case .playful: return "Let's Check In!"
|
|
case .journal: return "Is Private"
|
|
case .bloom: return "Bloom"
|
|
case .celestial: return "are safe here"
|
|
case .minimal: return "Know Yourself"
|
|
default: return "are safe here"
|
|
}
|
|
}
|
|
|
|
private var taglineText: String {
|
|
switch lockScreenStyle {
|
|
case .neon: return "Authenticate to sync your vibes"
|
|
case .editorial: return "Authenticate to continue"
|
|
case .mixtape: return "Authenticate to spin your tracks"
|
|
case .zen: return "Authenticate to begin your practice"
|
|
case .heartfelt: return "Authenticate to open your heart"
|
|
case .luxe: return "Authenticate for exclusive access"
|
|
case .forecast: return "Authenticate to check the weather"
|
|
case .playful: return "Authenticate to start the fun!"
|
|
case .journal: return "Authenticate to continue writing"
|
|
case .bloom: return "Authenticate to tend your garden"
|
|
case .celestial: return "Authenticate to explore the cosmos"
|
|
case .minimal: return "Authenticate to continue"
|
|
default: return "Authenticate to continue your journey"
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Themed background
|
|
backgroundView
|
|
.accessibilityHidden(true)
|
|
|
|
// Floating particles (Aurora only)
|
|
if lockScreenStyle == .aurora {
|
|
FloatingParticlesView()
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
// Main content
|
|
VStack(spacing: 0) {
|
|
Spacer()
|
|
|
|
// Central element
|
|
centralElement
|
|
.opacity(showContent ? 1 : 0)
|
|
.scaleEffect(showContent ? 1 : 0.8)
|
|
.accessibilityHidden(true)
|
|
|
|
Spacer()
|
|
.frame(height: 50)
|
|
|
|
// Text content
|
|
VStack(spacing: 12) {
|
|
Text(titleText)
|
|
.font(titleFont)
|
|
.foregroundColor(primaryTextColor)
|
|
.tracking(lockScreenStyle == .neon || lockScreenStyle == .mixtape ? 2 : 0)
|
|
|
|
Text(subtitleText)
|
|
.font(lockScreenStyle == .neon ? .system(size: 24, weight: .bold, design: .monospaced) : titleFont)
|
|
.foregroundColor(secondaryTextColor)
|
|
.tracking(lockScreenStyle == .neon || lockScreenStyle == .mixtape ? 2 : 0)
|
|
}
|
|
.multilineTextAlignment(.center)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
.accessibilityElement(children: .combine)
|
|
|
|
Spacer()
|
|
.frame(height: 16)
|
|
|
|
Text(taglineText)
|
|
.font(.system(size: 14, weight: .regular, design: .rounded))
|
|
.foregroundColor(tertiaryTextColor)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer()
|
|
|
|
// Unlock button
|
|
themedButton
|
|
.disabled(authManager.isAuthenticating)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 30)
|
|
.padding(.horizontal, 32)
|
|
.accessibilityIdentifier(AccessibilityID.LockScreen.unlockButton)
|
|
.accessibilityLabel("Unlock")
|
|
.accessibilityHint("Double tap to authenticate with \(authManager.biometricName)")
|
|
|
|
// Passcode button
|
|
if authManager.canUseDevicePasscode {
|
|
Button {
|
|
Task {
|
|
let success = await authManager.authenticate()
|
|
if !success {
|
|
showError = true
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Or use your device passcode")
|
|
.font(.system(size: 13, weight: .medium, design: .rounded))
|
|
.foregroundColor(passcodeButtonColor)
|
|
}
|
|
.disabled(authManager.isAuthenticating)
|
|
.padding(.top, 16)
|
|
.opacity(showContent ? 1 : 0)
|
|
.accessibilityIdentifier(AccessibilityID.LockScreen.passcodeUnlockButton)
|
|
.accessibilityLabel("Use device passcode")
|
|
.accessibilityHint("Double tap to authenticate with your device passcode")
|
|
}
|
|
|
|
Spacer()
|
|
.frame(height: 50)
|
|
}
|
|
.padding()
|
|
}
|
|
.alert("Authentication Failed", isPresented: $showError) {
|
|
Button("Try Again") {
|
|
Task {
|
|
await authManager.authenticate()
|
|
}
|
|
}
|
|
.accessibilityIdentifier(AccessibilityID.LockScreen.tryAgainButton)
|
|
Button("Cancel", role: .cancel) { }
|
|
.accessibilityIdentifier(AccessibilityID.LockScreen.cancelButton)
|
|
} message: {
|
|
Text("Unable to verify your identity. Please try again.")
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeOut(duration: AnimationConstants.contentAppearDuration).delay(AnimationConstants.contentAppearDelay)) {
|
|
showContent = true
|
|
}
|
|
|
|
if !authManager.isUnlocked && !authManager.isAuthenticating {
|
|
Task {
|
|
try? await Task.sleep(for: .milliseconds(AnimationConstants.authenticationDelay))
|
|
await authManager.authenticate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Themed Components
|
|
|
|
@ViewBuilder
|
|
private var backgroundView: some View {
|
|
switch lockScreenStyle {
|
|
case .aurora:
|
|
AuroraBackground()
|
|
case .zen:
|
|
ZenLockBackground()
|
|
case .neon:
|
|
NeonLockBackground()
|
|
case .celestial:
|
|
CelestialLockBackground()
|
|
case .editorial:
|
|
EditorialLockBackground()
|
|
case .mixtape:
|
|
MixtapeLockBackground()
|
|
case .bloom:
|
|
BloomLockBackground()
|
|
case .heartfelt:
|
|
HeartfeltLockBackground()
|
|
case .minimal:
|
|
MinimalLockBackground()
|
|
case .luxe:
|
|
LuxeLockBackground()
|
|
case .forecast:
|
|
ForecastLockBackground()
|
|
case .playful:
|
|
PlayfulLockBackground()
|
|
case .journal:
|
|
JournalLockBackground()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var centralElement: some View {
|
|
switch lockScreenStyle {
|
|
case .aurora:
|
|
BreathingOrb()
|
|
case .zen:
|
|
ZenEnsoOrb()
|
|
case .neon:
|
|
NeonRingOrb()
|
|
case .celestial:
|
|
CelestialOrbsElement()
|
|
case .editorial:
|
|
EditorialFrameElement()
|
|
case .mixtape:
|
|
CassetteElement()
|
|
case .bloom:
|
|
FlowerElement()
|
|
case .heartfelt:
|
|
HeartElement()
|
|
case .minimal:
|
|
MinimalCircleElement()
|
|
case .luxe:
|
|
DiamondElement()
|
|
case .forecast:
|
|
WeatherElement()
|
|
case .playful:
|
|
PlayfulEmojiElement()
|
|
case .journal:
|
|
JournalBookElement()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var themedButton: some View {
|
|
switch lockScreenStyle {
|
|
case .neon:
|
|
NeonUnlockButton(
|
|
icon: authManager.biometricIcon,
|
|
title: "Unlock with \(authManager.biometricName)"
|
|
) {
|
|
Task {
|
|
let success = await authManager.authenticate()
|
|
if !success { showError = true }
|
|
}
|
|
}
|
|
case .luxe:
|
|
LuxeUnlockButton(
|
|
icon: authManager.biometricIcon,
|
|
title: "Unlock with \(authManager.biometricName)"
|
|
) {
|
|
Task {
|
|
let success = await authManager.authenticate()
|
|
if !success { showError = true }
|
|
}
|
|
}
|
|
case .mixtape:
|
|
MixtapeUnlockButton(
|
|
icon: authManager.biometricIcon,
|
|
title: "Unlock with \(authManager.biometricName)"
|
|
) {
|
|
Task {
|
|
let success = await authManager.authenticate()
|
|
if !success { showError = true }
|
|
}
|
|
}
|
|
default:
|
|
GlassButton(
|
|
icon: authManager.biometricIcon,
|
|
title: "Unlock with \(authManager.biometricName)"
|
|
) {
|
|
Task {
|
|
let success = await authManager.authenticate()
|
|
if !success { showError = true }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var passcodeButtonColor: Color {
|
|
switch lockScreenStyle {
|
|
case .neon:
|
|
return Color(red: 0, green: 1, blue: 0.82).opacity(0.7)
|
|
case .luxe:
|
|
return Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.7)
|
|
case .mixtape, .forecast:
|
|
return .white.opacity(0.6)
|
|
case .editorial:
|
|
return isDark ? .white.opacity(0.5) : .black.opacity(0.5)
|
|
default:
|
|
return isDark ? .white.opacity(0.5) : .accentColor
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Themed Unlock Buttons
|
|
|
|
struct NeonUnlockButton: View {
|
|
let icon: String
|
|
let title: String
|
|
let action: () -> Void
|
|
|
|
@State private var pulse = false
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 14) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 20, weight: .bold))
|
|
Text(title)
|
|
.font(.system(size: 15, weight: .bold, design: .monospaced))
|
|
}
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 18)
|
|
.padding(.horizontal, 24)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color.black)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
),
|
|
lineWidth: 2
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0, green: 1, blue: 0.82).opacity(pulse ? 0.5 : 0.2), radius: pulse ? 15 : 8)
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
|
pulse = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pulse = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LuxeUnlockButton: View {
|
|
let icon: String
|
|
let title: String
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 14) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 20, weight: .medium))
|
|
Text(title)
|
|
.font(.system(size: 16, weight: .medium, design: .serif))
|
|
}
|
|
.foregroundColor(Color(red: 0.95, green: 0.9, blue: 0.75))
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 18)
|
|
.padding(.horizontal, 24)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.55, green: 0.45, blue: 0.25),
|
|
Color(red: 0.4, green: 0.32, blue: 0.18)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.85, green: 0.7, blue: 0.45),
|
|
Color(red: 0.65, green: 0.52, blue: 0.3)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 1
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.3), radius: 10)
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
}
|
|
|
|
struct MixtapeUnlockButton: View {
|
|
let icon: String
|
|
let title: String
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 14) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 20, weight: .bold))
|
|
Text(title)
|
|
.font(.system(size: 15, weight: .bold, design: .rounded))
|
|
}
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 18)
|
|
.padding(.horizontal, 24)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.fill(Color.black.opacity(0.7))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
|
)
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
#Preview("Aurora") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .aurora)
|
|
}
|
|
|
|
#Preview("Zen") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .zen)
|
|
}
|
|
|
|
#Preview("Neon") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .neon)
|
|
}
|
|
|
|
#Preview("Celestial") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .celestial)
|
|
}
|
|
|
|
#Preview("Editorial") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .editorial)
|
|
}
|
|
|
|
#Preview("Mixtape") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .mixtape)
|
|
}
|
|
|
|
#Preview("Bloom") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .bloom)
|
|
}
|
|
|
|
#Preview("Heartfelt") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .heartfelt)
|
|
}
|
|
|
|
#Preview("Minimal") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .minimal)
|
|
}
|
|
|
|
#Preview("Luxe") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .luxe)
|
|
}
|
|
|
|
#Preview("Forecast") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .forecast)
|
|
}
|
|
|
|
#Preview("Playful") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .playful)
|
|
}
|
|
|
|
#Preview("Journal") {
|
|
LockScreenView(authManager: BiometricAuthManager(), style: .journal)
|
|
}
|