QueueView.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import SwiftUI
  2. /// Sheet showing the current playback queue: Now Playing, User Queue, Up Next.
  3. struct QueueView: View {
  4. @Environment(PlayerViewModel.self) private var playerVM
  5. @EnvironmentObject private var theme: AppTheme
  6. @Environment(\.dismiss) private var dismiss
  7. var body: some View {
  8. NavigationStack {
  9. List {
  10. // Now Playing
  11. if let nowPlaying = playerVM.nowPlayingEntry {
  12. Section("Now Playing") {
  13. queueRow(nowPlaying, isNowPlaying: true)
  14. }
  15. }
  16. // User Queue (manually added)
  17. if !playerVM.userQueue.isEmpty {
  18. Section("Next in Queue") {
  19. ForEach(playerVM.userQueue) { entry in
  20. queueRow(entry)
  21. }
  22. .onMove { source, destination in
  23. playerVM.moveUserQueueEntry(from: source, to: destination)
  24. }
  25. .onDelete { offsets in
  26. for index in offsets.sorted().reversed() {
  27. let entry = playerVM.userQueue[index]
  28. playerVM.removeFromQueue(entry: entry)
  29. }
  30. }
  31. }
  32. }
  33. // Up Next (auto from playlist)
  34. if !playerVM.upNext.isEmpty {
  35. Section("Up Next") {
  36. ForEach(playerVM.upNext) { entry in
  37. queueRow(entry)
  38. }
  39. .onMove { source, destination in
  40. playerVM.moveUpNextEntry(from: source, to: destination)
  41. }
  42. .onDelete { offsets in
  43. for index in offsets.sorted().reversed() {
  44. let entry = playerVM.upNext[index]
  45. playerVM.removeFromQueue(entry: entry)
  46. }
  47. }
  48. }
  49. }
  50. if playerVM.nowPlayingEntry == nil && playerVM.userQueue.isEmpty && playerVM.upNext.isEmpty {
  51. Section {
  52. VStack(spacing: 12) {
  53. Image(systemName: "list.bullet")
  54. .font(.system(size: 36))
  55. .foregroundStyle(theme.tertiaryText)
  56. Text("Queue is empty")
  57. .font(.title3)
  58. .foregroundStyle(theme.secondaryText)
  59. Text("Add tracks using \"Play Next\" or \"Add to Queue\" from any track's context menu.")
  60. .font(.caption)
  61. .foregroundStyle(theme.tertiaryText)
  62. .multilineTextAlignment(.center)
  63. }
  64. .frame(maxWidth: .infinity)
  65. .padding(.vertical, 40)
  66. }
  67. .listRowBackground(Color.clear)
  68. }
  69. }
  70. .listStyle(.insetGrouped)
  71. .environment(\.editMode, .constant(.active))
  72. .navigationTitle("Queue")
  73. .navigationBarTitleDisplayMode(.inline)
  74. .toolbar {
  75. ToolbarItem(placement: .topBarLeading) {
  76. if !playerVM.userQueue.isEmpty || !playerVM.upNext.isEmpty {
  77. Button("Clear") {
  78. playerVM.clearQueue()
  79. }
  80. .foregroundStyle(.red)
  81. }
  82. }
  83. ToolbarItem(placement: .topBarTrailing) {
  84. Button("Done") { dismiss() }
  85. }
  86. }
  87. }
  88. }
  89. @ViewBuilder
  90. private func queueRow(_ entry: QueueEntry, isNowPlaying: Bool = false) -> some View {
  91. HStack(spacing: 12) {
  92. // Cloud indicator or music note
  93. Group {
  94. switch entry.source {
  95. case .cloudDirect:
  96. Image(systemName: "cloud.fill")
  97. .foregroundStyle(theme.accent)
  98. case .swiftDataTrack(_, let isCloud, _):
  99. if isCloud {
  100. Image(systemName: "cloud.fill")
  101. .foregroundStyle(theme.accent)
  102. } else {
  103. Image(systemName: "music.note")
  104. .foregroundStyle(theme.tertiaryText)
  105. }
  106. }
  107. }
  108. .font(.caption)
  109. .frame(width: 24)
  110. VStack(alignment: .leading, spacing: 2) {
  111. Text(entry.title)
  112. .font(.subheadline.weight(isNowPlaying ? .semibold : .regular))
  113. .foregroundStyle(isNowPlaying ? theme.accent : theme.primaryText)
  114. .lineLimit(1)
  115. if !entry.artist.isEmpty {
  116. Text(entry.artist)
  117. .font(.caption)
  118. .foregroundStyle(theme.secondaryText)
  119. .lineLimit(1)
  120. }
  121. }
  122. Spacer()
  123. Text(entry.formattedDuration)
  124. .font(.caption.monospacedDigit())
  125. .foregroundStyle(theme.tertiaryText)
  126. if isNowPlaying && playerVM.isPlaying {
  127. Image(systemName: "speaker.wave.2.fill")
  128. .font(.caption)
  129. .foregroundStyle(theme.accent)
  130. }
  131. }
  132. .padding(.vertical, 2)
  133. }
  134. }