247 lines
7.8 KiB
Swift
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())
|
|
}
|