Files
mediaplayer/ios/OralTrainer/Views/FullscreenPlayerView.swift
2026-08-31 13:37:38 +08:00

172 lines
6.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}
}