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 { 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)) } }