From 4bcad4d4b835c4a22b3cbd9b9ed17203eae600c8 Mon Sep 17 00:00:00 2001 From: empty Date: Sat, 7 Feb 2026 20:04:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=89=E5=85=A8=E5=AE=A1=E6=9F=A5=20P?= =?UTF-8?q?0-P2=20=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D=EF=BC=8826=E9=A1=B9?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 13 + .../LivePhotoCore/AIEnhancer/ODRManager.swift | 8 +- .../AIEnhancer/RealESRGANProcessor.swift | 22 +- .../AIEnhancer/TiledImageProcessor.swift | 83 +- Sources/LivePhotoCore/LivePhotoCore.swift | 55 +- quick-screenshot.sh | 4 +- scale-to-6.5inch.sh | 3 +- to-live-photo/to-live-photo/AppState.swift | 4 +- .../to-live-photo/DesignSystem.swift | 17 +- .../to-live-photo/LanguageManager.swift | 2 +- .../to-live-photo/Localizable.xcstrings | 1575 ++++------------- .../to-live-photo/Views/EditorView.swift | 76 +- .../to-live-photo/Views/HomeView.swift | 30 +- .../to-live-photo/Views/OnboardingView.swift | 10 +- .../Views/PrivacyPolicyView.swift | 36 +- .../to-live-photo/Views/ProcessingView.swift | 17 +- .../to-live-photo/Views/ResultView.swift | 31 +- .../to-live-photo/Views/SettingsView.swift | 26 +- .../Views/WallpaperGuideView.swift | 24 +- 19 files changed, 640 insertions(+), 1396 deletions(-) diff --git a/.gitignore b/.gitignore index eab6e26..a88cf9a 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,16 @@ to-live-photo/to-live-photo/build/ # PyTorch models (use Core ML instead) *.pth .serena/ + +# AI coding tools +.agent/ +.agents/ +.claude/ +.codex/ +.cursor/ +.gemini/ +.kiro/ +.opencode/ +.qoder/ +.trae/ +.windsurf/ diff --git a/Sources/LivePhotoCore/AIEnhancer/ODRManager.swift b/Sources/LivePhotoCore/AIEnhancer/ODRManager.swift index 3fd0209..cc11cf7 100644 --- a/Sources/LivePhotoCore/AIEnhancer/ODRManager.swift +++ b/Sources/LivePhotoCore/AIEnhancer/ODRManager.swift @@ -155,11 +155,13 @@ public actor ODRManager { private func checkODRAvailability() async -> Bool { // Use conditionallyBeginAccessingResources to check without triggering download let request = NSBundleResourceRequest(tags: [Self.modelTag]) - + return await withCheckedContinuation { continuation in - request.conditionallyBeginAccessingResources { available in + request.conditionallyBeginAccessingResources { [request] available in + // Capture request explicitly to prevent ARC from releasing it + // before the callback fires + _ = request if available { - // Model is already downloaded via ODR self.logger.debug("ODR model is available locally") } continuation.resume(returning: available) diff --git a/Sources/LivePhotoCore/AIEnhancer/RealESRGANProcessor.swift b/Sources/LivePhotoCore/AIEnhancer/RealESRGANProcessor.swift index 1cd5b6f..60b2fde 100644 --- a/Sources/LivePhotoCore/AIEnhancer/RealESRGANProcessor.swift +++ b/Sources/LivePhotoCore/AIEnhancer/RealESRGANProcessor.swift @@ -112,7 +112,10 @@ actor RealESRGANProcessor { logger.info("Running inference on \(width)x\(height) image...") - // Run inference synchronously (MLModel prediction is thread-safe) + // Capture actor-isolated state before entering non-isolated closure + let localModel = model + + // Run inference on background queue (MLModel prediction is thread-safe) let output: [UInt8] = try await withCheckedThrowingContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { do { @@ -123,22 +126,15 @@ actor RealESRGANProcessor { ) // Run inference synchronously - let prediction = try model.prediction(from: inputProvider) + let prediction = try localModel.prediction(from: inputProvider) // Extract output from model - // The model outputs to "activation_out" as either MultiArray or Image let rgbaData: [UInt8] if let outputValue = prediction.featureValue(for: "activation_out") { if let multiArray = outputValue.multiArrayValue { - // Output is MLMultiArray with shape [C, H, W] - self.logger.info("Output is MultiArray: \(multiArray.shape)") - rgbaData = try self.multiArrayToRGBA(multiArray) + rgbaData = try Self.multiArrayToRGBA(multiArray) } else if let outputBuffer = outputValue.imageBufferValue { - // Output is CVPixelBuffer (image) - let outWidth = CVPixelBufferGetWidth(outputBuffer) - let outHeight = CVPixelBufferGetHeight(outputBuffer) - self.logger.info("Output is Image: \(outWidth)x\(outHeight)") rgbaData = try ImageFormatConverter.pixelBufferToRGBAData(outputBuffer) } else { continuation.resume(throwing: AIEnhanceError.inferenceError( @@ -162,13 +158,14 @@ actor RealESRGANProcessor { } } + logger.info("Inference completed, output size: \(output.count) bytes") return output } /// Convert MLMultiArray [C, H, W] to RGBA byte array /// - Parameter multiArray: Output from model with shape [3, H, W] (RGB channels) /// - Returns: RGBA byte array with shape [H * W * 4] - private func multiArrayToRGBA(_ multiArray: MLMultiArray) throws -> [UInt8] { + private static func multiArrayToRGBA(_ multiArray: MLMultiArray) throws -> [UInt8] { let shape = multiArray.shape.map { $0.intValue } // Expect shape [3, H, W] for RGB @@ -178,12 +175,9 @@ actor RealESRGANProcessor { ) } - let channels = shape[0] let height = shape[1] let width = shape[2] - logger.info("Converting MultiArray \(channels)x\(height)x\(width) to RGBA") - // Output array: RGBA format var rgbaData = [UInt8](repeating: 255, count: width * height * 4) diff --git a/Sources/LivePhotoCore/AIEnhancer/TiledImageProcessor.swift b/Sources/LivePhotoCore/AIEnhancer/TiledImageProcessor.swift index b932726..98dcd87 100644 --- a/Sources/LivePhotoCore/AIEnhancer/TiledImageProcessor.swift +++ b/Sources/LivePhotoCore/AIEnhancer/TiledImageProcessor.swift @@ -63,17 +63,38 @@ struct TiledImageProcessor { logger.info("Extracted \(tiles.count) tiles") progress?(0.1) - // Step 2: Process each tile - var processedTiles: [(tile: ImageTile, output: [UInt8])] = [] + // Step 2: Pre-allocate output buffers for streaming stitching + let outputWidth = originalWidth * config.modelScale + let outputHeight = originalHeight * config.modelScale + var outputBuffer = [Float](repeating: 0, count: outputWidth * outputHeight * 3) + var weightBuffer = [Float](repeating: 0, count: outputWidth * outputHeight) + + // Step 3: Process each tile and blend immediately (streaming) let tileProgressBase = 0.1 - let tileProgressRange = 0.7 + let tileProgressRange = 0.75 for (index, tile) in tiles.enumerated() { try Task.checkCancellation() let pixelBuffer = try ImageFormatConverter.cgImageToPixelBuffer(tile.image) let outputData = try await processor.processImage(pixelBuffer) - processedTiles.append((tile, outputData)) + + // Blend tile into output immediately — no accumulation + let weights = createBlendingWeights( + tileWidth: min(config.outputTileSize, outputWidth - tile.outputOriginX), + tileHeight: min(config.outputTileSize, outputHeight - tile.outputOriginY) + ) + blendTileIntoOutput( + data: outputData, + weights: weights, + atX: tile.outputOriginX, + atY: tile.outputOriginY, + outputWidth: outputWidth, + outputHeight: outputHeight, + outputBuffer: &outputBuffer, + weightBuffer: &weightBuffer + ) + // outputData and weights are released here let tileProgress = tileProgressBase + tileProgressRange * Double(index + 1) / Double(tiles.count) progress?(tileProgress) @@ -82,19 +103,14 @@ struct TiledImageProcessor { await Task.yield() } - progress?(0.85) + progress?(0.9) - // Step 3: Stitch tiles with blending - let outputWidth = originalWidth * config.modelScale - let outputHeight = originalHeight * config.modelScale - let stitchedImage = try stitchTiles( - processedTiles, - outputWidth: outputWidth, - outputHeight: outputHeight - ) + // Step 4: Normalize and create final image + normalizeByWeights(&outputBuffer, weights: weightBuffer, width: outputWidth, height: outputHeight) + let stitchedImage = try createCGImage(from: outputBuffer, width: outputWidth, height: outputHeight) progress?(0.95) - // Step 4: Cap at max dimension if needed + // Step 5: Cap at max dimension if needed let finalImage = try capToMaxDimension(stitchedImage, maxDimension: 4320) progress?(1.0) @@ -196,45 +212,6 @@ struct TiledImageProcessor { // MARK: - Tile Stitching - /// Stitch processed tiles with weighted blending - private func stitchTiles( - _ tiles: [(tile: ImageTile, output: [UInt8])], - outputWidth: Int, - outputHeight: Int - ) throws -> CGImage { - // Create output buffers - var outputBuffer = [Float](repeating: 0, count: outputWidth * outputHeight * 3) - var weightBuffer = [Float](repeating: 0, count: outputWidth * outputHeight) - - let outputTileSize = config.outputTileSize // 2048 - - for (tile, data) in tiles { - // Create blending weights for this tile - let weights = createBlendingWeights( - tileWidth: min(outputTileSize, outputWidth - tile.outputOriginX), - tileHeight: min(outputTileSize, outputHeight - tile.outputOriginY) - ) - - // Blend tile into output - blendTileIntoOutput( - data: data, - weights: weights, - atX: tile.outputOriginX, - atY: tile.outputOriginY, - outputWidth: outputWidth, - outputHeight: outputHeight, - outputBuffer: &outputBuffer, - weightBuffer: &weightBuffer - ) - } - - // Normalize by accumulated weights - normalizeByWeights(&outputBuffer, weights: weightBuffer, width: outputWidth, height: outputHeight) - - // Convert to CGImage - return try createCGImage(from: outputBuffer, width: outputWidth, height: outputHeight) - } - /// Create blending weights with linear falloff at edges private func createBlendingWeights(tileWidth: Int, tileHeight: Int) -> [Float] { let overlap = config.outputOverlap // 256 diff --git a/Sources/LivePhotoCore/LivePhotoCore.swift b/Sources/LivePhotoCore/LivePhotoCore.swift index 385164a..1d80599 100644 --- a/Sources/LivePhotoCore/LivePhotoCore.swift +++ b/Sources/LivePhotoCore/LivePhotoCore.swift @@ -347,6 +347,21 @@ public actor AlbumWriter { } } +/// 线程安全的一次性消费守卫,防止 continuation 被 resume 多次 +private final class ResumeOnce: @unchecked Sendable { + private var _consumed = false + private let lock = NSLock() + + /// 尝试消费。仅第一次调用返回 true,后续调用均返回 false。 + func tryConsume() -> Bool { + lock.lock() + defer { lock.unlock() } + if _consumed { return false } + _consumed = true + return true + } +} + public actor LivePhotoValidator { public init() {} @@ -378,16 +393,13 @@ public actor LivePhotoValidator { public func requestLivePhoto(photoURL: URL, pairedVideoURL: URL) async -> PHLivePhoto? { await withCheckedContinuation { continuation in - var hasResumed = false + let resumeOnce = ResumeOnce() let requestID = PHLivePhoto.request( withResourceFileURLs: [pairedVideoURL, photoURL], placeholderImage: nil, targetSize: .zero, contentMode: .aspectFit ) { livePhoto, info in - // 确保只 resume 一次 - guard !hasResumed else { return } - // 如果是降级版本,等待完整版本 if let isDegraded = info[PHLivePhotoInfoIsDegradedKey] as? Bool, isDegraded { return @@ -398,8 +410,9 @@ public actor LivePhotoValidator { #if DEBUG print("[LivePhotoValidator] requestLivePhoto error: \(error.localizedDescription)") #endif - hasResumed = true - continuation.resume(returning: nil) + if resumeOnce.tryConsume() { + continuation.resume(returning: nil) + } return } @@ -407,23 +420,24 @@ public actor LivePhotoValidator { #if DEBUG print("[LivePhotoValidator] requestLivePhoto cancelled") #endif - hasResumed = true - continuation.resume(returning: nil) + if resumeOnce.tryConsume() { + continuation.resume(returning: nil) + } return } - hasResumed = true - continuation.resume(returning: livePhoto) + if resumeOnce.tryConsume() { + continuation.resume(returning: livePhoto) + } } // 添加超时保护,防止无限等待 DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - guard !hasResumed else { return } + guard resumeOnce.tryConsume() else { return } #if DEBUG print("[LivePhotoValidator] requestLivePhoto timeout, requestID: \(requestID)") #endif PHLivePhoto.cancelRequest(withRequestID: requestID) - hasResumed = true continuation.resume(returning: nil) } } @@ -966,6 +980,23 @@ public actor LivePhotoBuilder { assetWriter.startWriting() videoReader.startReading() metadataReader.startReading() + + // 检查 writer/reader 是否成功启动,防止 continuation 永不 resume + guard assetWriter.status == .writing else { + continuation.resume(throwing: AppError(code: "LPB-301", stage: .writeVideoMetadata, message: "视频处理失败", underlyingErrorDescription: assetWriter.error?.localizedDescription ?? "Writer 启动失败", suggestedActions: ["重试"])) + return + } + guard videoReader.status == .reading else { + assetWriter.cancelWriting() + continuation.resume(throwing: AppError(code: "LPB-301", stage: .writeVideoMetadata, message: "视频处理失败", underlyingErrorDescription: videoReader.error?.localizedDescription ?? "VideoReader 启动失败", suggestedActions: ["重试"])) + return + } + guard metadataReader.status == .reading else { + assetWriter.cancelWriting() + continuation.resume(throwing: AppError(code: "LPB-301", stage: .writeVideoMetadata, message: "视频处理失败", underlyingErrorDescription: metadataReader.error?.localizedDescription ?? "MetadataReader 启动失败", suggestedActions: ["重试"])) + return + } + assetWriter.startSession(atSourceTime: .zero) var currentFrameCount = 0 diff --git a/quick-screenshot.sh b/quick-screenshot.sh index 09cb208..ed0ff64 100755 --- a/quick-screenshot.sh +++ b/quick-screenshot.sh @@ -1,10 +1,12 @@ #!/bin/bash # 多语言快速截图脚本 +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + # 语言参数 LANGUAGE=${1:-zh-Hans} -SCREENSHOT_DIR="/Users/yuanjiantsui/projects/to-live-photo/app-store-screenshots/$LANGUAGE/6.7inch" +SCREENSHOT_DIR="$SCRIPT_DIR/app-store-screenshots/$LANGUAGE/6.7inch" mkdir -p "$SCREENSHOT_DIR" COUNTER=1 diff --git a/scale-to-6.5inch.sh b/scale-to-6.5inch.sh index e692cdb..734d01f 100755 --- a/scale-to-6.5inch.sh +++ b/scale-to-6.5inch.sh @@ -1,7 +1,8 @@ #!/bin/bash # 将 6.9" 截图缩放为 6.5" 截图 -SOURCE_DIR="/Users/yuanjiantsui/projects/to-live-photo/app-store-screenshots" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SOURCE_DIR="$SCRIPT_DIR/app-store-screenshots" LANGUAGES=("ja" "zh-Hans" "zh-Hant" "en" "es" "ar" "fr" "ko") echo "📐 开始缩放截图: 6.9\" (1320x2868) → 6.5\" (1284x2778)" diff --git a/to-live-photo/to-live-photo/AppState.swift b/to-live-photo/to-live-photo/AppState.swift index 216ac52..ebdd72a 100644 --- a/to-live-photo/to-live-photo/AppState.swift +++ b/to-live-photo/to-live-photo/AppState.swift @@ -76,6 +76,7 @@ final class AppState { self.currentWorkId = nil self.currentProcessingTask = nil self.processingProgress = nil + self.pop() } } } else { @@ -84,6 +85,7 @@ final class AppState { currentWorkId = nil currentProcessingTask = nil processingProgress = nil + pop() } Analytics.shared.log(.buildLivePhotoCancel) @@ -142,7 +144,7 @@ final class AppState { state.currentExportParams = nil } Analytics.shared.log(.buildLivePhotoSuccess) - Analytics.shared.log(.saveAlbumSuccess, parameters: ["assetId": result.savedAssetId]) + Analytics.shared.log(.saveAlbumSuccess, parameters: ["assetId": String(result.savedAssetId.prefix(8)) + "..."]) return result } catch is CancellationError { // 任务被取消,不需要额外处理 diff --git a/to-live-photo/to-live-photo/DesignSystem.swift b/to-live-photo/to-live-photo/DesignSystem.swift index 2635895..4b9cc02 100644 --- a/to-live-photo/to-live-photo/DesignSystem.swift +++ b/to-live-photo/to-live-photo/DesignSystem.swift @@ -530,19 +530,25 @@ struct SoftSlider: View { let gradient: LinearGradient let accessibilityLabel: String let step: Double + let onEditingChanged: ((Bool) -> Void)? + let isDisabled: Bool init( value: Binding, in range: ClosedRange, step: Double = 0.1, gradient: LinearGradient = Color.gradientPrimary, - accessibilityLabel: String = "" + accessibilityLabel: String = "", + isDisabled: Bool = false, + onEditingChanged: ((Bool) -> Void)? = nil ) { self._value = value self.range = range self.step = step self.gradient = gradient self.accessibilityLabel = accessibilityLabel + self.isDisabled = isDisabled + self.onEditingChanged = onEditingChanged } var body: some View { @@ -560,7 +566,7 @@ struct SoftSlider: View { // 进度填充 Capsule() - .fill(gradient) + .fill(isDisabled ? LinearGradient(colors: [Color.softPressed], startPoint: .leading, endPoint: .trailing) : gradient) .frame(width: max(0, thumbX), height: 8) // 滑块 @@ -572,18 +578,25 @@ struct SoftSlider: View { .gesture( DragGesture() .onChanged { gesture in + guard !isDisabled else { return } let newProgress = gesture.location.x / width let clampedProgress = max(0, min(1, newProgress)) value = range.lowerBound + (range.upperBound - range.lowerBound) * clampedProgress + onEditingChanged?(true) + } + .onEnded { _ in + onEditingChanged?(false) } ) } + .opacity(isDisabled ? 0.5 : 1.0) } .frame(height: 28) .accessibilityElement(children: .ignore) .accessibilityLabel(accessibilityLabel.isEmpty ? String(localized: "滑块") : accessibilityLabel) .accessibilityValue(Text(String(format: "%.1f", value))) .accessibilityAdjustableAction { direction in + guard !isDisabled else { return } switch direction { case .increment: value = min(range.upperBound, value + step) diff --git a/to-live-photo/to-live-photo/LanguageManager.swift b/to-live-photo/to-live-photo/LanguageManager.swift index 16e4c07..9303d29 100644 --- a/to-live-photo/to-live-photo/LanguageManager.swift +++ b/to-live-photo/to-live-photo/LanguageManager.swift @@ -20,7 +20,7 @@ final class LanguageManager { var displayName: String { switch self { - case .system: return "跟随系统" + case .system: return String(localized: "settings.language.system") case .zhHans: return "简体中文" case .zhHant: return "繁體中文" case .en: return "English" diff --git a/to-live-photo/to-live-photo/Localizable.xcstrings b/to-live-photo/to-live-photo/Localizable.xcstrings index 81dae32..eab5a10 100644 --- a/to-live-photo/to-live-photo/Localizable.xcstrings +++ b/to-live-photo/to-live-photo/Localizable.xcstrings @@ -5241,430 +5241,6 @@ } } }, - "privacy.cloudMode.item1.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "عند استخدام التحسين السحابي، يتم تحميل الصور أو إطارات الفيديو المحددة إلى الخادم عبر اتصال مشفر للمعالجة." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "When using Cloud Enhancement, selected images or video frames are uploaded to the server via an encrypted connection for processing." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Al usar la mejora en la nube, las imágenes o fotogramas de video seleccionados se cargan al servidor a través de una conexión cifrada para su procesamiento." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Lors de l'utilisation de l'amélioration cloud, les images ou images vidéo sélectionnées sont téléchargées vers le serveur via une connexion chiffrée pour traitement." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "クラウド強化を使用する場合、選択した画像または動画フレームは暗号化された接続を介してサーバーにアップロードされて処理されます。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "클라우드 향상을 사용할 때 선택한 이미지 또는 동영상 프레임이 암호화된 연결을 통해 서버에 업로드되어 처리됩니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "使用云端增强时,您选择的图片或视频帧会通过加密连接上传至服务器处理。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "使用雲端增強時,您選擇的圖片或影片幀會透過加密連線上傳至伺服器處理。" - } - } - } - }, - "privacy.cloudMode.item1.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "نقل البيانات" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Data Transfer" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Transferencia de datos" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Transfert de données" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "データ転送" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "데이터 전송" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据传输" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "資料傳輸" - } - } - } - }, - "privacy.cloudMode.item2.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "بعد المعالجة، سيتم حذف بياناتك الأصلية والنتائج تلقائيًا من الخادم في غضون 24 ساعة." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Upon completion, your original data and processed results will be automatically deleted from the server within 24 hours." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Después del procesamiento, sus datos originales y resultados se eliminarán automáticamente del servidor en 24 horas." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Après le traitement, vos données d'origine et résultats seront automatiquement supprimés du serveur sous 24 heures." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "処理後、元のデータと結果は24時間以内にサーバーから自動削除されます。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "처리 후 원본 데이터와 결과가 24시간 이내에 서버에서 자동 삭제됩니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "处理完成后,您的原始数据和处理结果将在 24 小时内从服务器自动删除。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "處理完成後,您的原始資料和處理結果將在 24 小時內從伺服器自動刪除。" - } - } - } - }, - "privacy.cloudMode.item2.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "الحذف الفوري" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Instant Deletion" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Eliminación instantánea" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Suppression instantanée" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "即時削除" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "즉시 삭제" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "即时删除" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "即時刪除" - } - } - } - }, - "privacy.cloudMode.item3.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يتم تشفير جميع البيانات المنقولة من طرف إلى طرف، وتقع الخوادم في مراكز بيانات متوافقة مع اللائحة العامة لحماية البيانات." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "All transmitted data is end-to-end encrypted, and servers are located in GDPR-compliant data centers." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Todos los datos transferidos están cifrados de extremo a extremo, y los servidores están ubicados en centros de datos que cumplen con GDPR." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Toutes les données transférées sont chiffrées de bout en bout, et les serveurs sont situés dans des centres de données conformes au RGPD." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "すべての転送データはエンドツーエンドで暗号化され、サーバーはGDPR準拠のデータセンターに配置されています。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "모든 전송 데이터는 엔드투엔드 암호화되며 서버는 GDPR 준수 데이터 센터에 위치합니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所有传输数据均经过端到端加密,服务器位于符合 GDPR 标准的数据中心。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "所有傳輸資料均經過端對端加密,伺服器位於符合 GDPR 標準的資料中心。" - } - } - } - }, - "privacy.cloudMode.item3.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "أمان البيانات" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Data Security" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Seguridad de datos" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Sécurité des données" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "データセキュリティ" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "데이터 보안" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据安全" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "資料安全" - } - } - } - }, - "privacy.cloudMode.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "وضع التحسين السحابي (Pro)" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cloud Enhancement Mode (Pro)" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Modo de Mejora en la Nube (Pro)" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mode d'Amélioration Cloud (Pro)" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "クラウド強化モード(Pro)" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "클라우드 향상 모드(Pro)" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "云端增强模式(Pro)" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "雲端增強模式(Pro)" - } - } - } - }, - "privacy.cloudMode.warning": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "⚠️ تتطلب وظيفة التحسين السحابي اتصال شبكة وسيتم طلب التأكيد قبل كل استخدام." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "⚠️ Cloud enhancement requires an internet connection; you will be explicitly prompted for consent before each use." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "⚠️ La función de mejora en la nube requiere conexión de red y se le pedirá confirmación antes de cada uso." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "⚠️ La fonction d'amélioration cloud nécessite une connexion réseau et une confirmation vous sera demandée avant chaque utilisation." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "⚠️ クラウド強化機能にはネットワーク接続が必要で、使用前に毎回確認が求められます。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "⚠️ 클라우드 향상 기능은 네트워크 연결이 필요하며 사용 전 매번 확인을 요청합니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "⚠️ 云端增强功能需要网络连接,每次使用前会明确提示并征得您的同意。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "⚠️ 雲端增強功能需要網路連線,每次使用前會明確提示並徵得您的同意。" - } - } - } - }, "privacy.collection.item1.desc": { "extractionState": "manual", "localizations": { @@ -7414,6 +6990,112 @@ } } }, + "privacy.storage.item3.desc": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "يتم تخزين معرّف مرجعي للصور المحددة محليًا في UserDefaults لعرض الأعمال الأخيرة بسرعة، ولا يتم تحميله إلى أي خادم." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "A local reference identifier for selected photos is stored in UserDefaults to quickly display recent works. It is never uploaded to any server." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se almacena un identificador de referencia local de las fotos seleccionadas en UserDefaults para mostrar rápidamente los trabajos recientes. Nunca se sube a ningún servidor." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un identifiant de référence local des photos sélectionnées est stocké dans UserDefaults pour afficher rapidement les travaux récents. Il n'est jamais envoyé à un serveur." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "最近の作品をすばやく表示するために、選択した写真のローカル参照識別子がUserDefaultsに保存されます。サーバーにアップロードされることはありません。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "최근 작품을 빠르게 표시하기 위해 선택한 사진의 로컬 참조 식별자가 UserDefaults에 저장됩니다. 서버에 업로드되지 않습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "为快速显示最近作品,所选照片的本地引用标识符会存储在 UserDefaults 中,不会上传至任何服务器。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "為快速顯示最近作品,所選照片的本地參考識別碼會儲存在 UserDefaults 中,不會上傳至任何伺服器。" + } + } + } + }, + "privacy.storage.item3.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "معرّف مرجعي محلي" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Local Reference Identifier" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Identificador de referencia local" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Identifiant de référence local" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ローカル参照識別子" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "로컬 참조 식별자" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "本地引用标识符" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "本地參考識別碼" + } + } + } + }, "privacy.storage.title": { "extractionState": "manual", "localizations": { @@ -9004,6 +8686,59 @@ } } }, + "result.backToRetry": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "العودة للمحاولة مرة أخرى" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to Retry" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver a Intentar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour pour Réessayer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "戻って再試行" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "돌아가서 다시 시도" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "返回重试" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "返回重試" + } + } + } + }, "result.continueCreating": { "extractionState": "manual", "localizations": { @@ -10382,6 +10117,165 @@ } } }, + "settings.feedbackConfirmExport": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تصدير" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Export" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Exportar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エクスポート" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "내보내기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "导出" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "匯出" + } + } + } + }, + "settings.feedbackConfirmMessage": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "ستتضمن حزمة التشخيص المعلومات التالية:\n\n• طراز الجهاز وإصدار النظام\n• إصدار التطبيق\n• حالة الأذونات\n• إحصائيات الأخطاء\n• ملخص الأعمال الأخيرة\n\nلا يتم تضمين أي محتوى وسائط." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "The diagnostic package will include the following information:\n\n• Device model & system version\n• App version\n• Permission status\n• Error statistics\n• Recent works summary\n\nNo media content is included." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El paquete de diagnóstico incluirá la siguiente información:\n\n• Modelo del dispositivo y versión del sistema\n• Versión de la app\n• Estado de permisos\n• Estadísticas de errores\n• Resumen de trabajos recientes\n\nNo se incluye contenido multimedia." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le package de diagnostic inclura les informations suivantes :\n\n• Modèle de l'appareil et version du système\n• Version de l'app\n• État des autorisations\n• Statistiques d'erreurs\n• Résumé des travaux récents\n\nAucun contenu multimédia n'est inclus." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "診断パッケージには以下の情報が含まれます:\n\n• デバイスモデルとシステムバージョン\n• アプリバージョン\n• 権限の状態\n• エラー統計\n• 最近の作品の概要\n\nメディアコンテンツは含まれません。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "진단 패키지에는 다음 정보가 포함됩니다:\n\n• 기기 모델 및 시스템 버전\n• 앱 버전\n• 권한 상태\n• 오류 통계\n• 최근 작업 요약\n\n미디어 콘텐츠는 포함되지 않습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "诊断包将包含以下信息:\n\n• 设备型号和系统版本\n• App 版本\n• 权限状态\n• 错误统计\n• 最近作品记录摘要\n\n不包含任何媒体内容。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "診斷包將包含以下資訊:\n\n• 裝置型號和系統版本\n• App 版本\n• 權限狀態\n• 錯誤統計\n• 最近作品記錄摘要\n\n不包含任何媒體內容。" + } + } + } + }, + "settings.feedbackConfirmTitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تصدير حزمة التشخيص" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Export Diagnostic Package" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Exportar paquete de diagnóstico" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter le package de diagnostic" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "診断パッケージをエクスポート" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "진단 패키지 내보내기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "导出诊断包" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "匯出診斷包" + } + } + } + }, "settings.goToSettings": { "extractionState": "manual", "localizations": { @@ -10488,54 +10382,107 @@ } } }, - "settings.languageChangeAlertMessage": { + "settings.language.system": { + "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "يحتاج التطبيق إلى إعادة التشغيل لتطبيق تغيير اللغة. إعادة التشغيل الآن؟" + "value": "اتباع النظام" } }, "en": { "stringUnit": { "state": "translated", - "value": "The app needs to restart for the language change to take effect. Restart now?" + "value": "System" } }, "es": { "stringUnit": { "state": "translated", - "value": "La aplicación necesita reiniciarse para aplicar el cambio de idioma. ¿Reiniciar ahora?" + "value": "Sistema" } }, "fr": { "stringUnit": { "state": "translated", - "value": "L'application doit redémarrer pour appliquer le changement de langue. Redémarrer maintenant ?" + "value": "Système" } }, "ja": { "stringUnit": { "state": "translated", - "value": "言語変更を適用するにはアプリの再起動が必要です。今すぐ再起動しますか?" + "value": "システムに従う" } }, "ko": { "stringUnit": { "state": "translated", - "value": "언어 변경을 적용하려면 앱을 다시 시작해야 합니다. 지금 다시 시작하시겠습니까?" + "value": "시스템 설정" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需要重启应用以使语言更改生效。现在重启?" + "value": "跟随系统" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需要重啟應用程式以使語言更改生效。現在重啟?" + "value": "跟隨系統" + } + } + } + }, + "settings.languageChangeAlertMessage": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "سيتم تطبيق تغيير اللغة عند إعادة تشغيل التطبيق في المرة القادمة." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "The language change will take effect the next time you open the app." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El cambio de idioma se aplicará la próxima vez que abras la app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le changement de langue prendra effet au prochain lancement de l'app." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "言語の変更は次回アプリを開いたときに適用されます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "언어 변경은 다음에 앱을 열 때 적용됩니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "语言更改将在下次打开应用时生效。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "語言更改將在下次打開應用程式時生效。" } } } @@ -11016,54 +10963,54 @@ } } }, - "settings.restartNow": { + "settings.applyAndRestart": { "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "إعادة التشغيل الآن" + "value": "تطبيق" } }, "en": { "stringUnit": { "state": "translated", - "value": "Restart Now" + "value": "Apply" } }, "es": { "stringUnit": { "state": "translated", - "value": "Reiniciar ahora" + "value": "Aplicar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Redémarrer" + "value": "Appliquer" } }, "ja": { "stringUnit": { "state": "translated", - "value": "今すぐ再起動" + "value": "適用" } }, "ko": { "stringUnit": { "state": "translated", - "value": "지금 다시 시작" + "value": "적용" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "立即重启" + "value": "应用" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "立即重啟" + "value": "套用" } } } @@ -11545,271 +11492,6 @@ } } }, - "terms.cloud.item1.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "تتطلب الوظائف السحابية اتصال شبكة مستقر، وقد تتسبب مشاكل الشبكة في فشل المعالجة." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cloud features require a stable internet connection; network issues may result in processing failure." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Las funciones en la nube requieren una conexión de red estable, los problemas de red pueden causar fallos de procesamiento." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Les fonctions cloud nécessitent une connexion réseau stable, des problèmes de réseau peuvent entraîner des échecs de traitement." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "クラウド機能には安定したネットワーク接続が必要で、ネットワーク問題により処理が失敗する可能性があります。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "클라우드 기능은 안정적인 네트워크 연결이 필요하며, 네트워크 문제로 인해 처리가 실패할 수 있습니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "云端功能需要稳定的网络连接,网络问题可能导致处理失败。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "雲端功能需要穩定的網路連線,網路問題可能導致處理失敗。" - } - } - } - }, - "terms.cloud.item1.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "الاعتماد على الشبكة" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Network Dependency" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Dependencia de red" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Dépendance réseau" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ネットワーク依存" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "네트워크 의존성" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "网络依赖" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "網路依賴" - } - } - } - }, - "terms.cloud.item2.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "سنبذل قصارى جهدنا لضمان استقرار الخدمة، لكننا لا نضمن توفرها بنسبة 100٪." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "We will strive for service stability but do not guarantee 100% availability." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Haremos todo lo posible para garantizar la estabilidad del servicio, pero no garantizamos una disponibilidad del 100%." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Nous ferons de notre mieux pour assurer la stabilité du service, mais ne garantissons pas une disponibilité à 100 %." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サービスの安定性を確保するよう努めますが、100%の可用性は保証しません。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "서비스 안정성을 보장하기 위해 최선을 다하지만 100% 가용성을 보장하지는 않습니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "我们将尽力保证服务稳定,但不保证 100% 可用性。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "我們將盡力保證服務穩定,但不保證 100% 可用性。" - } - } - } - }, - "terms.cloud.item2.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "توفر الخدمة" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Service Availability" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Disponibilidad del servicio" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Disponibilité du service" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サービスの可用性" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "서비스 가용성" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "服务可用性" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "服務可用性" - } - } - } - }, - "terms.cloud.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "الخدمات السحابية" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cloud Services" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Servicios en la Nube" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Services Cloud" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "クラウドサービス" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "클라우드 서비스" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "云端服务" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "雲端服務" - } - } - } - }, "terms.contact.email": { "extractionState": "manual", "localizations": { @@ -12976,483 +12658,6 @@ } } }, - "terms.subscription.item1.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "تتم معالجة جميع المشتريات من خلال Apple App Store. سيتم خصم رسوم الاشتراك من حساب Apple ID الخاص بك." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "All purchases are processed through the Apple App Store. Subscription fees will be charged to your Apple ID account." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Todas las compras se procesan a través de Apple App Store. Las tarifas de suscripción se deducirán de su cuenta de Apple ID." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Tous les achats sont traités via Apple App Store. Les frais d'abonnement seront déduits de votre compte Apple ID." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "すべての購入はApple App Storeを通じて処理されます。サブスクリプション料金はApple IDアカウントから引き落とされます。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "모든 구매는 Apple App Store를 통해 처리됩니다. 구독 요금은 Apple ID 계정에서 차감됩니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所有购买通过 Apple App Store 处理。订阅费用将从您的 Apple ID 账户中扣除。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "所有購買透過 Apple App Store 處理。訂閱費用將從您的 Apple ID 帳戶中扣除。" - } - } - } - }, - "terms.subscription.item1.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "طريقة الدفع" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Payment Method" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Método de pago" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mode de paiement" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "支払い方法" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "결제 방법" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "付款方式" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "付款方式" - } - } - } - }, - "terms.subscription.item2.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "سيتم تجديد الاشتراك تلقائيًا قبل 24 ساعة من انتهاء الصلاحية، ما لم تقم بإيقاف التجديد التلقائي قبل 24 ساعة على الأقل من انتهاء الصلاحية." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Subscriptions automatically renew within 24 hours prior to expiration unless auto-renew is turned off at least 24 hours before the end of the current period." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "La suscripción se renovará automáticamente 24 horas antes del vencimiento, a menos que desactive la renovación automática al menos 24 horas antes del vencimiento." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "L'abonnement sera renouvelé automatiquement 24 heures avant l'expiration, sauf si vous désactivez le renouvellement automatique au moins 24 heures avant l'expiration." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サブスクリプションは有効期限の24時間前に自動更新されます。有効期限の少なくとも24時間前に自動更新をオフにしない限り。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "구독은 만료 24시간 전에 자동으로 갱신됩니다. 만료 최소 24시간 전에 자동 갱신을 끄지 않는 한." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "订阅将在到期前 24 小时内自动续订,除非您在到期前至少 24 小时关闭自动续订。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "訂閱將在到期前 24 小時內自動續訂,除非您在到期前至少 24 小時關閉自動續訂。" - } - } - } - }, - "terms.subscription.item2.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "التجديد التلقائي" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Auto-Renewal" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Renovación automática" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Renouvellement automatique" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "自動更新" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "자동 갱신" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动续订" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "自動續訂" - } - } - } - }, - "terms.subscription.item3.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يمكنك إدارة أو إلغاء اشتراكك في أي وقت في إعدادات حساب App Store الخاص بك." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "You can manage or cancel your subscription at any time in your App Store account settings." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Puede administrar o cancelar su suscripción en cualquier momento en la configuración de su cuenta de App Store." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Vous pouvez gérer ou annuler votre abonnement à tout moment dans les paramètres de votre compte App Store." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "App Storeアカウント設定でいつでもサブスクリプションを管理またはキャンセルできます。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "App Store 계정 설정에서 언제든지 구독을 관리하거나 취소할 수 있습니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "您可以随时在 App Store 账户设置中管理或取消订阅。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "您可以隨時在 App Store 帳戶設定中管理或取消訂閱。" - } - } - } - }, - "terms.subscription.item3.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "إلغاء الاشتراك" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cancel Subscription" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Cancelar suscripción" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Annuler l'abonnement" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サブスクリプションのキャンセル" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "구독 취소" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "取消订阅" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "取消訂閱" - } - } - } - }, - "terms.subscription.item4.desc": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يجب طلب استرداد المبالغ المدفوعة للاشتراكات من خلال Apple." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Subscription refunds must be requested and processed through Apple." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Los reembolsos de suscripciones deben solicitarse a través de Apple." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Les remboursements d'abonnements doivent être demandés via Apple." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サブスクリプションの返金はAppleを通じて申請する必要があります。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "구독 환불은 Apple을 통해 신청해야 합니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "订阅退款需通过 Apple 申请处理。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "訂閱退款需透過 Apple 申請處理。" - } - } - } - }, - "terms.subscription.item4.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "سياسة الاسترداد" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Refund Policy" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Política de reembolso" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Politique de remboursement" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "返金ポリシー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "환불 정책" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "退款政策" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "退款政策" - } - } - } - }, - "terms.subscription.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "الاشتراكات والمشتريات" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Subscriptions & In-App Purchases" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Suscripciones y Compras" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Abonnements et Achats" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サブスクリプションと購入" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "구독 및 구매" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "订阅与内购" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "訂閱與內購" - } - } - } - }, "wallpaper.canAlwaysCreate": { "extractionState": "manual", "localizations": { @@ -15894,4 +15099,4 @@ } }, "version": "1.0" -} \ No newline at end of file +} diff --git a/to-live-photo/to-live-photo/Views/EditorView.swift b/to-live-photo/to-live-photo/Views/EditorView.swift index cb3d089..d02d894 100644 --- a/to-live-photo/to-live-photo/Views/EditorView.swift +++ b/to-live-photo/to-live-photo/Views/EditorView.swift @@ -241,9 +241,9 @@ struct EditorView: View { .font(.caption) .foregroundColor(.textSecondary) } - .padding(16) + .padding(DesignTokens.Spacing.lg) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } // MARK: - 封面帧预览 @@ -289,9 +289,9 @@ struct EditorView: View { } } } - .padding(16) + .padding(DesignTokens.Spacing.lg) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } // MARK: - 时长控制 @@ -310,18 +310,25 @@ struct EditorView: View { .foregroundStyle(.tint) } - Slider(value: $trimEnd, in: 1.0...max(1.0, min(1.5, videoDuration))) { _ in - updateKeyFrameTime() - } - .disabled(videoDuration < 1.0) + SoftSlider( + value: $trimEnd, + in: 1.0...max(1.0, min(1.5, videoDuration)), + step: 0.1, + gradient: Color.gradientPrimary, + accessibilityLabel: String(localized: "editor.videoDuration"), + isDisabled: videoDuration < 1.0, + onEditingChanged: { _ in + updateKeyFrameTime() + } + ) Text(String(localized: "editor.durationHint")) .font(.caption) .foregroundColor(.textSecondary) } - .padding(16) + .padding(DesignTokens.Spacing.lg) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } // MARK: - 封面帧时间选择 @@ -340,19 +347,26 @@ struct EditorView: View { .foregroundStyle(.tint) } - Slider(value: $keyFrameTime, in: trimStart...max(trimStart + 0.1, trimEnd)) { editing in - if !editing { - extractCoverFrame() + SoftSlider( + value: $keyFrameTime, + in: trimStart...max(trimStart + 0.1, trimEnd), + step: 0.05, + gradient: Color.gradientCyan, + accessibilityLabel: String(localized: "editor.keyFrameTime"), + onEditingChanged: { editing in + if !editing { + extractCoverFrame() + } } - } + ) Text(String(localized: "editor.keyFrameHint")) .font(.caption) .foregroundColor(.textSecondary) } - .padding(16) + .padding(DesignTokens.Spacing.lg) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } // MARK: - AI 超分辨率开关 @@ -362,7 +376,7 @@ struct EditorView: View { Toggle(isOn: $aiEnhanceEnabled) { HStack { Image(systemName: "wand.and.stars.inverse") - .foregroundStyle(.purple) + .foregroundStyle(Color.accentPurple) VStack(alignment: .leading, spacing: 2) { Text(String(localized: "editor.aiEnhance")) .font(.headline) @@ -372,7 +386,7 @@ struct EditorView: View { } } } - .tint(.purple) + .tint(Color.accentPurple) .disabled(!AIEnhancer.isAvailable() || aiModelDownloading) .onChange(of: aiEnhanceEnabled) { _, newValue in if newValue { @@ -392,8 +406,8 @@ struct EditorView: View { } ProgressView(value: aiModelDownloadProgress) - .tint(.purple) - + .tint(Color.accentPurple) + Text(String(format: "%.0f%%", aiModelDownloadProgress * 100)) .font(.caption2) .foregroundColor(.textSecondary) @@ -414,21 +428,21 @@ struct EditorView: View { } HStack(spacing: 4) { Image(systemName: "sparkles") - .foregroundStyle(.purple) + .foregroundStyle(Color.accentPurple) .font(.caption) Text(String(localized: "editor.aiResolutionBoost")) .font(.caption) } HStack(spacing: 4) { Image(systemName: "clock") - .foregroundStyle(.purple) + .foregroundStyle(Color.accentPurple) .font(.caption) Text(String(localized: "editor.aiProcessingTime")) .font(.caption) } HStack(spacing: 4) { Image(systemName: "cpu") - .foregroundStyle(.purple) + .foregroundStyle(Color.accentPurple) .font(.caption) Text(String(localized: "editor.aiLocalProcessing")) .font(.caption) @@ -450,9 +464,9 @@ struct EditorView: View { .padding(.top, 4) } } - .padding(16) - .background(Color.purple.opacity(0.1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(DesignTokens.Spacing.lg) + .background(Color.accentPurple.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) .task { // 检查模型是否需要下载 aiModelNeedsDownload = await AIEnhancer.needsDownload() @@ -513,9 +527,9 @@ struct EditorView: View { .padding(.leading, 4) } } - .padding(16) + .padding(DesignTokens.Spacing.lg) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } // MARK: - 诊断建议 @@ -561,9 +575,9 @@ struct EditorView: View { } } } - .padding(16) - .background(Color.yellow.opacity(0.1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(DesignTokens.Spacing.lg) + .background(Color.accentOrange.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } // MARK: - 生成按钮 diff --git a/to-live-photo/to-live-photo/Views/HomeView.swift b/to-live-photo/to-live-photo/Views/HomeView.swift index 3e37e94..8de7882 100644 --- a/to-live-photo/to-live-photo/Views/HomeView.swift +++ b/to-live-photo/to-live-photo/Views/HomeView.swift @@ -72,11 +72,11 @@ struct HomeView: View { VStack(spacing: DesignTokens.Spacing.sm) { Text(String(localized: "home.title")) - .font(.system(size: DesignTokens.FontSize.xxl, weight: .bold)) + .font(.title2.bold()) .foregroundColor(.textPrimary) Text(String(localized: "home.subtitle")) - .font(.system(size: DesignTokens.FontSize.base)) + .font(.subheadline) .foregroundColor(.textSecondary) .multilineTextAlignment(.center) } @@ -91,7 +91,7 @@ struct HomeView: View { Image(systemName: "video.badge.plus") .font(.system(size: 18, weight: .semibold)) Text(String(localized: "home.selectVideo")) - .font(.system(size: DesignTokens.FontSize.base, weight: .semibold)) + .font(.subheadline.weight(.semibold)) } .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -102,9 +102,6 @@ struct HomeView: View { } .buttonStyle(HomeButtonStyle()) .disabled(isLoading) - .onChange(of: selectedItem) { _, _ in - Analytics.shared.log(.homeImportVideoClick) - } // 加载状态 if isLoading { @@ -112,7 +109,7 @@ struct HomeView: View { ProgressView() .tint(.accentPurple) Text(String(localized: "home.loading")) - .font(.system(size: DesignTokens.FontSize.sm)) + .font(.footnote) .foregroundColor(.textSecondary) } .padding(.top, DesignTokens.Spacing.sm) @@ -124,7 +121,7 @@ struct HomeView: View { Image(systemName: "exclamationmark.circle.fill") .foregroundColor(.accentOrange) Text(errorMessage) - .font(.system(size: DesignTokens.FontSize.sm)) + .font(.footnote) .foregroundColor(.accentOrange) } .padding(.top, DesignTokens.Spacing.sm) @@ -150,7 +147,7 @@ struct HomeView: View { } Text(String(localized: "home.quickStart")) - .font(.system(size: DesignTokens.FontSize.lg, weight: .semibold)) + .font(.headline) .foregroundColor(.textPrimary) Spacer() @@ -166,7 +163,7 @@ struct HomeView: View { HStack { Spacer() Text(String(localized: "home.emptyHint")) - .font(.system(size: DesignTokens.FontSize.xs)) + .font(.caption2) .foregroundColor(.textMuted) Spacer() } @@ -191,13 +188,13 @@ struct HomeView: View { } Text(String(localized: "home.recentWorks")) - .font(.system(size: DesignTokens.FontSize.lg, weight: .semibold)) + .font(.headline) .foregroundColor(.textPrimary) Spacer() Text(String(localized: "home.worksCount \(recentWorks.recentWorks.count)")) - .font(.system(size: DesignTokens.FontSize.sm)) + .font(.footnote) .foregroundColor(.textMuted) } .padding(.horizontal, DesignTokens.Spacing.xs) @@ -219,6 +216,7 @@ struct HomeView: View { private func handleSelectedItem(_ item: PhotosPickerItem?) async { guard let item else { return } + Analytics.shared.log(.homeImportVideoClick) isLoading = true errorMessage = nil @@ -254,12 +252,12 @@ struct QuickStartStep: View { .frame(width: 24, height: 24) Text("\(number)") - .font(.system(size: DesignTokens.FontSize.xs, weight: .bold)) + .font(.caption2.bold()) .foregroundColor(.white) } Text(text) - .font(.system(size: DesignTokens.FontSize.sm)) + .font(.footnote) .foregroundColor(.textSecondary) Spacer() @@ -336,11 +334,11 @@ struct RecentWorkCard: View { // 信息 VStack(alignment: .leading, spacing: 2) { Text(work.aspectRatioDisplayName) - .font(.system(size: DesignTokens.FontSize.sm, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(.textPrimary) Text(work.createdAt.formatted(.relative(presentation: .named))) - .font(.system(size: DesignTokens.FontSize.xs)) + .font(.caption2) .foregroundColor(.textMuted) } } diff --git a/to-live-photo/to-live-photo/Views/OnboardingView.swift b/to-live-photo/to-live-photo/Views/OnboardingView.swift index 6fd3bce..9713c58 100644 --- a/to-live-photo/to-live-photo/Views/OnboardingView.swift +++ b/to-live-photo/to-live-photo/Views/OnboardingView.swift @@ -105,11 +105,11 @@ struct OnboardingView: View { // 文字内容 VStack(spacing: DesignTokens.Spacing.lg) { Text(page.title) - .font(.system(size: DesignTokens.FontSize.xxxl, weight: .bold)) + .font(.title.bold()) .foregroundColor(.textPrimary) Text(page.description) - .font(.system(size: DesignTokens.FontSize.base)) + .font(.subheadline) .foregroundColor(.textSecondary) .multilineTextAlignment(.center) .lineSpacing(4) @@ -148,7 +148,7 @@ struct OnboardingView: View { } label: { HStack(spacing: DesignTokens.Spacing.sm) { Text(currentPage < pages.count - 1 ? String(localized: "onboarding.nextStep") : String(localized: "onboarding.getStarted")) - .font(.system(size: DesignTokens.FontSize.base, weight: .semibold)) + .font(.subheadline.weight(.semibold)) Image(systemName: currentPage < pages.count - 1 ? "arrow.right" : "checkmark") .font(.system(size: 14, weight: .semibold)) @@ -168,8 +168,8 @@ struct OnboardingView: View { Button { completeOnboarding() } label: { - Text("跳过") - .font(.system(size: DesignTokens.FontSize.sm, weight: .medium)) + Text(String(localized: "onboarding.skip")) + .font(.footnote.weight(.medium)) .foregroundColor(.textMuted) } .padding(.top, DesignTokens.Spacing.sm) diff --git a/to-live-photo/to-live-photo/Views/PrivacyPolicyView.swift b/to-live-photo/to-live-photo/Views/PrivacyPolicyView.swift index 5c05432..bcae16a 100644 --- a/to-live-photo/to-live-photo/Views/PrivacyPolicyView.swift +++ b/to-live-photo/to-live-photo/Views/PrivacyPolicyView.swift @@ -13,7 +13,7 @@ struct PrivacyPolicyView: View { VStack(alignment: .leading, spacing: 20) { Text("privacy.lastUpdated") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Color.textSecondary) Group { sectionHeader(String(localized: "privacy.overview.title")) @@ -24,15 +24,6 @@ struct PrivacyPolicyView: View { bulletPoint(String(localized: "privacy.localMode.item1.title"), String(localized: "privacy.localMode.item1.desc")) bulletPoint(String(localized: "privacy.localMode.item2.title"), String(localized: "privacy.localMode.item2.desc")) - sectionHeader(String(localized: "privacy.cloudMode.title")) - bulletPoint(String(localized: "privacy.cloudMode.item1.title"), String(localized: "privacy.cloudMode.item1.desc")) - bulletPoint(String(localized: "privacy.cloudMode.item2.title"), String(localized: "privacy.cloudMode.item2.desc")) - bulletPoint(String(localized: "privacy.cloudMode.item3.title"), String(localized: "privacy.cloudMode.item3.desc")) - Text("privacy.cloudMode.warning") - .font(.caption) - .foregroundStyle(.orange) - .padding(.vertical, 4) - sectionHeader(String(localized: "privacy.permissions.title")) bulletPoint(String(localized: "privacy.permissions.item1.title"), String(localized: "privacy.permissions.item1.desc")) bulletPoint(String(localized: "privacy.permissions.item2.title"), String(localized: "privacy.permissions.item2.desc")) @@ -45,6 +36,7 @@ struct PrivacyPolicyView: View { sectionHeader(String(localized: "privacy.storage.title")) bulletPoint(String(localized: "privacy.storage.item1.title"), String(localized: "privacy.storage.item1.desc")) bulletPoint(String(localized: "privacy.storage.item2.title"), String(localized: "privacy.storage.item2.desc")) + bulletPoint(String(localized: "privacy.storage.item3.title"), String(localized: "privacy.storage.item3.desc")) sectionHeader(String(localized: "privacy.thirdParty.title")) bulletPoint(String(localized: "privacy.thirdParty.item1.title"), String(localized: "privacy.thirdParty.item1.desc")) @@ -57,7 +49,7 @@ struct PrivacyPolicyView: View { sectionHeader(String(localized: "privacy.contact.title")) Text("privacy.contact.intro") Text("privacy.contact.email") - .foregroundStyle(.blue) + .foregroundStyle(Color.accentPurple) } .padding(.horizontal) } @@ -76,12 +68,12 @@ struct PrivacyPolicyView: View { private func bulletPoint(_ title: String, _ description: String) -> some View { HStack(alignment: .top, spacing: 8) { Text("•") - .foregroundStyle(.secondary) + .foregroundStyle(Color.textSecondary) VStack(alignment: .leading, spacing: 2) { Text(title) .fontWeight(.medium) Text(description) - .foregroundStyle(.secondary) + .foregroundStyle(Color.textSecondary) } } } @@ -93,7 +85,7 @@ struct TermsOfServiceView: View { VStack(alignment: .leading, spacing: 20) { Text("terms.lastUpdated") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Color.textSecondary) Group { sectionHeader(String(localized: "terms.acceptance.title")) @@ -103,21 +95,11 @@ struct TermsOfServiceView: View { Text("terms.service.desc1") Text("terms.service.desc2") - sectionHeader(String(localized: "terms.subscription.title")) - bulletPoint(String(localized: "terms.subscription.item1.title"), String(localized: "terms.subscription.item1.desc")) - bulletPoint(String(localized: "terms.subscription.item2.title"), String(localized: "terms.subscription.item2.desc")) - bulletPoint(String(localized: "terms.subscription.item3.title"), String(localized: "terms.subscription.item3.desc")) - bulletPoint(String(localized: "terms.subscription.item4.title"), String(localized: "terms.subscription.item4.desc")) - sectionHeader(String(localized: "terms.limits.title")) bulletPoint(String(localized: "terms.limits.item1.title"), String(localized: "terms.limits.item1.desc")) bulletPoint(String(localized: "terms.limits.item2.title"), String(localized: "terms.limits.item2.desc")) bulletPoint(String(localized: "terms.limits.item3.title"), String(localized: "terms.limits.item3.desc")) - sectionHeader(String(localized: "terms.cloud.title")) - bulletPoint(String(localized: "terms.cloud.item1.title"), String(localized: "terms.cloud.item1.desc")) - bulletPoint(String(localized: "terms.cloud.item2.title"), String(localized: "terms.cloud.item2.desc")) - sectionHeader(String(localized: "terms.disclaimer.title")) bulletPoint(String(localized: "terms.disclaimer.item1.title"), String(localized: "terms.disclaimer.item1.desc")) bulletPoint(String(localized: "terms.disclaimer.item2.title"), String(localized: "terms.disclaimer.item2.desc")) @@ -131,7 +113,7 @@ struct TermsOfServiceView: View { sectionHeader(String(localized: "terms.contact.title")) Text("terms.contact.email") - .foregroundStyle(.blue) + .foregroundStyle(Color.accentPurple) } .padding(.horizontal) } @@ -150,12 +132,12 @@ struct TermsOfServiceView: View { private func bulletPoint(_ title: String, _ description: String) -> some View { HStack(alignment: .top, spacing: 8) { Text("•") - .foregroundStyle(.secondary) + .foregroundStyle(Color.textSecondary) VStack(alignment: .leading, spacing: 2) { Text(title) .fontWeight(.medium) Text(description) - .foregroundStyle(.secondary) + .foregroundStyle(Color.textSecondary) } } } diff --git a/to-live-photo/to-live-photo/Views/ProcessingView.swift b/to-live-photo/to-live-photo/Views/ProcessingView.swift index a2564a6..f7fde95 100644 --- a/to-live-photo/to-live-photo/Views/ProcessingView.swift +++ b/to-live-photo/to-live-photo/Views/ProcessingView.swift @@ -43,7 +43,6 @@ struct ProcessingView: View { ToolbarItem(placement: .navigationBarLeading) { Button(String(localized: "processing.cancel")) { appState.cancelProcessing() - appState.pop() } .foregroundColor(.accentPurple) } @@ -78,7 +77,7 @@ struct ProcessingView: View { } Text(String(localized: "processing.cancelling")) - .font(.system(size: DesignTokens.FontSize.lg, weight: .semibold)) + .font(.headline) .foregroundColor(.textSecondary) } } else { @@ -109,7 +108,7 @@ struct ProcessingView: View { if let progress = appState.processingProgress { Text(String(format: "%.0f%%", progress.fraction * 100)) - .font(.system(size: DesignTokens.FontSize.lg, weight: .bold)) + .font(.headline.bold()) .foregroundColor(.textPrimary) .contentTransition(.numericText()) } @@ -119,13 +118,13 @@ struct ProcessingView: View { // 阶段信息 VStack(spacing: DesignTokens.Spacing.md) { Text(stageText) - .font(.system(size: DesignTokens.FontSize.lg, weight: .semibold)) + .font(.headline) .foregroundColor(.textPrimary) .contentTransition(.numericText()) .animation(.easeInOut(duration: 0.3), value: stageText) Text(stageDescription) - .font(.system(size: DesignTokens.FontSize.sm)) + .font(.footnote) .foregroundColor(.textSecondary) .multilineTextAlignment(.center) } @@ -162,11 +161,11 @@ struct ProcessingView: View { if let error = appState.processingError { VStack(spacing: DesignTokens.Spacing.md) { Text(String(localized: "processing.failed")) - .font(.system(size: DesignTokens.FontSize.xl, weight: .bold)) + .font(.title3.bold()) .foregroundColor(.textPrimary) Text(error.message) - .font(.system(size: DesignTokens.FontSize.base)) + .font(.subheadline) .foregroundColor(.textSecondary) .multilineTextAlignment(.center) @@ -177,7 +176,7 @@ struct ProcessingView: View { Image(systemName: "lightbulb.fill") .foregroundColor(.accentOrange) Text(String(localized: "processing.suggestions")) - .font(.system(size: DesignTokens.FontSize.sm, weight: .semibold)) + .font(.footnote.weight(.semibold)) .foregroundColor(.textPrimary) } @@ -187,7 +186,7 @@ struct ProcessingView: View { .fill(Color.accentOrange) .frame(width: 6, height: 6) Text(action) - .font(.system(size: DesignTokens.FontSize.sm)) + .font(.footnote) .foregroundColor(.textSecondary) } } diff --git a/to-live-photo/to-live-photo/Views/ResultView.swift b/to-live-photo/to-live-photo/Views/ResultView.swift index 671a94d..2bac17c 100644 --- a/to-live-photo/to-live-photo/Views/ResultView.swift +++ b/to-live-photo/to-live-photo/Views/ResultView.swift @@ -100,12 +100,12 @@ struct ResultView: View { private var resultInfo: some View { VStack(spacing: DesignTokens.Spacing.lg) { Text(isSuccess ? String(localized: "result.saved") : String(localized: "result.saveFailed")) - .font(.system(size: DesignTokens.FontSize.xxl, weight: .bold)) + .font(.title2.bold()) .foregroundColor(.textPrimary) if isSuccess { Text(String(localized: "result.savedDescription")) - .font(.system(size: DesignTokens.FontSize.base)) + .font(.subheadline) .foregroundColor(.textSecondary) .multilineTextAlignment(.center) @@ -122,7 +122,7 @@ struct ResultView: View { .padding(.top, DesignTokens.Spacing.sm) } else { Text(String(localized: "result.failedDescription")) - .font(.system(size: DesignTokens.FontSize.base)) + .font(.subheadline) .foregroundColor(.textSecondary) } } @@ -144,7 +144,7 @@ struct ResultView: View { appState.popToRoot() } } else { - SoftPrimaryButton("返回重试", icon: "arrow.counterclockwise", gradient: Color.gradientWarm) { + SoftPrimaryButton(String(localized: "result.backToRetry"), icon: "arrow.counterclockwise", gradient: Color.gradientWarm) { appState.pop() } @@ -199,7 +199,7 @@ struct ValidationBadge: View { Image(systemName: icon) .font(.system(size: 12)) Text(text) - .font(.system(size: DesignTokens.FontSize.xs, weight: .medium)) + .font(.caption2.weight(.medium)) } .foregroundColor(color) .padding(.horizontal, DesignTokens.Spacing.md) @@ -212,6 +212,7 @@ struct ValidationBadge: View { // MARK: - 庆祝粒子效果 struct CelebrationParticles: View { @State private var particles: [Particle] = [] + @State private var viewSize: CGSize = .zero var body: some View { GeometryReader { geometry in @@ -222,22 +223,30 @@ struct CelebrationParticles: View { .position(particle.position) .opacity(particle.opacity) } - } - .onAppear { - generateParticles() + .onAppear { + viewSize = geometry.size + generateParticles(in: geometry.size) + } } } - private func generateParticles() { + private func generateParticles(in size: CGSize) { let colors: [Color] = [.accentPurple, .accentPink, .accentGreen, .accentCyan, .accentOrange] + // 使用实际屏幕尺寸计算粒子位置 + let horizontalPadding: CGFloat = 50 + let minX = horizontalPadding + let maxX = max(horizontalPadding + 100, size.width - horizontalPadding) + let startY = size.height * 0.5 // 从屏幕中部开始 + let flyDistance = size.height * 0.4 // 飞行距离为屏幕高度的 40% + for i in 0..<30 { let delay = Double(i) * 0.03 DispatchQueue.main.asyncAfter(deadline: .now() + delay) { let particle = Particle( id: UUID(), - position: CGPoint(x: CGFloat.random(in: 50...350), y: 400), + position: CGPoint(x: CGFloat.random(in: minX...maxX), y: startY), color: colors.randomElement()!, size: CGFloat.random(in: 6...12), opacity: 1.0 @@ -247,7 +256,7 @@ struct CelebrationParticles: View { // 动画粒子向上飘动 withAnimation(.easeOut(duration: Double.random(in: 1.5...2.5))) { if let index = particles.firstIndex(where: { $0.id == particle.id }) { - particles[index].position.y -= CGFloat.random(in: 300...500) + particles[index].position.y -= CGFloat.random(in: flyDistance...(flyDistance * 1.5)) particles[index].position.x += CGFloat.random(in: -50...50) particles[index].opacity = 0 } diff --git a/to-live-photo/to-live-photo/Views/SettingsView.swift b/to-live-photo/to-live-photo/Views/SettingsView.swift index 33985ba..f261751 100644 --- a/to-live-photo/to-live-photo/Views/SettingsView.swift +++ b/to-live-photo/to-live-photo/Views/SettingsView.swift @@ -16,6 +16,7 @@ struct SettingsView: View { @State private var feedbackPackageURL: URL? @State private var showingShareSheet = false @State private var showingLanguageChangeAlert = false + @State private var showingFeedbackConfirmAlert = false @State private var pendingLanguage: LanguageManager.Language? var body: some View { @@ -93,7 +94,7 @@ struct SettingsView: View { // 反馈 Section { Button { - exportFeedbackPackage() + showingFeedbackConfirmAlert = true } label: { Label(String(localized: "settings.exportDiagnostics"), systemImage: "doc.text") } @@ -165,15 +166,22 @@ struct SettingsView: View { Button(String(localized: "common.cancel"), role: .cancel) { pendingLanguage = nil } - Button(String(localized: "settings.restartNow"), role: .none) { + Button(String(localized: "settings.applyAndRestart"), role: .none) { if let newLanguage = pendingLanguage { LanguageManager.shared.current = newLanguage } - exit(0) } } message: { Text(String(localized: "settings.languageChangeAlertMessage")) } + .alert(String(localized: "settings.feedbackConfirmTitle"), isPresented: $showingFeedbackConfirmAlert) { + Button(String(localized: "common.cancel"), role: .cancel) {} + Button(String(localized: "settings.feedbackConfirmExport"), role: .none) { + exportFeedbackPackage() + } + } message: { + Text(String(localized: "settings.feedbackConfirmMessage")) + } .sheet(isPresented: $showingShareSheet) { if let url = feedbackPackageURL { ShareSheet(activityItems: [url]) @@ -407,12 +415,12 @@ struct SettingsView: View { extension PHAuthorizationStatus: @retroactive CustomStringConvertible { public var description: String { switch self { - case .notDetermined: return "未确定" - case .restricted: return "受限" - case .denied: return "已拒绝" - case .authorized: return "已授权" - case .limited: return "部分授权" - @unknown default: return "未知" + case .notDetermined: return "notDetermined" + case .restricted: return "restricted" + case .denied: return "denied" + case .authorized: return "authorized" + case .limited: return "limited" + @unknown default: return "unknown" } } } diff --git a/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift b/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift index 9db29c4..8e85bb0 100644 --- a/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift +++ b/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift @@ -18,7 +18,7 @@ struct WallpaperGuideView: View { var body: some View { ScrollView { - VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.xxl) { headerSection quickActionSection @@ -29,8 +29,8 @@ struct WallpaperGuideView: View { doneButton } - .padding(.horizontal, 20) - .padding(.vertical, 16) + .padding(.horizontal, DesignTokens.Spacing.xl) + .padding(.vertical, DesignTokens.Spacing.lg) } .navigationTitle(String(localized: "wallpaper.title")) .navigationBarTitleDisplayMode(.inline) @@ -95,17 +95,11 @@ struct WallpaperGuideView: View { Image(systemName: "arrow.up.right.square") .font(.title3) } - .padding(16) + .padding(DesignTokens.Spacing.lg) .frame(maxWidth: .infinity) - .background( - LinearGradient( - colors: [Color.blue, Color.purple], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) + .background(Color.gradientPrimary) .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 14)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } } @@ -172,7 +166,7 @@ struct WallpaperGuideView: View { } .padding(12) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } } @@ -233,7 +227,7 @@ struct WallpaperGuideView: View { .padding() .background(Color.accentColor) .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 14)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } Text(String(localized: "wallpaper.canAlwaysCreate")) @@ -317,7 +311,7 @@ struct FAQRow: View { .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.softElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md)) } }