| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import SwiftUI
- /// Sheet showing the current playback queue: Now Playing, User Queue, Up Next.
- struct QueueView: View {
- @Environment(PlayerViewModel.self) private var playerVM
- @EnvironmentObject private var theme: AppTheme
- @Environment(\.dismiss) private var dismiss
- var body: some View {
- NavigationStack {
- List {
- // Now Playing
- if let nowPlaying = playerVM.nowPlayingEntry {
- Section("Now Playing") {
- queueRow(nowPlaying, isNowPlaying: true)
- }
- }
- // User Queue (manually added)
- if !playerVM.userQueue.isEmpty {
- Section("Next in Queue") {
- ForEach(playerVM.userQueue) { entry in
- queueRow(entry)
- }
- .onMove { source, destination in
- playerVM.moveUserQueueEntry(from: source, to: destination)
- }
- .onDelete { offsets in
- for index in offsets.sorted().reversed() {
- let entry = playerVM.userQueue[index]
- playerVM.removeFromQueue(entry: entry)
- }
- }
- }
- }
- // Up Next (auto from playlist)
- if !playerVM.upNext.isEmpty {
- Section("Up Next") {
- ForEach(playerVM.upNext) { entry in
- queueRow(entry)
- }
- .onMove { source, destination in
- playerVM.moveUpNextEntry(from: source, to: destination)
- }
- .onDelete { offsets in
- for index in offsets.sorted().reversed() {
- let entry = playerVM.upNext[index]
- playerVM.removeFromQueue(entry: entry)
- }
- }
- }
- }
- if playerVM.nowPlayingEntry == nil && playerVM.userQueue.isEmpty && playerVM.upNext.isEmpty {
- Section {
- VStack(spacing: 12) {
- Image(systemName: "list.bullet")
- .font(.system(size: 36))
- .foregroundStyle(theme.tertiaryText)
- Text("Queue is empty")
- .font(.title3)
- .foregroundStyle(theme.secondaryText)
- Text("Add tracks using \"Play Next\" or \"Add to Queue\" from any track's context menu.")
- .font(.caption)
- .foregroundStyle(theme.tertiaryText)
- .multilineTextAlignment(.center)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 40)
- }
- .listRowBackground(Color.clear)
- .accessibilityIdentifier("queue.emptyState")
- }
- }
- .listStyle(.insetGrouped)
- .environment(\.editMode, .constant(.active))
- .navigationTitle("Queue")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .topBarLeading) {
- if !playerVM.userQueue.isEmpty || !playerVM.upNext.isEmpty {
- Button("Clear") {
- playerVM.clearQueue()
- }
- .foregroundStyle(.red)
- .accessibilityIdentifier("queue.clearButton")
- }
- }
- ToolbarItem(placement: .topBarTrailing) {
- Button("Done") { dismiss() }
- }
- }
- }
- }
- @ViewBuilder
- private func queueRow(_ entry: QueueEntry, isNowPlaying: Bool = false) -> some View {
- HStack(spacing: 12) {
- // Cloud indicator or music note
- Group {
- switch entry.source {
- case .cloudDirect:
- Image(systemName: "cloud.fill")
- .foregroundStyle(theme.accent)
- case .swiftDataTrack(_, let isCloud, _):
- if isCloud {
- Image(systemName: "cloud.fill")
- .foregroundStyle(theme.accent)
- } else {
- Image(systemName: "music.note")
- .foregroundStyle(theme.tertiaryText)
- }
- }
- }
- .font(.caption)
- .frame(width: 24)
- VStack(alignment: .leading, spacing: 2) {
- Text(entry.title)
- .font(.subheadline.weight(isNowPlaying ? .semibold : .regular))
- .foregroundStyle(isNowPlaying ? theme.accent : theme.primaryText)
- .lineLimit(1)
- if !entry.artist.isEmpty {
- Text(entry.artist)
- .font(.caption)
- .foregroundStyle(theme.secondaryText)
- .lineLimit(1)
- }
- }
- Spacer()
- Text(entry.formattedDuration)
- .font(.caption.monospacedDigit())
- .foregroundStyle(theme.tertiaryText)
- if isNowPlaying && playerVM.isPlaying {
- Image(systemName: "speaker.wave.2.fill")
- .font(.caption)
- .foregroundStyle(theme.accent)
- }
- }
- .padding(.vertical, 2)
- }
- }
|