| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import SwiftUI
- /// Queue panel 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
- var body: some View {
- VStack(spacing: 0) {
- // Header
- HStack {
- Text("Queue")
- .font(.system(size: 13, weight: .semibold))
- Spacer()
- if !playerVM.userQueue.isEmpty || !playerVM.upNext.isEmpty {
- Button("Clear") {
- playerVM.clearQueue()
- }
- .font(.system(size: 11))
- .foregroundStyle(.red)
- .buttonStyle(.plain)
- }
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 8)
- .background(.bar)
- Divider()
- // Queue content
- 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)
- }
- }
- }
- }
- // Empty state
- if playerVM.nowPlayingEntry == nil && playerVM.userQueue.isEmpty && playerVM.upNext.isEmpty {
- Section {
- VStack(spacing: 12) {
- Image(systemName: "list.bullet")
- .font(.system(size: 36))
- .foregroundStyle(.tertiary)
- Text("Queue is empty")
- .font(.title3)
- .foregroundStyle(.secondary)
- Text("Add tracks using \"Play Next\" or \"Add to Queue\" from any track's context menu.")
- .font(.caption)
- .foregroundStyle(.tertiary)
- .multilineTextAlignment(.center)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 40)
- }
- }
- }
- .listStyle(.inset)
- }
- }
- @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(Color.accentColor)
- case .swiftDataTrack(_, let isCloud, _):
- if isCloud {
- Image(systemName: "cloud.fill")
- .foregroundStyle(Color.accentColor)
- } else {
- Image(systemName: "music.note")
- .foregroundStyle(.tertiary)
- }
- }
- }
- .font(.caption)
- .frame(width: 20)
- VStack(alignment: .leading, spacing: 2) {
- Text(entry.title)
- .font(.system(size: 12, weight: isNowPlaying ? .semibold : .regular))
- .foregroundStyle(isNowPlaying ? Color.accentColor : .primary)
- .lineLimit(1)
- if !entry.artist.isEmpty {
- Text(entry.artist)
- .font(.system(size: 11))
- .foregroundStyle(.secondary)
- .lineLimit(1)
- }
- }
- Spacer()
- Text(entry.formattedDuration)
- .font(.system(size: 11, design: .monospaced))
- .foregroundStyle(.tertiary)
- if isNowPlaying && playerVM.isPlaying {
- Image(systemName: "speaker.wave.2.fill")
- .font(.caption)
- .foregroundStyle(Color.accentColor)
- }
- }
- .padding(.vertical, 2)
- }
- }
|