import SwiftUI import AVFoundation struct PlayerView: View { @EnvironmentObject var engine: PlayerEngine @State private var showControls = true @State private var isSeeking = false @State private var controlsTimer: Timer? @FocusState private var focused: Bool var body: some View { ZStack { Color.black if engine.currentItem != nil { PlayerLayerView(player: engine.player) .onTapGesture { toggleControls() } if showControls { controlsOverlay .transition(.opacity) } } else { emptyState } } .ignoresSafeArea(.all, edges: engine.isFullscreen ? .all : []) .onAppear { focused = true resetControlsTimer() } } // MARK: - 空状态 private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "play.rectangle.on.rectangle") .font(.system(size: 64)) .foregroundStyle(.secondary) Text("添加文件开始播放") .font(.title3) .foregroundStyle(.secondary) Text("支持 MP4, MKV, AVI, M4V, MOV, MP3, FLAC, M3U8 等格式") .font(.caption) .foregroundStyle(.tertiary) } } // MARK: - 控制层 private var controlsOverlay: some View { VStack { // 顶部栏: 标题 + 全屏按钮 HStack { if let item = engine.currentItem { Text(item.name) .font(.headline) .lineLimit(1) .padding(.horizontal, 12) .padding(.vertical, 6) .background(.ultraThinMaterial, in: Capsule()) } Spacer() Button { engine.toggleFullscreen() } label: { Image(systemName: engine.isFullscreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right") .font(.title3) .padding(10) .background(.ultraThinMaterial, in: Circle()) } .buttonStyle(.plain) } .padding(.horizontal, 20) .padding(.top, 12) Spacer() // 底部控制区 VStack(spacing: 12) { // 进度条 progressSection // 按钮行 buttonRow } .padding(.horizontal, 20) .padding(.bottom, 16) .padding(.vertical, 12) .background( LinearGradient( colors: [.clear, .black.opacity(0.6)], startPoint: .top, endPoint: .bottom ) ) } } // MARK: - 进度条 private var progressSection: some View { VStack(spacing: 4) { Slider( value: Binding( get: { isSeeking ? engine.currentTime : engine.currentTime }, set: { newValue in engine.currentTime = newValue isSeeking = true } ), in: 0...max(engine.duration, 0.1) ) { editing in if !editing { engine.seek(to: engine.currentTime) isSeeking = false } } #if os(iOS) .tint(.white) #else .tint(.accentColor) #endif HStack { Text(PlayerEngine.formatTime(engine.currentTime)) .font(.caption.monospacedDigit()) Spacer() Text(PlayerEngine.formatTime(engine.duration)) .font(.caption.monospacedDigit()) } .foregroundStyle(.white.opacity(0.8)) } } // MARK: - 按钮行 private var buttonRow: some View { HStack(spacing: 24) { // 音轨 Menu { ForEach(Array(engine.audioTracks.enumerated()), id: \.offset) { idx, track in Button { engine.selectAudioTrack(track) } label: { HStack { Text(track.displayName.isEmpty ? "音轨 \(idx + 1)" : track.displayName) if track == engine.selectedAudioTrack { Image(systemName: "checkmark") } } } } } label: { Image(systemName: "waveform") .font(.title3) } .buttonStyle(.plain) .disabled(engine.audioTracks.count <= 1) Spacer() // 上一个 Button { engine.playPrevious() } label: { Image(systemName: "backward.fill") .font(.title2) } .buttonStyle(.plain) // 快退 Button { engine.seekBackward() } label: { Image(systemName: "gobackward.10") .font(.title2) } .buttonStyle(.plain) // 播放/暂停 Button { engine.togglePlay() } label: { Image(systemName: engine.isPlaying ? "pause.circle.fill" : "play.circle.fill") .font(.system(size: 44)) } .buttonStyle(.plain) // 快进 Button { engine.seekForward() } label: { Image(systemName: "goforward.10") .font(.title2) } .buttonStyle(.plain) // 下一个 Button { engine.playNext() } label: { Image(systemName: "forward.fill") .font(.title2) } .buttonStyle(.plain) Spacer() // 循环 Button { engine.repeatMode = engine.repeatMode.next } label: { Image(systemName: engine.repeatMode.icon) .font(.title3) .overlay(alignment: .bottom) { if engine.repeatMode != .none { Circle() .fill(.blue) .frame(width: 4, height: 4) .offset(y: -4) } } } .buttonStyle(.plain) } .foregroundStyle(.white) } // MARK: - 辅助 private func toggleControls() { withAnimation(.easeInOut(duration: 0.2)) { showControls.toggle() } resetControlsTimer() } private func resetControlsTimer() { controlsTimer?.invalidate() controlsTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: false) { _ in Task { @MainActor in withAnimation(.easeInOut(duration: 0.3)) { showControls = false } } } } }