// // PlayerNowPlayingView.swift // Mobile Music Assistant // // Created by Sven Hanold on 05.04.26. // import SwiftUI struct PlayerNowPlayingView: View { @Environment(MAService.self) private var service @Environment(\.dismiss) private var dismiss let playerId: String // Auto-tracks live updates via @Observable private var player: MAPlayer? { service.playerManager.players[playerId] } private var mediaItem: MAMediaItem? { player?.currentItem?.mediaItem } var body: some View { ZStack { // Blurred artwork background CachedAsyncImage(url: service.imageProxyURL( path: mediaItem?.imageUrl, provider: mediaItem?.imageProvider, size: 64 )) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Color.clear } .ignoresSafeArea() .blur(radius: 80) .scaleEffect(1.4) .opacity(0.5) Rectangle() .fill(.ultraThinMaterial) .ignoresSafeArea() // Content VStack(spacing: 0) { // Drag indicator Capsule() .fill(.secondary.opacity(0.4)) .frame(width: 36, height: 4) .padding(.top, 12) .padding(.bottom, 8) // Player name HStack { Button { dismiss() } label: { Image(systemName: "chevron.down") .font(.title3) .fontWeight(.semibold) .foregroundStyle(.primary) .frame(width: 44, height: 44) .contentShape(Rectangle()) } Spacer() VStack(spacing: 2) { Text("Now Playing") .font(.caption) .foregroundStyle(.secondary) Text(player?.name ?? "") .font(.subheadline) .fontWeight(.semibold) .lineLimit(1) } Spacer() // Balance chevron button Color.clear .frame(width: 44, height: 44) } .padding(.horizontal, 20) .padding(.bottom, 8) // Album art GeometryReader { geo in let size = min(geo.size.width - 64, geo.size.height) CachedAsyncImage(url: service.imageProxyURL( path: mediaItem?.imageUrl, provider: mediaItem?.imageProvider, size: 512 )) { image in image .resizable() .aspectRatio(1, contentMode: .fill) } placeholder: { RoundedRectangle(cornerRadius: 16) .fill(Color.gray.opacity(0.2)) .overlay { Image(systemName: "music.note") .font(.system(size: 56)) .foregroundStyle(.secondary) } } .frame(width: size, height: size) .clipShape(RoundedRectangle(cornerRadius: 16)) .shadow(color: .black.opacity(0.35), radius: 24, y: 12) .frame(maxWidth: .infinity, maxHeight: .infinity) } .padding(.horizontal, 32) .padding(.vertical, 24) // Track info VStack(spacing: 6) { Text(player?.currentItem?.name ?? "–") .font(.title2) .fontWeight(.bold) .lineLimit(2) .multilineTextAlignment(.center) if let artists = mediaItem?.artists, !artists.isEmpty { Text(artists.map { $0.name }.joined(separator: ", ")) .font(.body) .foregroundStyle(.secondary) .lineLimit(1) } else if let album = mediaItem?.album { Text(album.name) .font(.body) .foregroundStyle(.secondary) .lineLimit(1) } } .padding(.horizontal, 32) Spacer(minLength: 24) // Transport controls if let player { HStack(spacing: 48) { Button { Task { try? await service.playerManager.previousTrack(playerId: playerId) } } label: { Image(systemName: "backward.fill") .font(.system(size: 30)) .foregroundStyle(.primary) } Button { Task { if player.state == .playing { try? await service.playerManager.pause(playerId: playerId) } else { try? await service.playerManager.play(playerId: playerId) } } } label: { Image(systemName: player.state == .playing ? "pause.circle.fill" : "play.circle.fill") .font(.system(size: 72)) .foregroundStyle(.primary) .symbolEffect(.bounce, value: player.state == .playing) } Button { Task { try? await service.playerManager.nextTrack(playerId: playerId) } } label: { Image(systemName: "forward.fill") .font(.system(size: 30)) .foregroundStyle(.primary) } } .buttonStyle(.plain) } Spacer(minLength: 24) // Volume if let volume = player?.volume { HStack(spacing: 10) { Image(systemName: "speaker.fill") .font(.caption) .foregroundStyle(.secondary) .frame(width: 20) Slider( value: Binding( get: { Double(volume) }, set: { newValue in Task { try? await service.playerManager.setVolume( playerId: playerId, level: Int(newValue) ) } } ), in: 0...100, step: 1 ) Image(systemName: "speaker.wave.3.fill") .font(.caption) .foregroundStyle(.secondary) .frame(width: 20) } .padding(.horizontal, 32) } Spacer(minLength: 32) } } .presentationDetents([.large]) .presentationDragIndicator(.hidden) // using custom indicator } }