172 lines
6.3 KiB
Swift
172 lines
6.3 KiB
Swift
import SwiftUI
|
||
|
||
/// 与安卓端横屏布局一致:全屏播放 + 半透明句面板 + 退出全屏按钮。
|
||
struct FullscreenPlayerView: View {
|
||
@ObservedObject var controller: PlayerController
|
||
let onExit: () -> Void
|
||
|
||
@State private var subtitleVisible = true
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
Color.black.ignoresSafeArea()
|
||
PlayerSurfaceView(controller: controller) { isSwipeUp in
|
||
guard subtitleVisible != isSwipeUp else {
|
||
return
|
||
}
|
||
withAnimation(.easeInOut(duration: 0.22)) {
|
||
subtitleVisible = isSwipeUp
|
||
}
|
||
}
|
||
.ignoresSafeArea()
|
||
HStack(spacing: 8) {
|
||
overlayPill(timeText)
|
||
Spacer()
|
||
overlayPill(Theme.formatSpeed(controller.snapshot.speed))
|
||
}
|
||
.padding(12)
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||
.allowsHitTesting(false)
|
||
VStack {
|
||
Spacer()
|
||
if subtitleVisible {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
Text(controller.currentItem?.title ?? "未选择课程")
|
||
.font(.system(size: 17, weight: .bold))
|
||
.foregroundColor(.white)
|
||
.lineLimit(1)
|
||
Text(sentenceMetaText)
|
||
.font(.system(size: 13))
|
||
.foregroundColor(Theme.textOverlay)
|
||
Text(sentenceTextText)
|
||
.font(.system(size: 15))
|
||
.foregroundColor(.white)
|
||
.lineLimit(2)
|
||
progressRow
|
||
Text(statusText)
|
||
.font(.system(size: 13))
|
||
.foregroundColor(Theme.textOverlay)
|
||
}
|
||
.padding(18)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.background(Theme.overlayFaint)
|
||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||
}
|
||
}
|
||
VStack {
|
||
Spacer()
|
||
HStack {
|
||
Spacer()
|
||
Button(action: onExit) {
|
||
Image(systemName: "arrow.down.right.and.arrow.up.left")
|
||
.font(.system(size: 18, weight: .bold))
|
||
.foregroundColor(.white)
|
||
.frame(width: 44, height: 44)
|
||
.background(Theme.overlay)
|
||
.clipShape(Circle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
.padding(16)
|
||
}
|
||
}
|
||
|
||
@State private var progressDragging = false
|
||
@State private var dragPositionMs: Int64 = 0
|
||
|
||
private var timeText: String {
|
||
let snapshot = controller.snapshot
|
||
return "\(Theme.formatTime(snapshot.positionMs)) / \(Theme.formatTime(snapshot.durationMs))"
|
||
}
|
||
|
||
private var progressRow: some View {
|
||
let duration = controller.snapshot.durationMs
|
||
return HStack(spacing: 8) {
|
||
Text(Theme.formatTime(controller.snapshot.positionMs))
|
||
.font(.system(size: 13))
|
||
.foregroundColor(Theme.textOverlay)
|
||
Slider(
|
||
value: progressBinding,
|
||
in: 0...max(1, Double(duration)),
|
||
onEditingChanged: { editing in
|
||
if editing {
|
||
progressDragging = true
|
||
} else {
|
||
progressDragging = false
|
||
controller.seek(to: dragPositionMs)
|
||
}
|
||
}
|
||
)
|
||
.tint(Theme.accentLight)
|
||
Text(Theme.formatTime(controller.snapshot.durationMs))
|
||
.font(.system(size: 13))
|
||
.foregroundColor(Theme.textOverlay)
|
||
}
|
||
}
|
||
|
||
private var progressBinding: Binding<Double> {
|
||
Binding(
|
||
get: {
|
||
if progressDragging {
|
||
return Double(dragPositionMs)
|
||
}
|
||
return Double(controller.snapshot.positionMs)
|
||
},
|
||
set: { newValue in
|
||
dragPositionMs = Int64(newValue)
|
||
}
|
||
)
|
||
}
|
||
|
||
private var sentenceMetaText: String {
|
||
switch controller.boundaryState {
|
||
case .loading(let isLocal):
|
||
return isLocal ? "句子边界分析中" : "句子边界加载中"
|
||
case .failed:
|
||
return "暂无句子边界"
|
||
case .idle, .loaded:
|
||
guard let sentence = controller.currentSentence else {
|
||
return "暂无句子边界"
|
||
}
|
||
let count = controller.currentItem?.sentences.count ?? 0
|
||
let countText = count > 0 ? " / \(count)" : ""
|
||
return "第 \(sentence.index + 1)\(countText) 句 \(Theme.formatTime(sentence.startMs))-\(Theme.formatTime(sentence.endMs))"
|
||
}
|
||
}
|
||
|
||
private var sentenceTextText: String {
|
||
switch controller.boundaryState {
|
||
case .loading(let isLocal):
|
||
return isLocal ? "正在匹配服务端句子边界" : (controller.currentItem?.title ?? "加载中")
|
||
case .failed:
|
||
return controller.currentItem?.title ?? "未选择课程"
|
||
case .idle, .loaded:
|
||
guard let sentence = controller.currentSentence else {
|
||
return controller.currentItem?.title ?? "未选择课程"
|
||
}
|
||
if let text = sentence.text, !text.isEmpty {
|
||
return text
|
||
}
|
||
return "当前句子"
|
||
}
|
||
}
|
||
|
||
private var statusText: String {
|
||
if let message = controller.transientMessage {
|
||
return message
|
||
}
|
||
return controller.playbackStatusText(controller.snapshot)
|
||
}
|
||
|
||
private func overlayPill(_ text: String) -> some View {
|
||
Text(text)
|
||
.font(.system(size: 13, weight: .bold))
|
||
.foregroundColor(.white)
|
||
.padding(.horizontal, 10)
|
||
.padding(.vertical, 6)
|
||
.background(Theme.overlay)
|
||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||
}
|
||
}
|