Files
Amphlify/Amphlify/UI/Mac/NowPlaying/MacQueuePanel.swift
T

125 lines
5.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if os(macOS)
import Playback
import SwiftUI
struct MacQueuePanel: View {
@Environment(PlayerModel.self) private var player
private static let upcoming = "upcoming"
var body: some View {
ScrollViewReader { proxy in
List {
if !player.state.history.isEmpty {
Section {
ForEach(player.state.history) { row($0, upcoming: false, last: $0.id == player.state.history.last?.id) }
} header: {
header("History", clear: .clearHistory)
}
}
Section {
if player.state.upcoming.isEmpty {
Text("There’s no music in the queue.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, minHeight: 120)
.listRowSeparator(.hidden)
}
ForEach(player.state.upcoming) { row($0, upcoming: true, last: $0.id == player.state.upcoming.last?.id) }
.onMove(perform: move)
} header: {
header("Continue Playing", subtitle: source, clear: player.state.upcoming.isEmpty ? nil : .clearUpcoming)
.id(Self.upcoming)
}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.environment(\.defaultMinListRowHeight, 48)
.onAppear { proxy.scrollTo(Self.upcoming, anchor: .top) }
.onChange(of: player.state.current?.id) { proxy.scrollTo(Self.upcoming, anchor: .top) }
}
}
private var source: String? {
let album = player.current?.albumTitle.trimmingCharacters(in: .whitespaces) ?? ""
return album.isEmpty ? nil : album
}
private func header(_ title: LocalizedStringKey, subtitle: String? = nil, clear: PlaybackCommand?) -> some View {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 3) {
Text(title).font(.system(size: 15, weight: .bold)).foregroundStyle(.primary)
if let subtitle {
Text("From \(subtitle)").font(.system(size: 12)).foregroundStyle(.secondary)
}
}
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
if let clear {
Button("Clear") { player.send(clear) }
.buttonStyle(.plain)
.font(.system(size: 13))
.foregroundStyle(.tint)
}
}
.padding(.top, 14)
.padding(.bottom, 8)
}
private func row(_ item: QueueItem, upcoming: Bool, last: Bool) -> some View {
HStack(spacing: 10) {
ArtworkView(artwork: item.track.artwork, shape: .rounded(4), pixelSize: 128).frame(width: 34, height: 34)
HStack(spacing: 8) {
VStack(alignment: .leading, spacing: 1) {
Text(item.track.title).font(.system(size: 13))
Text([item.track.artistName, item.track.albumTitle].map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }.joined(separator: " — "))
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
Menu { menu(item, upcoming: upcoming) } label: {
Image(systemName: "ellipsis")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.secondary)
.frame(width: 24, height: 24)
.contentShape(.rect)
}
.menuStyle(.button)
.menuIndicator(.hidden)
.buttonStyle(.plain)
.fixedSize()
.accessibilityLabel("More")
}
.frame(maxHeight: .infinity)
.overlay(alignment: .bottom) { Rectangle().fill(.separator).frame(height: 1).opacity(last ? 0 : 1) }
}
.frame(height: 48)
.contentShape(.rect)
.onTapGesture(count: 2) { player.send(.jump(to: item.id)) }
.contextMenu { menu(item, upcoming: upcoming) }
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
}
@ViewBuilder private func menu(_ item: QueueItem, upcoming: Bool) -> some View {
TrackMenu(track: item.track)
if upcoming {
Divider()
Button("Remove from queue", systemImage: "trash", role: .destructive) { player.send(.remove(item.id)) }
}
}
private func move(from offsets: IndexSet, to destination: Int) {
let upcoming = player.state.upcoming
guard let from = offsets.first else { return }
var reordered = upcoming
reordered.move(fromOffsets: offsets, toOffset: destination)
guard let landed = reordered.firstIndex(where: { $0.id == upcoming[from].id }) else { return }
player.send(.move(item: upcoming[from].id, after: landed > 0 ? reordered[landed - 1].id : nil))
}
}
#endif