optimize the appicon of iOS
This commit is contained in:
@@ -12,6 +12,18 @@ struct TestView: View {
|
||||
@State private var testStatus = "请先在训练模块选择一个云端课程"
|
||||
@State private var scoreSummary = "尚未评测"
|
||||
@State private var scoreDetail = ""
|
||||
@State private var dubSegments: [Int: URL] = [:]
|
||||
@State private var latestScores: [Int: ImitationAssessmentResult] = [:]
|
||||
@State private var mergedDubURL: URL?
|
||||
@State private var dubPlayer: AVPlayer?
|
||||
@State private var dubbingPlayback = false
|
||||
@State private var dubbingStatus = "配音:尚未生成"
|
||||
@State private var isGeneratingDub = false
|
||||
@State private var isSharingDub = false
|
||||
@State private var shareURL: URL?
|
||||
@State private var showShareSheet = false
|
||||
@State private var dubPlaybackObserver: NSObjectProtocol?
|
||||
@State private var testSubtitleVisible = true
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
@@ -20,8 +32,15 @@ struct TestView: View {
|
||||
controller: controller,
|
||||
showsContinuousToggle: false,
|
||||
continuousPlayback: .constant(true),
|
||||
showsSubtitle: $testSubtitleVisible,
|
||||
onFullscreen: onFullscreen
|
||||
)
|
||||
if testSubtitleVisible {
|
||||
Text(controller.currentSentence?.text ?? "当前句子")
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(Theme.textDark)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("朗读评测(测试)")
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
@@ -30,6 +49,12 @@ struct TestView: View {
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Theme.accentDeep)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
HStack(spacing: 8) {
|
||||
Toggle("显示字幕", isOn: $testSubtitleVisible)
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(Theme.textDark)
|
||||
.tint(Theme.accent)
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
sentenceButton("上一句") {
|
||||
controller.seekToPreviousSentence()
|
||||
@@ -53,6 +78,21 @@ struct TestView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isAssessing)
|
||||
HStack(spacing: 8) {
|
||||
dubButton("播放配音", enabled: !dubSegments.isEmpty && !dubbingPlayback && !isGeneratingDub) {
|
||||
toggleDubPlayback()
|
||||
}
|
||||
dubButton("生成配音", enabled: !dubSegments.isEmpty && !dubbingPlayback && !isGeneratingDub) {
|
||||
generateDubShareAudio()
|
||||
}
|
||||
dubButton("分享配音", enabled: mergedDubURL != nil && !dubbingPlayback && !isSharingDub) {
|
||||
shareMergedDub()
|
||||
}
|
||||
}
|
||||
Text(dubbingStatus)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Theme.accentDeep)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(scoreSummary)
|
||||
.font(.system(size: 22, weight: .bold))
|
||||
.foregroundColor(Theme.textDark)
|
||||
@@ -78,6 +118,14 @@ struct TestView: View {
|
||||
.onReceive(controller.$currentSentence) { _ in
|
||||
refreshTestUI()
|
||||
}
|
||||
.onDisappear {
|
||||
stopDubPlayback(message: nil)
|
||||
}
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
if let url = shareURL {
|
||||
ActivityShareSheet(items: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sentenceButton(_ label: String, action: @escaping () -> Void) -> some View {
|
||||
@@ -93,6 +141,20 @@ struct TestView: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func dubButton(_ label: String, enabled: Bool, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Text(label)
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 38)
|
||||
.background(enabled ? Theme.buttonBlue : Theme.border)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!enabled)
|
||||
}
|
||||
|
||||
// MARK: - 录音
|
||||
|
||||
private func toggleRecording() {
|
||||
@@ -176,6 +238,16 @@ struct TestView: View {
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
return
|
||||
}
|
||||
guard let sentence = controller.currentSentence else {
|
||||
testStatus = "没有可归属的句子,本次录音已丢弃"
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
return
|
||||
}
|
||||
if let oldURL = dubSegments[sentence.index] {
|
||||
try? FileManager.default.removeItem(at: oldURL)
|
||||
}
|
||||
dubSegments[sentence.index] = url
|
||||
mergedDubURL = nil
|
||||
submitAssessment(recordingURL: url)
|
||||
}
|
||||
|
||||
@@ -269,6 +341,119 @@ struct TestView: View {
|
||||
testStatus = "请先在训练模块选择一个云端课程"
|
||||
return
|
||||
}
|
||||
testStatus = "评测对象:第 \(sentence.index + 1) 句(共 \(item.sentences.count) 句)"
|
||||
let currentRecording = dubSegments[sentence.index]
|
||||
let segmentLabel = currentRecording == nil ? "" : " · 本句已录音,重录将覆盖"
|
||||
testStatus = "评测对象:第 \(sentence.index + 1) 句(共 \(item.sentences.count) 句)\(segmentLabel)"
|
||||
if mergedDubURL == nil {
|
||||
dubbingStatus = dubSegments.isEmpty ? "配音:请先录音" : "配音:已保存 \(dubSegments.count) 句,可生成配音"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 配音
|
||||
|
||||
private func toggleDubPlayback() {
|
||||
if dubbingPlayback {
|
||||
stopDubPlayback(message: "已停止配音播放")
|
||||
return
|
||||
}
|
||||
let sentence = controller.currentSentence
|
||||
let audioURL = mergedDubURL ?? sentence.flatMap { dubSegments[$0.index] }
|
||||
guard let audioURL else { return }
|
||||
if mergedDubURL == nil, let sentence = sentence {
|
||||
controller.seek(to: sentence.startMs)
|
||||
}
|
||||
controller.pause()
|
||||
controller.setVolume(0)
|
||||
let player = dubPlayer ?? AVPlayer()
|
||||
dubPlayer = player
|
||||
player.replaceCurrentItem(with: AVPlayerItem(url: audioURL))
|
||||
dubbingPlayback = true
|
||||
dubbingStatus = mergedDubURL == nil ? "正在播放当前句配音,视频原声静音" : "正在播放整段配音,视频原声静音"
|
||||
player.play()
|
||||
if dubPlaybackObserver == nil {
|
||||
dubPlaybackObserver = NotificationCenter.default.addObserver(
|
||||
forName: AVPlayerItem.didPlayToEndTimeNotification,
|
||||
object: player.currentItem,
|
||||
queue: .main
|
||||
) { _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
stopDubPlayback(message: "配音播放完成")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopDubPlayback(message: String?) {
|
||||
dubPlayer?.pause()
|
||||
dubPlayer?.seek(to: .zero)
|
||||
controller.pause()
|
||||
controller.setVolume(1)
|
||||
dubbingPlayback = false
|
||||
if let dubPlaybackObserver {
|
||||
NotificationCenter.default.removeObserver(dubPlaybackObserver)
|
||||
self.dubPlaybackObserver = nil
|
||||
}
|
||||
if let message {
|
||||
dubbingStatus = message
|
||||
}
|
||||
}
|
||||
|
||||
private func generateDubShareAudio() {
|
||||
guard !dubSegments.isEmpty else { return }
|
||||
let segments = dubSegments.sorted { $0.key < $1.key }
|
||||
let outputURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("dubbing-\(Int(Date().timeIntervalSince1970 * 1000)).m4a")
|
||||
isGeneratingDub = true
|
||||
dubbingStatus = "正在合成 \(segments.count) 句配音…"
|
||||
Task {
|
||||
do {
|
||||
try await DubbingCompositor.merge(urls: segments.map(\.value), outputURL: outputURL)
|
||||
if let oldURL = mergedDubURL {
|
||||
try? FileManager.default.removeItem(at: oldURL)
|
||||
}
|
||||
mergedDubURL = outputURL
|
||||
dubbingStatus = "已合成 \(segments.count) 句配音,可点击“分享配音”"
|
||||
} catch {
|
||||
dubbingStatus = "合成失败:\(error.localizedDescription)"
|
||||
}
|
||||
isGeneratingDub = false
|
||||
}
|
||||
}
|
||||
|
||||
private func shareMergedDub() {
|
||||
guard let item = controller.currentItem, mergedDubURL != nil else { return }
|
||||
guard item.id.range(of: AppConfig.sha256Pattern, options: .regularExpression) != nil else {
|
||||
dubbingStatus = "分享失败:网页分享仅支持云端课程视频"
|
||||
return
|
||||
}
|
||||
isSharingDub = true
|
||||
dubbingStatus = "正在生成分享链接…"
|
||||
let request = DubShareUploadRequest(
|
||||
videoHash: item.id,
|
||||
title: item.title,
|
||||
segments: dubSegments.map { ($0.key, $0.value) },
|
||||
scores: latestScores
|
||||
)
|
||||
Task {
|
||||
do {
|
||||
let link = try await ImitationAssessor.uploadDubShare(request: request)
|
||||
shareURL = URL(string: link)
|
||||
showShareSheet = true
|
||||
dubbingStatus = "已生成分享链接:\(link)"
|
||||
} catch {
|
||||
dubbingStatus = "分享失败:\(error.localizedDescription)"
|
||||
}
|
||||
isSharingDub = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ActivityShareSheet: UIViewControllerRepresentable {
|
||||
let items: [Any]
|
||||
|
||||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||
UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user