added ios module
This commit is contained in:
136
ios/OralTrainer/Views/FullscreenPlayerView.swift
Normal file
136
ios/OralTrainer/Views/FullscreenPlayerView.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 与安卓端横屏布局一致:全屏播放 + 半透明句面板 + 退出全屏按钮。
|
||||
struct FullscreenPlayerView: View {
|
||||
@ObservedObject var controller: PlayerController
|
||||
let onExit: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
PlayerSurfaceView(controller: controller)
|
||||
.ignoresSafeArea()
|
||||
VStack {
|
||||
Spacer()
|
||||
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.overlay)
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user