Audit found ~50+ interactive elements (buttons, toggles, pickers, alerts, links) missing accessibility identifiers across 13 view files. Added centralized ID definitions and applied them to every entry detail button, guided reflection control, settings toggle, paywall unlock button, subscription/IAP button, lock screen control, and photo action dialog.
2046 lines
65 KiB
Swift
2046 lines
65 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 {
|
|
@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)
|
|
.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: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
|
|
if !authManager.isUnlocked && !authManager.isAuthenticating {
|
|
Task {
|
|
try? await Task.sleep(for: .milliseconds(800))
|
|
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)
|
|
}
|