// // EnhancedPlayerPickerView.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import SwiftUI struct EnhancedPlayerPickerView: View { @Environment(\.dismiss) private var dismiss @Environment(MAService.self) private var service let players: [MAPlayer] let title: String let onSelect: (MAPlayer) -> Void init(players: [MAPlayer], title: String = "Play on...", onSelect: @escaping (MAPlayer) -> Void) { self.players = players self.title = title self.onSelect = onSelect } /// IDs of all players that are sync members (not the leader) private var syncedMemberIds: Set { Set(players.flatMap { $0.groupChilds }) } private var groupLeaders: [MAPlayer] { players.filter { $0.isGroupLeader } } private var soloPlayers: [MAPlayer] { players.filter { !$0.isGroupLeader && !syncedMemberIds.contains($0.playerId) } } var body: some View { NavigationStack { ScrollView { VStack(spacing: 12) { // Group cards at the top ForEach(groupLeaders) { leader in let memberNames = leader.groupChilds .compactMap { service.playerManager.players[$0]?.name } PickerGroupCard(leader: leader, memberNames: memberNames) { onSelect(leader) dismiss() } } // Solo player cards ForEach(soloPlayers) { player in PickerPlayerCard(player: player) { onSelect(player) dismiss() } } } .padding(.horizontal, 16) .padding(.vertical, 8) } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } } // MARK: - Picker Player Card private struct PickerPlayerCard: View { @Environment(MAService.self) private var service let player: MAPlayer let onSelect: () -> Void // Always read live state so the indicator reflects real-time changes private var livePlayer: MAPlayer { service.playerManager.players[player.playerId] ?? player } private var currentItem: MAQueueItem? { service.playerManager.playerQueues[player.playerId]?.currentItem } private var mediaItem: MAMediaItem? { currentItem?.mediaItem } var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { if livePlayer.state == .playing { Image(systemName: "waveform") .font(.caption) .foregroundStyle(.green) } Text(livePlayer.name) .font(.headline) .foregroundStyle(.primary) .lineLimit(1) } if let item = currentItem { Text(item.name) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) if let artists = mediaItem?.artists, !artists.isEmpty { Text(artists.map { $0.name }.joined(separator: ", ")) .font(.caption) .foregroundStyle(.tertiary) .lineLimit(1) } } else { Text(livePlayer.state == .off ? "Powered Off" : "No Track Playing") .font(.subheadline) .foregroundStyle(.tertiary) .lineLimit(1) } } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.secondary) } .padding(.horizontal, 16) .padding(.vertical, 14) .background { ZStack { CachedAsyncImage(url: service.imageProxyURL( path: mediaItem?.imageUrl, provider: mediaItem?.imageProvider, size: 256 )) { image in image.resizable().aspectRatio(contentMode: .fill) } placeholder: { Color.clear } .blur(radius: 20) .scaleEffect(1.1) .clipped() Rectangle().fill(.ultraThinMaterial) } } .clipShape(RoundedRectangle(cornerRadius: 16)) .contentShape(RoundedRectangle(cornerRadius: 16)) .onTapGesture { onSelect() } } } // MARK: - Picker Group Card private struct PickerGroupCard: View { @Environment(MAService.self) private var service let leader: MAPlayer let memberNames: [String] let onSelect: () -> Void // Always read live state so the indicator reflects real-time changes private var liveLeader: MAPlayer { service.playerManager.players[leader.playerId] ?? leader } private var currentItem: MAQueueItem? { service.playerManager.playerQueues[leader.playerId]?.currentItem } private var mediaItem: MAMediaItem? { currentItem?.mediaItem } private var groupName: String { ([liveLeader.name] + memberNames).joined(separator: " + ") } var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Image(systemName: "speaker.2.fill") .font(.caption) .foregroundStyle(.blue) if liveLeader.state == .playing { Image(systemName: "waveform") .font(.caption) .foregroundStyle(.green) } Text(groupName) .font(.headline) .foregroundStyle(.primary) .lineLimit(1) } if let item = currentItem { Text(item.name) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) if let artists = mediaItem?.artists, !artists.isEmpty { Text(artists.map { $0.name }.joined(separator: ", ")) .font(.caption) .foregroundStyle(.tertiary) .lineLimit(1) } } else { Text(liveLeader.state == .off ? "Powered Off" : "No Track Playing") .font(.subheadline) .foregroundStyle(.tertiary) .lineLimit(1) } } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.secondary) } .padding(.horizontal, 16) .padding(.vertical, 14) .background { ZStack { CachedAsyncImage(url: service.imageProxyURL( path: mediaItem?.imageUrl, provider: mediaItem?.imageProvider, size: 256 )) { image in image.resizable().aspectRatio(contentMode: .fill) } placeholder: { Color.clear } .blur(radius: 20) .scaleEffect(1.1) .clipped() Rectangle().fill(.ultraThinMaterial) } } .clipShape(RoundedRectangle(cornerRadius: 16)) .contentShape(RoundedRectangle(cornerRadius: 16)) .onTapGesture { onSelect() } } } #Preview { EnhancedPlayerPickerView( players: [], onSelect: { _ in } ) .environment(MAService()) }