fix: 代码审查 P0+P1 问题修复(2 个崩溃 + 14 个体验问题)

P0 致命问题:
- LivePhotoCore: requestLivePhoto continuation 多次 resume 崩溃
  使用 ResumeOnce + highQualityFormat + 跳过降级版本
- ResultView: PHLivePhoto.request continuation 泄漏/多次 resume
  NSLock + hasResumed 守卫 + 处理 cancel/error 边缘情况

P1 并发安全:
- PresetManager/RecentWorksManager: 移除 NotificationCenter 闭包
  中的 guard let self(Swift 6 并发安全)
- RecentWorksManager: cleanupDeletedAssets 移到 Task.detached
- RecentWorksManager: ThumbnailLoader.load 移到后台 + 防重复

P1 EditorView 修复:
- AI 百分比改用 .percent FormatStyle(本地化)
- 预设选择器 if-else 互斥 + Group 包裹
- 诊断按钮优先使用 suggestion.action 闭包
- PhotosPicker 导入添加 50MB 大小限制
- 封面文件写入 try? 改为 do-catch

P1 其他修复:
- HomeView: selectedItem 在所有退出路径重置
- LivePhotoCore: trimmedSeconds 改用实际裁剪后视频时长
- LivePhotoCore: keyFrameTime 越界显式 clamp + DEBUG 警告
- LivePhotoCore: ExportParams Codable 向后兼容

本地化:
- Localizable.xcstrings 9 批翻译修复(~295 条)
  ar/es/fr/ja/ko 英文占位替换为正确翻译

修改文件:
- Sources/LivePhotoCore/LivePhotoCore.swift
- to-live-photo/Views/ResultView.swift
- to-live-photo/Views/HomeView.swift
- to-live-photo/Views/EditorView.swift
- to-live-photo/PresetManager.swift
- to-live-photo/RecentWorksManager.swift
- to-live-photo/Localizable.xcstrings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
empty
2026-02-08 00:19:54 +08:00
parent ec2e0a3ce5
commit f3bcaf4651
7 changed files with 10887 additions and 10826 deletions

View File

@@ -177,6 +177,25 @@ public struct ExportParams: Codable, Sendable, Hashable {
params.hdrPolicy = .toneMapToSDR
return params
}
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
trimStart = try container.decode(Double.self, forKey: .trimStart)
trimEnd = try container.decode(Double.self, forKey: .trimEnd)
keyFrameTime = try container.decode(Double.self, forKey: .keyFrameTime)
audioPolicy = try container.decode(AudioPolicy.self, forKey: .audioPolicy)
codecPolicy = try container.decode(CodecPolicy.self, forKey: .codecPolicy)
hdrPolicy = try container.decode(HDRPolicy.self, forKey: .hdrPolicy)
maxDimension = try container.decode(Int.self, forKey: .maxDimension)
cropRect = try container.decode(CropRect.self, forKey: .cropRect)
aspectRatio = try container.decode(AspectRatioTemplate.self, forKey: .aspectRatio)
compatibilityMode = try container.decode(Bool.self, forKey: .compatibilityMode)
targetFrameRate = try container.decode(Int.self, forKey: .targetFrameRate)
coverImageURL = try container.decodeIfPresent(URL.self, forKey: .coverImageURL)
aiEnhanceConfig = try container.decodeIfPresent(AIEnhanceConfig.self, forKey: .aiEnhanceConfig) ?? .disabled
}
}
public struct AppError: Error, Codable, Sendable, Hashable {
@@ -399,13 +418,22 @@ public actor LivePhotoValidator {
}
return await withCheckedContinuation { continuation in
let resumeOnce = ResumeOnce()
let options = PHLivePhotoRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestLivePhoto(
for: asset,
targetSize: CGSize(width: 1, height: 1),
contentMode: .aspectFit,
options: nil
) { livePhoto, _ in
continuation.resume(returning: livePhoto)
options: options
) { livePhoto, info in
//
if let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool, isDegraded {
return
}
if resumeOnce.tryConsume() {
continuation.resume(returning: livePhoto)
}
}
}
}
@@ -521,8 +549,10 @@ public actor LivePhotoBuilder {
destinationURL: trimmedURL
)
// 5
let trimmedSeconds = min(max(exportParams.trimEnd - exportParams.trimStart, 0.5), 5.0)
// trimEnd
let trimmedAsset = AVURLAsset(url: trimmedVideoURL)
let actualTrimmedDuration = try await trimmedAsset.load(.duration).seconds
let trimmedSeconds = min(max(actualTrimmedDuration, 0.5), 5.0)
let targetDuration = CMTime(seconds: trimmedSeconds, preferredTimescale: 600)
progress?(LivePhotoBuildProgress(stage: .normalize, fraction: 0.5))
let scaledVideoURL = try await scaleVideoToTargetDuration(
@@ -535,8 +565,19 @@ public actor LivePhotoBuilder {
destinationURL: scaledURL
)
// clamp keyFrameTime [trimStart, trimEnd]
let clampedKeyFrameTime: Double
if exportParams.keyFrameTime < exportParams.trimStart || exportParams.keyFrameTime > exportParams.trimEnd {
#if DEBUG
print("[LivePhotoBuilder] WARNING: keyFrameTime \(exportParams.keyFrameTime) out of range [\(exportParams.trimStart), \(exportParams.trimEnd)], clamping")
#endif
clampedKeyFrameTime = min(max(exportParams.keyFrameTime, exportParams.trimStart), exportParams.trimEnd)
} else {
clampedKeyFrameTime = exportParams.keyFrameTime
}
//
let keyFrameRatio = (exportParams.keyFrameTime - exportParams.trimStart) / max(0.001, exportParams.trimEnd - exportParams.trimStart)
let keyFrameRatio = (clampedKeyFrameTime - exportParams.trimStart) / max(0.001, exportParams.trimEnd - exportParams.trimStart)
let relativeKeyFrameTime = max(0, min(trimmedSeconds, keyFrameRatio * trimmedSeconds))
progress?(LivePhotoBuildProgress(stage: .extractKeyFrame, fraction: 0))