import SwiftUI struct SplashView: View { @State private var appeared = false var body: some View { ZStack { #if os(iOS) Color(.systemBackground).ignoresSafeArea() #else Color(NSColor.windowBackgroundColor).ignoresSafeArea() #endif VStack(spacing: 36) { // ── Animated icon ──────────────────────────────────────── ZStack { // Outer glow pulse Circle() .fill(Color.accentColor.opacity(0.15)) .frame(width: 180, height: 180) .scaleEffect(appeared ? 1.0 : 0.2) .blur(radius: appeared ? 12 : 40) .animation(.easeOut(duration: 1.1), value: appeared) // Ring border Circle() .strokeBorder(Color.accentColor.opacity(0.35), lineWidth: 1.5) .frame(width: 130, height: 130) .scaleEffect(appeared ? 1.0 : 0.4) .opacity(appeared ? 1 : 0) .animation(.easeOut(duration: 0.8).delay(0.1), value: appeared) // Book icon springs into place Image(systemName: "books.vertical.fill") .font(.system(size: 58, weight: .regular)) .foregroundStyle(Color.accentColor) .scaleEffect(appeared ? 1.0 : 0.1) .opacity(appeared ? 1 : 0) .animation(.spring(duration: 0.65, bounce: 0.55), value: appeared) .symbolEffect(.pulse.byLayer, options: .speed(0.5).repeating, value: appeared) } // ── Text ───────────────────────────────────────────────── VStack(spacing: 6) { Text("ABS Client") .font(.title2.bold()) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 18) .animation(.easeOut(duration: 0.5).delay(0.28), value: appeared) Text("Audiobookshelf") .font(.subheadline) .foregroundStyle(.secondary) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 10) .animation(.easeOut(duration: 0.45).delay(0.42), value: appeared) } } // ── Loading dots at bottom ──────────────────────────────── VStack { Spacer() LoadingDots() .opacity(appeared ? 1 : 0) .animation(.easeIn(duration: 0.3).delay(0.65), value: appeared) .padding(.bottom, 60) } } .onAppear { appeared = true } } } private struct LoadingDots: View { @State private var phase: Int = 0 var body: some View { HStack(spacing: 7) { ForEach(0..<3, id: \.self) { i in Circle() .fill(Color.accentColor.opacity(phase == i ? 0.9 : 0.3)) .frame(width: 7, height: 7) .scaleEffect(phase == i ? 1.25 : 1.0) .animation(.easeInOut(duration: 0.35), value: phase) } } .onAppear { Timer.scheduledTimer(withTimeInterval: 0.38, repeats: true) { _ in phase = (phase + 1) % 3 } } } }