Version 1.5: Groups
This commit is contained in:
@@ -6,49 +6,157 @@
|
||||
//
|
||||
|
||||
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 {
|
||||
// Loading screen while checking for saved credentials
|
||||
VStack(spacing: 20) {
|
||||
ProgressView()
|
||||
Text("Connecting...")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
SplashView(progress: loadingProgress, phase: loadingPhase)
|
||||
.transition(.opacity)
|
||||
} else if service.isConnected {
|
||||
// Main app view when connected
|
||||
MainTabView()
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
// Login view when not connected
|
||||
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 {
|
||||
// Try to connect with saved credentials
|
||||
if service.authManager.isAuthenticated {
|
||||
do {
|
||||
try await service.connectWithSavedCredentials()
|
||||
} catch {
|
||||
print("Auto-connect failed: \(error.localizedDescription)")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isInitializing = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user