// // LocalPlayerView.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import SwiftUI struct LocalPlayerView: View { @Environment(MAService.self) private var service @Environment(\.audioPlayer) private var audioPlayer var body: some View { NavigationStack { VStack(spacing: 24) { if let player = audioPlayer { // Now Playing Section nowPlayingSection(player: player) // Progress Bar progressBar(player: player) // Transport Controls transportControls(player: player) // Volume Control volumeControl(player: player) } else { ContentUnavailableView( "No Active Playback", systemImage: "play.circle", description: Text("Play something from your library to see controls here") ) } Spacer() } .padding() .navigationTitle("Now Playing") .navigationBarTitleDisplayMode(.inline) } } // MARK: - Now Playing Section @ViewBuilder private func nowPlayingSection(player: MAAudioPlayer) -> some View { VStack(spacing: 16) { // Album Art if let item = player.currentItem, let mediaItem = item.mediaItem, let imageUrl = mediaItem.imageUrl { let coverURL = service.imageProxyURL(path: imageUrl, size: 512) CachedAsyncImage(url: coverURL) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Rectangle() .fill(Color.gray.opacity(0.2)) .overlay { ProgressView() } } .frame(width: 300, height: 300) .clipShape(RoundedRectangle(cornerRadius: 12)) .shadow(radius: 10) } else { RoundedRectangle(cornerRadius: 12) .fill(Color.gray.opacity(0.2)) .frame(width: 300, height: 300) .overlay { Image(systemName: "music.note") .font(.system(size: 60)) .foregroundStyle(.secondary) } .shadow(radius: 10) } // Track Info VStack(spacing: 8) { if let item = player.currentItem { Text(item.name) .font(.title2) .fontWeight(.semibold) .multilineTextAlignment(.center) if let mediaItem = item.mediaItem { if let artists = mediaItem.artists, !artists.isEmpty { Text(artists.map { $0.name }.joined(separator: ", ")) .font(.title3) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } if let album = mediaItem.album { Text(album.name) .font(.subheadline) .foregroundStyle(.tertiary) .multilineTextAlignment(.center) } } } else { Text("No Track Playing") .font(.title2) .foregroundStyle(.secondary) } } .padding(.horizontal) } .padding(.top) } // MARK: - Progress Bar @ViewBuilder private func progressBar(player: MAAudioPlayer) -> some View { VStack(spacing: 8) { // Progress slider Slider( value: Binding( get: { player.currentTime }, set: { player.seek(to: $0) } ), in: 0...max(1, player.duration) ) // Time labels HStack { Text(formatTime(player.currentTime)) .font(.caption) .foregroundStyle(.secondary) Spacer() Text(formatTime(player.duration)) .font(.caption) .foregroundStyle(.secondary) } } .padding(.horizontal) } // MARK: - Transport Controls @ViewBuilder private func transportControls(player: MAAudioPlayer) -> some View { HStack(spacing: 40) { // Previous Button { Task { await player.previousTrack() } } label: { Image(systemName: "backward.fill") .font(.system(size: 32)) .foregroundStyle(.primary) } // Play/Pause Button { if player.isPlaying { player.pause() } else { player.play() } } label: { Image(systemName: player.isPlaying ? "pause.circle.fill" : "play.circle.fill") .font(.system(size: 64)) .foregroundStyle(.primary) } // Next Button { Task { await player.nextTrack() } } label: { Image(systemName: "forward.fill") .font(.system(size: 32)) .foregroundStyle(.primary) } } .padding() } // MARK: - Volume Control @ViewBuilder private func volumeControl(player: MAAudioPlayer) -> some View { VStack(spacing: 12) { HStack { Image(systemName: "speaker.fill") .foregroundStyle(.secondary) // System volume - read-only on iOS Slider( value: Binding( get: { Double(player.volume) }, set: { _ in } ), in: 0...1 ) .disabled(true) Image(systemName: "speaker.wave.3.fill") .foregroundStyle(.secondary) } Text("Use device volume buttons") .font(.caption2) .foregroundStyle(.secondary) } .padding(.horizontal) } // MARK: - Helpers private func formatTime(_ seconds: TimeInterval) -> String { guard seconds.isFinite else { return "0:00" } let minutes = Int(seconds) / 60 let remainingSeconds = Int(seconds) % 60 return String(format: "%d:%02d", minutes, remainingSeconds) } } #Preview { LocalPlayerView() .environment(MAService()) }