P0 关键修复: - 移除 exit(0) 强制退出,改为应用语言设置后下次启动生效 - 修复 LivePhotoValidator hasResumed data race,引入线程安全 ResumeOnce - 修复 addAssetID(toVideo:) continuation 泄漏,添加 writer/reader 启动状态检查 - 修复 OnboardingView "跳过" 按钮未国际化 - 修复 LanguageManager "跟随系统" 硬编码中文 - .gitignore 补全 AI 工具目录 P1 架构与 UI 修复: - 修复 RealESRGANProcessor actor 隔离违规 - 修复 ODRManager continuation 生命周期保护 - TiledImageProcessor 改为流式拼接,降低内存峰值 - EditorView 硬编码颜色统一为设计系统 - ProcessingView 取消导航竞态修复 - 反馈诊断包添加知情同意提示 P2 代码质量与合规: - EditorView/WallpaperGuideView 硬编码间距圆角统一为设计令牌 - PrivacyPolicyView 设计系统颜色统一 - HomeView 重复 onChange 合并 - PHAuthorizationStatus 改为英文技术术语 - Analytics 日志 assetId 脱敏 - 隐私政策补充 localIdentifier 存储说明 - 清理孤立的 subscription 翻译 key - 脚本硬编码绝对路径改为相对路径 - DesignSystem SoftSlider 类型不匹配编译错误修复 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
184 lines
5.9 KiB
Swift
184 lines
5.9 KiB
Swift
//
|
||
// AppState.swift
|
||
// to-live-photo
|
||
//
|
||
// App 全局状态管理 + 页面导航状态机
|
||
//
|
||
|
||
import SwiftUI
|
||
import PhotosUI
|
||
import LivePhotoCore
|
||
|
||
enum AppRoute: Hashable {
|
||
case home
|
||
case editor(videoURL: URL)
|
||
case processing(videoURL: URL, exportParams: ExportParams)
|
||
case result(workflowResult: LivePhotoWorkflowResult)
|
||
case wallpaperGuide(assetId: String)
|
||
case settings
|
||
}
|
||
|
||
@MainActor
|
||
@Observable
|
||
final class AppState {
|
||
var navigationPath = NavigationPath()
|
||
|
||
var processingProgress: LivePhotoBuildProgress?
|
||
var processingError: AppError?
|
||
var isProcessing = false
|
||
var isCancelling = false
|
||
|
||
/// 当前处理中的导出参数(用于保存最近作品时记录)
|
||
private var currentExportParams: ExportParams?
|
||
|
||
private var workflow: LivePhotoWorkflow?
|
||
private var currentProcessingTask: Task<LivePhotoWorkflowResult?, Never>?
|
||
private var currentWorkId: UUID?
|
||
|
||
init() {
|
||
do {
|
||
workflow = try LivePhotoWorkflow()
|
||
} catch {
|
||
#if DEBUG
|
||
print("Failed to init LivePhotoWorkflow: \(error)")
|
||
#endif
|
||
}
|
||
}
|
||
|
||
func navigateTo(_ route: AppRoute) {
|
||
navigationPath.append(route)
|
||
}
|
||
|
||
func popToRoot() {
|
||
navigationPath = NavigationPath()
|
||
}
|
||
|
||
func pop() {
|
||
if !navigationPath.isEmpty {
|
||
navigationPath.removeLast()
|
||
}
|
||
}
|
||
|
||
func cancelProcessing() {
|
||
guard isProcessing, !isCancelling else { return }
|
||
isCancelling = true
|
||
|
||
// 取消任务
|
||
currentProcessingTask?.cancel()
|
||
|
||
// 清理中间文件
|
||
if let workId = currentWorkId, let workflow {
|
||
Task {
|
||
await workflow.cleanupWork(workId: workId)
|
||
await MainActor.run {
|
||
self.isProcessing = false
|
||
self.isCancelling = false
|
||
self.currentWorkId = nil
|
||
self.currentProcessingTask = nil
|
||
self.processingProgress = nil
|
||
self.pop()
|
||
}
|
||
}
|
||
} else {
|
||
isProcessing = false
|
||
isCancelling = false
|
||
currentWorkId = nil
|
||
currentProcessingTask = nil
|
||
processingProgress = nil
|
||
pop()
|
||
}
|
||
|
||
Analytics.shared.log(.buildLivePhotoCancel)
|
||
}
|
||
|
||
func startProcessing(videoURL: URL, exportParams: ExportParams) async -> LivePhotoWorkflowResult? {
|
||
guard let workflow else {
|
||
processingError = AppError(code: "LPB-001", message: "初始化失败", suggestedActions: ["重启 App"])
|
||
return nil
|
||
}
|
||
|
||
isProcessing = true
|
||
isCancelling = false
|
||
processingProgress = nil
|
||
processingError = nil
|
||
currentExportParams = exportParams
|
||
|
||
let workId = UUID()
|
||
currentWorkId = workId
|
||
|
||
Analytics.shared.log(.buildLivePhotoStart)
|
||
|
||
let task = Task<LivePhotoWorkflowResult?, Never> {
|
||
do {
|
||
// 检查是否已取消
|
||
try Task.checkCancellation()
|
||
|
||
let state = self
|
||
let result = try await workflow.buildSaveValidate(
|
||
workId: workId,
|
||
sourceVideoURL: videoURL,
|
||
coverImageURL: nil,
|
||
exportParams: exportParams
|
||
) { progress in
|
||
Task { @MainActor in
|
||
state.processingProgress = progress
|
||
}
|
||
}
|
||
|
||
// 再次检查是否已取消
|
||
try Task.checkCancellation()
|
||
|
||
await MainActor.run {
|
||
state.isProcessing = false
|
||
state.currentWorkId = nil
|
||
state.currentProcessingTask = nil
|
||
|
||
// 保存到最近作品列表
|
||
if let params = state.currentExportParams {
|
||
RecentWorksManager.shared.addWork(
|
||
assetId: result.savedAssetId,
|
||
aspectRatio: params.aspectRatio.rawValue,
|
||
compatibilityMode: params.compatibilityMode
|
||
)
|
||
}
|
||
state.currentExportParams = nil
|
||
}
|
||
Analytics.shared.log(.buildLivePhotoSuccess)
|
||
Analytics.shared.log(.saveAlbumSuccess, parameters: ["assetId": String(result.savedAssetId.prefix(8)) + "..."])
|
||
return result
|
||
} catch is CancellationError {
|
||
// 任务被取消,不需要额外处理
|
||
return nil
|
||
} catch let error as AppError {
|
||
await MainActor.run {
|
||
self.isProcessing = false
|
||
self.processingError = error
|
||
self.currentWorkId = nil
|
||
self.currentProcessingTask = nil
|
||
}
|
||
Analytics.shared.log(.buildLivePhotoFail, parameters: [
|
||
"code": error.code,
|
||
"stage": error.stage?.rawValue ?? "unknown",
|
||
"message": error.message
|
||
])
|
||
if error.stage == .saveToAlbum {
|
||
Analytics.shared.log(.saveAlbumFail, parameters: ["code": error.code])
|
||
}
|
||
return nil
|
||
} catch {
|
||
await MainActor.run {
|
||
self.isProcessing = false
|
||
self.processingError = AppError(code: "LPB-901", message: "未知错误", underlyingErrorDescription: error.localizedDescription, suggestedActions: ["重试"])
|
||
self.currentWorkId = nil
|
||
self.currentProcessingTask = nil
|
||
}
|
||
Analytics.shared.logError(.buildLivePhotoFail, error: error)
|
||
return nil
|
||
}
|
||
}
|
||
|
||
currentProcessingTask = task
|
||
return await task.value
|
||
}
|
||
}
|