diff --git a/Jarvis/JarvisApp.swift b/Jarvis/JarvisApp.swift index 7a2f371..ed7b8fb 100644 --- a/Jarvis/JarvisApp.swift +++ b/Jarvis/JarvisApp.swift @@ -5,33 +5,53 @@ import SwiftData import UIKit import BackgroundTasks -// MARK: - Logo + splash - -/// The three-bar logo that mirrors the app icon. -struct JarvisLogoMark: View { - var width: CGFloat = 180 - - var body: some View { - VStack(alignment: .center, spacing: 8) { - // Orange (short) · White (longest) · Grey (medium) — same ratios as icon - Capsule().fill(Palette.orange) - .frame(width: width * 0.48, height: width * 0.075) - .shadow(color: Palette.orange.opacity(0.55), radius: 10, y: 2) - Capsule().fill(Color.white) - .frame(width: width, height: width * 0.075) - Capsule().fill(Color(hex: "888888")) - .frame(width: width * 0.74, height: width * 0.075) - } - } -} +// MARK: - Splash struct SplashView: View { + var onDismiss: () -> Void + + @State private var iconScale: CGFloat = 0.82 + @State private var iconOpacity: Double = 0 + @State private var markOpacity: Double = 0 + @State private var rootOpacity: Double = 1 + var body: some View { ZStack { Color(hex: "0A0A0A").ignoresSafeArea() - VStack(spacing: 28) { - JarvisLogoMark(width: 180) - JarvisWordmark(size: 34) + VStack(spacing: 22) { + // App icon displayed as a circle + Group { + if let img = UIImage(named: "AppIcon") { + Image(uiImage: img) + .resizable() + } else { + Circle().fill(Palette.surface) + } + } + .frame(width: 108, height: 108) + .clipShape(Circle()) + .shadow(color: Color.black.opacity(0.4), radius: 20, y: 6) + .scaleEffect(iconScale) + .opacity(iconOpacity) + + JarvisWordmark(size: 28) + .opacity(markOpacity) + } + } + .opacity(rootOpacity) + .onAppear { + withAnimation(.spring(response: 0.48, dampingFraction: 0.74).delay(0.08)) { + iconScale = 1 + iconOpacity = 1 + } + withAnimation(.easeOut(duration: 0.28).delay(0.32)) { + markOpacity = 1 + } + withAnimation(.easeIn(duration: 0.26).delay(1.6)) { + rootOpacity = 0 + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.9) { + onDismiss() } } } @@ -69,7 +89,7 @@ struct JarvisApp: App { @StateObject private var connectivity = ConnectivitySettings.shared @StateObject private var connManager = ConnectivityManager.shared @StateObject private var notifications = NotificationManager.shared - @State private var showSplash = true + @State private var splashDone = false // Single ModelContainer instance shared with BackgroundRefreshManager. private let container: ModelContainer = { @@ -81,21 +101,19 @@ struct JarvisApp: App { var body: some Scene { WindowGroup { ZStack { - if showSplash { - SplashView() - .transition(.opacity) - } else { - Group { - if settings.isConfigured { - RootTabView() - } else { - OnboardingView() - } + // Main app always rendered underneath so it's ready when splash fades. + Group { + if settings.isConfigured { + RootTabView() + } else { + OnboardingView() } - .transition(.opacity) + } + + if !splashDone { + SplashView { splashDone = true } } } - .animation(.easeOut(duration: 0.5), value: showSplash) .preferredColorScheme(appearanceMode.colorScheme) .environmentObject(settings) .environmentObject(store) @@ -109,9 +127,6 @@ struct JarvisApp: App { connManager.start() await connManager.resolveAndActivate() await notifications.bootstrap() - // Hold splash until setup is done, minimum 1.3 s for legibility. - try? await Task.sleep(nanoseconds: 1_300_000_000) - showSplash = false } } .modelContainer(container)