// // RootView.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import SwiftUI import UIKit struct RootView: View { @Environment(MAService.self) private var service @State private var isInitializing = true @State private var loadingProgress: Double = 0.0 @State private var loadingPhase: String = "Starting…" var body: some View { Group { if isInitializing { SplashView(progress: loadingProgress, phase: loadingPhase) .transition(.opacity) } else if service.isConnected { MainTabView() .transition(.opacity) } else { LoginView() .transition(.opacity) } } .animation(.easeInOut(duration: 0.4), value: isInitializing) .animation(.easeInOut(duration: 0.4), value: service.isConnected) .applyTheme() .applyLocale() .task { await initializeConnection() } } // MARK: - Initialization private func initializeConnection() async { guard service.authManager.isAuthenticated else { // No saved credentials — skip straight to login withAnimation { loadingProgress = 1.0 } try? await Task.sleep(for: .milliseconds(300)) isInitializing = false return } // Phase 1: Connect to server withAnimation { loadingPhase = "Connecting to server…"; loadingProgress = 0.1 } do { try await service.connectWithSavedCredentials() } catch { print("Auto-connect failed: \(error.localizedDescription)") withAnimation { loadingProgress = 1.0 } try? await Task.sleep(for: .milliseconds(300)) isInitializing = false return } // Phase 2: Load players withAnimation { loadingPhase = "Loading players…"; loadingProgress = 0.55 } try? await service.playerManager.loadPlayers() // Done withAnimation { loadingPhase = "Ready"; loadingProgress = 1.0 } try? await Task.sleep(for: .milliseconds(400)) isInitializing = false } } // MARK: - Splash Screen private let splashBackground = Color(red: 0.07, green: 0.09, blue: 0.12) private let splashTeal = Color(red: 0.0, green: 0.82, blue: 0.75) private struct SplashView: View { let progress: Double let phase: String var body: some View { ZStack { splashBackground.ignoresSafeArea() VStack(spacing: 0) { Spacer() // App icon if let icon = UIImage(named: "AppIcon") { Image(uiImage: icon) .resizable() .frame(width: 120, height: 120) .clipShape(RoundedRectangle(cornerRadius: 27)) .shadow(color: splashTeal.opacity(0.25), radius: 24, y: 8) } else { // Fallback: recreate the waveform icon in SwiftUI WaveformIcon() .frame(width: 120, height: 120) } Spacer().frame(height: 24) // App name Text("Mobile MA") .font(.title2) .fontWeight(.semibold) .foregroundStyle(.white) Text("A client for Music Assistant") .font(.subheadline) .foregroundStyle(.white.opacity(0.45)) Spacer() // Progress section VStack(spacing: 10) { GeometryReader { geo in ZStack(alignment: .leading) { Capsule() .fill(.white.opacity(0.1)) .frame(height: 3) Capsule() .fill(splashTeal) .frame(width: geo.size.width * progress, height: 3) .animation(.easeInOut(duration: 0.4), value: progress) } } .frame(height: 3) .padding(.horizontal, 48) Text(phase) .font(.caption) .foregroundStyle(.white.opacity(0.4)) .animation(.easeInOut, value: phase) } .padding(.bottom, 60) } } } } /// Fallback waveform graphic matching the app icon style. private struct WaveformIcon: View { private let barHeights: [CGFloat] = [0.32, 0.55, 0.75, 1.0, 0.78, 0.52, 0.28] var body: some View { ZStack { RoundedRectangle(cornerRadius: 27) .fill(splashBackground) HStack(alignment: .center, spacing: 5) { ForEach(Array(barHeights.enumerated()), id: \.offset) { _, h in Capsule() .fill(splashTeal.opacity(0.5 + 0.5 * h)) .frame(width: 10, height: 70 * h) } } } } } #Preview { RootView() .environment(MAService()) }