186 lines
5.9 KiB
Swift
186 lines
5.9 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
|
|
@Environment(MAStoreManager.self) private var storeManager
|
|
|
|
@State private var isInitializing = true
|
|
@State private var loadingProgress: Double = 0.0
|
|
@State private var loadingPhase: String = "Starting…"
|
|
@State private var showNudge = false
|
|
|
|
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()
|
|
.sheet(isPresented: $showNudge, onDismiss: {
|
|
storeManager.recordNudgeShown()
|
|
}) {
|
|
SupportNudgeView(isPresented: $showNudge)
|
|
}
|
|
.task {
|
|
await initializeConnection()
|
|
}
|
|
.onChange(of: isInitializing) { _, initializing in
|
|
if !initializing { checkNudge() }
|
|
}
|
|
}
|
|
|
|
private func checkNudge() {
|
|
guard storeManager.shouldShowNudge else { return }
|
|
// Small delay so the main UI settles before the sheet appears
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(1))
|
|
showNudge = true
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|