// // PlayerQueueView.swift // Mobile Music Assistant // // Created by Sven Hanold on 06.04.26. // import SwiftUI struct PlayerQueueView: View { @Environment(MAService.self) private var service let playerId: String @State private var isLoading = false private var queueItems: [MAQueueItem] { service.playerManager.queues[playerId] ?? [] } private var currentIndex: Int? { service.playerManager.playerQueues[playerId]?.currentIndex } private var currentItemId: String? { service.playerManager.playerQueues[playerId]?.currentItem?.queueItemId } var body: some View { Group { if isLoading && queueItems.isEmpty { VStack { Spacer() ProgressView() Spacer() } } else if queueItems.isEmpty { VStack { Spacer() Text("Queue is empty") .font(.subheadline) .foregroundStyle(.secondary) Spacer() } } else { ScrollViewReader { proxy in ScrollView { LazyVStack(spacing: 0) { ForEach(Array(queueItems.enumerated()), id: \.element.id) { index, item in let isCurrent = currentIndex == index || item.queueItemId == currentItemId QueueItemRow(item: item, isCurrent: isCurrent) .id(item.queueItemId) .contentShape(Rectangle()) .onTapGesture { Task { try? await service.playerManager.playIndex( playerId: playerId, index: index ) } } if index < queueItems.count - 1 { Divider() .padding(.leading, 76) } } } .padding(.bottom, 8) } .onAppear { if let id = currentItemId { proxy.scrollTo(id, anchor: .center) } } .onChange(of: currentItemId) { _, newId in if let newId { withAnimation { proxy.scrollTo(newId, anchor: .center) } } } } } } .task { isLoading = true try? await service.playerManager.loadQueue(playerId: playerId) isLoading = false } } } // MARK: - Queue Item Row private struct QueueItemRow: View { @Environment(MAService.self) private var service let item: MAQueueItem let isCurrent: Bool var body: some View { HStack(spacing: 12) { // Thumbnail CachedAsyncImage(url: service.imageProxyURL( path: item.mediaItem?.imageUrl, provider: item.mediaItem?.imageProvider, size: 96 )) { image in image.resizable().scaledToFill() } placeholder: { RoundedRectangle(cornerRadius: 6) .fill(Color.gray.opacity(0.2)) .overlay { Image(systemName: "music.note") .font(.caption) .foregroundStyle(.secondary) } } .frame(width: 48, height: 48) .clipShape(RoundedRectangle(cornerRadius: 6)) // Track info VStack(alignment: .leading, spacing: 3) { HStack(spacing: 5) { if isCurrent { Image(systemName: "waveform") .font(.caption2) .foregroundStyle(.green) } Text(item.name) .font(.body) .fontWeight(isCurrent ? .semibold : .regular) .foregroundStyle(.primary) .lineLimit(1) } if let artists = item.mediaItem?.artists, !artists.isEmpty { Text(artists.map { $0.name }.joined(separator: ", ")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } Spacer() // Duration if let duration = item.duration ?? item.mediaItem?.duration { Text(formatDuration(duration)) .font(.caption) .foregroundStyle(.secondary) .monospacedDigit() } } .padding(.vertical, 8) .padding(.horizontal, 16) .background(isCurrent ? Color.primary.opacity(0.08) : Color.clear) } private func formatDuration(_ seconds: Int) -> String { let minutes = seconds / 60 let remainingSeconds = seconds % 60 return String(format: "%d:%02d", minutes, remainingSeconds) } }