MiniPlayer/Sources/PlaylistView.swift

159 lines
5.3 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
struct PlaylistView: View {
@EnvironmentObject var engine: PlayerEngine
@State private var showFileImporter = false
var body: some View {
VStack(spacing: 0) {
//
HStack {
Text("播放列表")
.font(.headline)
Spacer()
Text("\(engine.queue.count)")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(.quaternary, in: Capsule())
Button {
showFileImporter = true
} label: {
Image(systemName: "plus.circle.fill")
.font(.title3)
}
.buttonStyle(.plain)
if !engine.queue.isEmpty {
Button {
engine.queue.removeAll()
engine.currentIndex = -1
engine.pause()
engine.player.replaceCurrentItem(with: nil)
} label: {
Image(systemName: "trash")
.font(.caption)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
Divider()
//
if engine.queue.isEmpty {
Spacer()
VStack(spacing: 8) {
Image(systemName: "music.note.list")
.font(.largeTitle)
.foregroundStyle(.tertiary)
Text("列表为空")
.font(.subheadline)
.foregroundStyle(.tertiary)
Button("添加文件") {
showFileImporter = true
}
.font(.caption)
}
Spacer()
} else {
List {
ForEach(Array(engine.queue.enumerated()), id: \.element.id) { idx, item in
playlistRow(item: item, index: idx)
.contentShape(Rectangle())
.onTapGesture {
engine.play(index: idx)
}
}
.onDelete { offsets in
engine.remove(at: offsets)
}
.onMove { source, destination in
engine.move(from: source, to: destination)
}
}
.listStyle(.plain)
}
}
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: supportedTypes,
allowsMultipleSelection: true
) { result in
if case .success(let urls) = result {
engine.addToQueue(urls: urls)
}
}
}
// MARK: -
@ViewBuilder
private func playlistRow(item: MediaItem, index: Int) -> some View {
let isCurrent = index == engine.currentIndex
HStack(spacing: 10) {
// /
ZStack {
if isCurrent {
Image(systemName: engine.isPlaying ? "waveform" : "pause.fill")
.font(.caption)
.foregroundStyle(.blue)
.symbolEffect(.variableColor.iterative, isActive: engine.isPlaying && isCurrent)
} else {
Text("\(index + 1)")
.font(.caption.monospacedDigit())
.foregroundStyle(.tertiary)
}
}
.frame(width: 20)
//
Image(systemName: iconName(for: item.type))
.font(.caption)
.foregroundStyle(isCurrent ? .blue : .secondary)
//
Text(item.name)
.font(.system(size: 13))
.lineLimit(1)
.foregroundStyle(isCurrent ? .primary : .secondary)
Spacer()
}
.padding(.vertical, 4)
.listRowBackground(isCurrent ? Color.accentColor.opacity(0.1) : Color.clear)
}
private func iconName(for type: MediaType) -> String {
switch type {
case .video: return "film"
case .audio: return "music.note"
case .stream: return "dot.radiowaves.left.and.right"
}
}
//
private var supportedTypes: [UTType] {
var types: [UTType] = [
.movie, .video, .audio, .mpeg4Movie, .quickTimeMovie, .mp3,
.mpeg4Audio, .avi, .url
]
//
for ext in ["mkv", "flac", "wav", "ogg", "m3u8", "m3u", "ts", "mov", "webm"] {
if let uttype = UTType(filenameExtension: ext) {
types.append(uttype)
}
}
return types
}
}