Files
MobileMusicAssistant/Mobile Music Assistant/ViewsMainTabView.swift
T
2026-03-27 09:21:41 +01:00

247 lines
7.8 KiB
Swift

//
// MainTabView.swift
// Mobile Music Assistant
//
// Created by Sven Hanold on 26.03.26.
//
import SwiftUI
struct MainTabView: View {
@Environment(MAService.self) private var service
var body: some View {
TabView {
Tab("Players", systemImage: "speaker.wave.2.fill") {
PlayerListView()
}
Tab("Library", systemImage: "music.note.list") {
LibraryView()
}
Tab("Settings", systemImage: "gear") {
SettingsView()
}
}
.task {
// Start listening to player events when main view appears
service.playerManager.startListening()
}
.onDisappear {
// Stop listening when view disappears
service.playerManager.stopListening()
}
}
}
// MARK: - Placeholder Views (to be implemented in Phase 2+)
struct PlayerListView: View {
@Environment(MAService.self) private var service
@State private var isLoading = false
@State private var errorMessage: String?
private var players: [MAPlayer] {
Array(service.playerManager.players.values).sorted { $0.name < $1.name }
}
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView()
} else if let errorMessage {
ContentUnavailableView(
"Error Loading Players",
systemImage: "exclamationmark.triangle",
description: Text(errorMessage)
)
} else if players.isEmpty {
ContentUnavailableView(
"No Players Found",
systemImage: "speaker.slash",
description: Text("Make sure your Music Assistant server has configured players")
)
} else {
List(players) { player in
NavigationLink(value: player.playerId) {
PlayerRow(player: player)
}
}
.navigationDestination(for: String.self) { playerId in
PlayerView(playerId: playerId)
}
}
}
.navigationTitle("Players")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
Task {
await loadPlayers()
}
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.task {
await loadPlayers()
}
}
}
private func loadPlayers() async {
print("🔵 PlayerListView: Starting to load players...")
isLoading = true
errorMessage = nil
do {
print("🔵 PlayerListView: Calling playerManager.loadPlayers()")
try await service.playerManager.loadPlayers()
print("✅ PlayerListView: Successfully loaded \(players.count) players")
} catch {
print("❌ PlayerListView: Failed to load players: \(error)")
errorMessage = error.localizedDescription
}
isLoading = false
}
}
struct PlayerRow: View {
@Environment(MAService.self) private var service
let player: MAPlayer
var body: some View {
HStack(spacing: 12) {
// Album Art Thumbnail
if let item = player.currentItem,
let mediaItem = item.mediaItem,
let imageUrl = mediaItem.imageUrl {
let coverURL = service.imageProxyURL(path: imageUrl, size: 64)
CachedAsyncImage(url: coverURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.2))
}
.frame(width: 48, height: 48)
.clipShape(RoundedRectangle(cornerRadius: 6))
} else {
RoundedRectangle(cornerRadius: 6)
.fill(Color.gray.opacity(0.2))
.frame(width: 48, height: 48)
.overlay {
Image(systemName: "music.note")
.foregroundStyle(.secondary)
.font(.caption)
}
}
// Player Info
VStack(alignment: .leading, spacing: 4) {
Text(player.name)
.font(.headline)
HStack(spacing: 6) {
Image(systemName: stateIcon)
.foregroundStyle(stateColor)
.font(.caption)
Text(player.state.rawValue.capitalized)
.font(.caption)
.foregroundStyle(.secondary)
if let item = player.currentItem {
Text("\(item.name)")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
Spacer()
// Volume Indicator
if player.available {
VStack(spacing: 2) {
Image(systemName: "speaker.wave.2.fill")
.font(.caption)
.foregroundStyle(.secondary)
Text("\(player.volume)%")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 4)
}
private var stateIcon: String {
switch player.state {
case .playing: return "play.circle.fill"
case .paused: return "pause.circle.fill"
case .idle: return "stop.circle"
case .off: return "power.circle"
}
}
private var stateColor: Color {
switch player.state {
case .playing: return .green
case .paused: return .orange
case .idle: return .gray
case .off: return .red
}
}
}
// Removed - Now using dedicated PlayerView.swift file
// Removed - Now using dedicated LibraryView.swift file
struct SettingsView: View {
@Environment(MAService.self) private var service
var body: some View {
NavigationStack {
Form {
Section {
if let serverURL = service.authManager.serverURL {
LabeledContent("Server", value: serverURL.absoluteString)
}
LabeledContent("Status") {
HStack {
Circle()
.fill(service.isConnected ? .green : .red)
.frame(width: 8, height: 8)
Text(service.isConnected ? "Connected" : "Disconnected")
}
}
}
Section {
Button(role: .destructive) {
service.disconnect()
service.authManager.logout()
} label: {
Label("Disconnect", systemImage: "arrow.right.square")
}
}
}
.navigationTitle("Settings")
}
}
}
#Preview {
MainTabView()
.environment(MAService())
}