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 } }