add chinese subtitles
This commit is contained in:
@@ -4,6 +4,8 @@ import SwiftUI
|
||||
struct FullscreenPlayerView: View {
|
||||
@ObservedObject var controller: PlayerController
|
||||
var showsLessonTitle: Bool = true
|
||||
var showChineseSubtitles: Bool = false
|
||||
var chineseSubtitlePlaceholder: String = "正在翻译中文字幕…"
|
||||
let onExit: () -> Void
|
||||
|
||||
@State private var subtitleVisible = true
|
||||
@@ -66,10 +68,18 @@ struct FullscreenPlayerView: View {
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Theme.textOverlay)
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
Text(sentenceTextText)
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sentenceTextText)
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
if let chineseSubtitleText = chineseSubtitleText {
|
||||
Text(chineseSubtitleText)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Theme.textOverlay)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
guard let sentence = controller.currentSentence,
|
||||
@@ -203,6 +213,20 @@ struct FullscreenPlayerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var chineseSubtitleText: String? {
|
||||
guard showChineseSubtitles else {
|
||||
return nil
|
||||
}
|
||||
if let zh = controller.currentSentence?.zhText?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!zh.isEmpty {
|
||||
return zh
|
||||
}
|
||||
if (controller.currentSentence?.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return nil
|
||||
}
|
||||
return chineseSubtitlePlaceholder
|
||||
}
|
||||
|
||||
private var statusText: String {
|
||||
if let message = controller.cacheState.statusMessage {
|
||||
return message
|
||||
|
||||
@@ -7,6 +7,8 @@ struct PlayerSectionView: View {
|
||||
@Binding var continuousPlayback: Bool
|
||||
var showsSubtitle: Binding<Bool>? = .constant(true)
|
||||
var showsLessonTitle: Bool = true
|
||||
var showChineseSubtitles: Binding<Bool>? = nil
|
||||
var chineseSubtitlePlaceholder: String = "正在翻译中文字幕…"
|
||||
var onFullscreen: () -> Void
|
||||
|
||||
@State private var progressDragging = false
|
||||
@@ -93,14 +95,34 @@ struct PlayerSectionView: View {
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
if let showChineseSubtitles = showChineseSubtitles {
|
||||
HStack(spacing: 0) {
|
||||
Text("中文字幕")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(Theme.textSubtle)
|
||||
Spacer()
|
||||
Toggle("", isOn: showChineseSubtitles)
|
||||
.labelsHidden()
|
||||
.tint(Theme.accent)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
Text(sentenceMetaText)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Theme.textSubtle)
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
Text(sentenceTextText)
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(4)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sentenceTextText)
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(4)
|
||||
if let chineseSubtitleText = chineseSubtitleText {
|
||||
Text(chineseSubtitleText)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Theme.textMuted)
|
||||
.lineLimit(4)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
guard let sentence = controller.currentSentence, !(sentence.text ?? "").isEmpty else {
|
||||
@@ -205,6 +227,20 @@ struct PlayerSectionView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var chineseSubtitleText: String? {
|
||||
guard showChineseSubtitles?.wrappedValue == true else {
|
||||
return nil
|
||||
}
|
||||
if let zh = controller.currentSentence?.zhText?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!zh.isEmpty {
|
||||
return zh
|
||||
}
|
||||
if (controller.currentSentence?.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return nil
|
||||
}
|
||||
return chineseSubtitlePlaceholder
|
||||
}
|
||||
|
||||
private var statusText: String {
|
||||
if let message = controller.cacheState.statusMessage {
|
||||
return message
|
||||
|
||||
@@ -9,6 +9,10 @@ struct RootView: View {
|
||||
@StateObject private var historyManager = TrainingHistoryManager()
|
||||
@State private var activeModule: Module = .course
|
||||
@State private var continuousPlayback = false
|
||||
@State private var showChineseSubtitles = false
|
||||
@State private var chineseTranslationLoading = false
|
||||
@State private var chineseTranslationError = ""
|
||||
@State private var chineseTranslationTask: Task<Void, Never>?
|
||||
@State private var showImporter = false
|
||||
@State private var showFullscreenPlayer = false
|
||||
@State private var courses: [Course] = []
|
||||
@@ -53,6 +57,8 @@ struct RootView: View {
|
||||
controller: controller,
|
||||
catalogVM: catalogVM,
|
||||
continuousPlayback: $continuousPlayback,
|
||||
showChineseSubtitles: $showChineseSubtitles,
|
||||
chineseSubtitlePlaceholder: chineseSubtitlePlaceholder,
|
||||
onFullscreen: enterFullscreen
|
||||
)
|
||||
case .test:
|
||||
@@ -88,9 +94,24 @@ struct RootView: View {
|
||||
Task { await loadCourses() }
|
||||
}
|
||||
}
|
||||
.onChange(of: showChineseSubtitles) { enabled in
|
||||
if enabled {
|
||||
ensureChineseTranslations()
|
||||
} else {
|
||||
chineseTranslationTask?.cancel()
|
||||
chineseTranslationLoading = false
|
||||
}
|
||||
}
|
||||
.onChange(of: controller.currentItem?.id) { _ in
|
||||
ensureChineseTranslations()
|
||||
}
|
||||
.onChange(of: controller.currentItem?.sentences.count) { _ in
|
||||
ensureChineseTranslations()
|
||||
}
|
||||
.onChange(of: activeModule) { module in
|
||||
if module == .train {
|
||||
controller.setContinuousPlayback(continuousPlayback)
|
||||
ensureChineseTranslations()
|
||||
} else if module == .test {
|
||||
// 与安卓测试页一致:按当前句窗口播放,避免滑进下一句。
|
||||
controller.setContinuousPlayback(false)
|
||||
@@ -115,7 +136,9 @@ struct RootView: View {
|
||||
.fullScreenCover(isPresented: $showFullscreenPlayer) {
|
||||
FullscreenPlayerView(
|
||||
controller: controller,
|
||||
showsLessonTitle: activeModule != .train
|
||||
showsLessonTitle: activeModule != .train,
|
||||
showChineseSubtitles: showChineseSubtitles,
|
||||
chineseSubtitlePlaceholder: chineseSubtitlePlaceholder
|
||||
) {
|
||||
exitFullscreen()
|
||||
}
|
||||
@@ -593,4 +616,59 @@ struct RootView: View {
|
||||
AppDelegate.lockOrientation(restored)
|
||||
showFullscreenPlayer = false
|
||||
}
|
||||
|
||||
private var chineseSubtitlePlaceholder: String {
|
||||
if chineseTranslationLoading {
|
||||
return "正在翻译中文字幕…"
|
||||
}
|
||||
if !chineseTranslationError.isEmpty {
|
||||
return chineseTranslationError
|
||||
}
|
||||
return "暂无中文字幕"
|
||||
}
|
||||
|
||||
private func ensureChineseTranslations() {
|
||||
guard showChineseSubtitles, activeModule == .train else {
|
||||
return
|
||||
}
|
||||
guard let item = controller.currentItem else {
|
||||
return
|
||||
}
|
||||
let needsTranslation = item.sentences.contains {
|
||||
!($0.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
($0.zhText ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
guard needsTranslation else {
|
||||
chineseTranslationLoading = false
|
||||
chineseTranslationError = ""
|
||||
return
|
||||
}
|
||||
chineseTranslationTask?.cancel()
|
||||
chineseTranslationLoading = true
|
||||
chineseTranslationError = ""
|
||||
let mediaId = item.id
|
||||
chineseTranslationTask = Task {
|
||||
do {
|
||||
let map = try await SentenceBoundaryAPI.translateSubtitles(videoHash: mediaId)
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run {
|
||||
guard controller.currentItem?.id == mediaId else { return }
|
||||
let updated = (controller.currentItem?.sentences ?? []).map { sentence in
|
||||
sentence.withZhText(map[sentence.index] ?? sentence.zhText)
|
||||
}
|
||||
controller.updateBoundaries(updated)
|
||||
chineseTranslationLoading = false
|
||||
chineseTranslationError = ""
|
||||
controller.showTransientMessage("已加载中文字幕")
|
||||
}
|
||||
} catch {
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run {
|
||||
chineseTranslationLoading = false
|
||||
chineseTranslationError = "中文字幕翻译失败"
|
||||
controller.showTransientMessage("中文字幕翻译失败:\(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ struct TrainingView: View {
|
||||
@ObservedObject var controller: PlayerController
|
||||
@ObservedObject var catalogVM: CatalogViewModel
|
||||
@Binding var continuousPlayback: Bool
|
||||
@Binding var showChineseSubtitles: Bool
|
||||
var chineseSubtitlePlaceholder: String = "正在翻译中文字幕…"
|
||||
let onFullscreen: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@@ -14,6 +16,8 @@ struct TrainingView: View {
|
||||
showsContinuousToggle: true,
|
||||
continuousPlayback: $continuousPlayback,
|
||||
showsLessonTitle: false,
|
||||
showChineseSubtitles: $showChineseSubtitles,
|
||||
chineseSubtitlePlaceholder: chineseSubtitlePlaceholder,
|
||||
onFullscreen: onFullscreen
|
||||
)
|
||||
CatalogView(vm: catalogVM)
|
||||
@@ -28,5 +32,8 @@ struct TrainingView: View {
|
||||
: "已关闭连续播放,将循环播放当前句子"
|
||||
)
|
||||
}
|
||||
.onChange(of: showChineseSubtitles) { enabled in
|
||||
controller.showTransientMessage(enabled ? "已开启中文字幕" : "已关闭中文字幕")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user