Files
MobileMusicAssistant/Mobile Music Assistant/ViewsRootView.swift
T
2026-04-16 05:48:51 +02:00

167 lines
5.2 KiB
Swift

//
// 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())
}