optimize the appicon of iOS

This commit is contained in:
2026-08-26 17:14:59 +08:00
parent 76b0332035
commit bb677ea8cf
11 changed files with 406 additions and 11 deletions

View File

@@ -5,6 +5,7 @@ struct PlayerSectionView: View {
@ObservedObject var controller: PlayerController
var showsContinuousToggle: Bool
@Binding var continuousPlayback: Bool
var showsSubtitle: Binding<Bool>? = .constant(true)
var onFullscreen: () -> Void
@State private var progressDragging = false
@@ -13,7 +14,9 @@ struct PlayerSectionView: View {
var body: some View {
VStack(spacing: 0) {
playerSurface
sentencePanel
if showsSubtitle?.wrappedValue == true {
sentencePanel
}
}
.padding(.horizontal, 16)
}

View File

@@ -131,13 +131,25 @@ struct RootView: View {
private func enterFullscreen() {
AppDelegate.orientationLock = UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape
UIViewController.attemptRotationToDeviceOrientation()
updateSupportedInterfaceOrientations()
showFullscreenPlayer = true
}
private func exitFullscreen() {
AppDelegate.orientationLock = UIDevice.current.userInterfaceIdiom == .pad ? .all : .portrait
UIViewController.attemptRotationToDeviceOrientation()
updateSupportedInterfaceOrientations()
showFullscreenPlayer = false
}
private func updateSupportedInterfaceOrientations() {
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
let controllers = scenes.flatMap { $0.windows }.compactMap(\.rootViewController)
for controller in controllers {
if #available(iOS 16.0, *) {
controller.setNeedsUpdateOfSupportedInterfaceOrientations()
} else {
UIViewController.attemptRotationToDeviceOrientation()
}
}
}
}

View File

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