248 lines
8.2 KiB
Swift
248 lines
8.2 KiB
Swift
//
|
|
// 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<String> {
|
|
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())
|
|
}
|