242 lines
7.5 KiB
Swift
242 lines
7.5 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|