159 lines
5.3 KiB
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
|
|
}
|
|
}
|