import SwiftUI /// 播放区 + 悬浮信息 + 进度/句面板。训练与全屏横屏共用。 struct PlayerSectionView: View { @ObservedObject var controller: PlayerController var showsContinuousToggle: Bool @Binding var continuousPlayback: Bool var showsSubtitle: Binding? = .constant(true) var onFullscreen: () -> Void @State private var progressDragging = false @State private var dragPositionMs: Int64 = 0 var body: some View { VStack(spacing: 0) { playerSurface if showsSubtitle?.wrappedValue == true { sentencePanel } } .padding(.horizontal, 16) } private var playerSurface: some View { ZStack { PlayerSurfaceView(controller: controller) .aspectRatio(16.0 / 9.0, contentMode: .fit) .frame(maxWidth: .infinity) VStack { HStack(spacing: 8) { overlayPill(timeText) Spacer() Button(action: cycleSpeed) { overlayPill(Theme.formatSpeed(controller.snapshot.speed)) } .buttonStyle(.plain) } Spacer() } .padding(12) VStack { Spacer() HStack { Spacer() Button(action: onFullscreen) { Image(systemName: "arrow.up.left.and.arrow.down.right") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) .frame(width: 44, height: 44) .background(Theme.overlay) .clipShape(Circle()) } .buttonStyle(.plain) } } .padding(12) } .background(Color.black) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Theme.border, lineWidth: 1) ) .padding(.bottom, 10) } private var sentencePanel: some View { VStack(alignment: .leading, spacing: 6) { Text(controller.currentItem?.title ?? "未选择课程") .font(.system(size: 17, weight: .bold)) .foregroundColor(.white) .lineLimit(1) if showsContinuousToggle { HStack(spacing: 0) { Text("连续播放") .font(.system(size: 13, weight: .bold)) .foregroundColor(Theme.textSubtle) Spacer() Toggle("", isOn: $continuousPlayback) .labelsHidden() .tint(Theme.accent) } .padding(.vertical, 2) } Text(sentenceMetaText) .font(.system(size: 13)) .foregroundColor(Theme.textSubtle) Text(sentenceTextText) .font(.system(size: 15)) .foregroundColor(.white) .lineLimit(2) progressRow Text(statusText) .font(.system(size: 13)) .foregroundColor(Theme.textSubtle) } .padding(.bottom, 10) } private var progressRow: some View { let duration = controller.snapshot.durationMs return HStack(spacing: 8) { 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.accent) } } private var progressBinding: Binding { Binding( get: { if progressDragging { return Double(dragPositionMs) } return Double(controller.snapshot.positionMs) }, set: { newValue in dragPositionMs = Int64(newValue) } ) } private var timeText: String { let snapshot = controller.snapshot return "\(Theme.formatTime(snapshot.positionMs)) / \(Theme.formatTime(snapshot.durationMs))" } 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): if isLocal { return "正在匹配服务端句子边界" } return 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)) } private func cycleSpeed() { let current = controller.snapshot.speed let cycle: [Float] = [1.0, 1.25, 1.5, 1.75, 2.0] let next = cycle.first { $0 > current + 0.001 } ?? cycle.first ?? 1.0 controller.setPlaybackSpeed(next) } }