diff --git a/Adobe Downloader.xcodeproj/project.pbxproj b/Adobe Downloader.xcodeproj/project.pbxproj index 8c6b63a..2a82c86 100644 --- a/Adobe Downloader.xcodeproj/project.pbxproj +++ b/Adobe Downloader.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = TG862GVKHK; @@ -237,7 +238,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist; LOCALIZATION_PREFERS_STRING_CATALOGS = NO; - MACOSX_DEPLOYMENT_TARGET = 15.1; + MACOSX_DEPLOYMENT_TARGET = 12.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-sectcreate", @@ -259,6 +260,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = TG862GVKHK; @@ -266,7 +268,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist; LOCALIZATION_PREFERS_STRING_CATALOGS = NO; - MACOSX_DEPLOYMENT_TARGET = 15.1; + MACOSX_DEPLOYMENT_TARGET = 12.0; OTHER_LDFLAGS = ( "-sectcreate", __TEXT, @@ -413,6 +415,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 120; @@ -430,7 +433,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -445,6 +448,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 120; @@ -462,7 +466,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index a0335d4..88c84fe 100644 --- a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -14,8 +14,8 @@ filePath = "Adobe Downloader/Utils/DownloadUtils.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "463" - endingLineNumber = "463" + startingLineNumber = "450" + endingLineNumber = "450" landmarkName = "startDownloadProcess(task:)" landmarkType = "7"> @@ -23,16 +23,48 @@ + + + + + + + + diff --git a/Adobe Downloader/Adobe DownloaderApp.swift b/Adobe Downloader/Adobe DownloaderApp.swift index 89103c1..5b3e05f 100644 --- a/Adobe Downloader/Adobe DownloaderApp.swift +++ b/Adobe Downloader/Adobe DownloaderApp.swift @@ -9,51 +9,42 @@ struct Adobe_DownloaderApp: App { @State private var showTipsSheet = false @State private var showLanguagePicker = false @State private var showCreativeCloudAlert = false - @AppStorage("useDefaultLanguage") private var useDefaultLanguage: Bool = true - @AppStorage("defaultLanguage") private var defaultLanguage: String = "ALL" - @AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true - @AppStorage("confirmRedownload") private var confirmRedownload: Bool = true - @AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true - @AppStorage("defaultDirectory") private var defaultDirectory: String = "" + @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage + @StorageValue(\.defaultLanguage) private var defaultLanguage + @StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon + @StorageValue(\.confirmRedownload) private var confirmRedownload + @StorageValue(\.useDefaultDirectory) private var useDefaultDirectory + @StorageValue(\.defaultDirectory) private var defaultDirectory @State private var showBackupResultAlert = false @State private var backupResultMessage = "" @State private var backupSuccess = false private let updaterController: SPUStandardUpdaterController init() { - updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) - - let isFirstRun = UserDefaults.standard.object(forKey: "downloadAppleSilicon") == nil || - UserDefaults.standard.object(forKey: "useDefaultLanguage") == nil - - UserDefaults.standard.set(isFirstRun, forKey: "isFirstLaunch") - - if UserDefaults.standard.object(forKey: "downloadAppleSilicon") == nil { - UserDefaults.standard.set(AppStatics.isAppleSilicon, forKey: "downloadAppleSilicon") + if StorageData.shared.installedHelperBuild == "0" { + StorageData.shared.installedHelperBuild = "0" } - if UserDefaults.standard.object(forKey: "useDefaultLanguage") == nil { + updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + + if StorageData.shared.isFirstLaunch { + StorageData.shared.downloadAppleSilicon = AppStatics.isAppleSilicon + let systemLanguage = Locale.current.identifier let matchedLanguage = AppStatics.supportedLanguages.first { systemLanguage.hasPrefix($0.code.prefix(2)) }?.code ?? "ALL" + StorageData.shared.defaultLanguage = matchedLanguage + StorageData.shared.useDefaultLanguage = true - UserDefaults.standard.set(true, forKey: "useDefaultLanguage") - UserDefaults.standard.set(matchedLanguage, forKey: "defaultLanguage") - } - - if UserDefaults.standard.object(forKey: "useDefaultDirectory") == nil { if let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first { - print(downloadsURL.path) - defaultDirectory = downloadsURL.path - UserDefaults.standard.set(true, forKey: "useDefaultDirectory") - UserDefaults.standard.set(downloadsURL.path, forKey: "defaultDirectory") + StorageData.shared.defaultDirectory = downloadsURL.path + StorageData.shared.useDefaultDirectory = true } } - PrivilegedHelperManager.shared.checkInstall() - if UserDefaults.standard.string(forKey: "apiVersion") == nil { - UserDefaults.standard.set("6", forKey: "apiVersion") + if StorageData.shared.apiVersion == "6" { + StorageData.shared.apiVersion = "6" } } @@ -64,6 +55,8 @@ struct Adobe_DownloaderApp: App { .frame(width: 850, height: 800) .tint(.blue) .task { + PrivilegedHelperManager.shared.checkInstall() + await MainActor.run { appDelegate.networkManager = networkManager networkManager.loadSavedTasks() @@ -79,9 +72,9 @@ struct Adobe_DownloaderApp: App { showBackupAlert = true } - if UserDefaults.standard.bool(forKey: "isFirstLaunch") { + if StorageData.shared.isFirstLaunch { showTipsSheet = true - UserDefaults.standard.removeObject(forKey: "isFirstLaunch") + StorageData.shared.isFirstLaunch = false } } } @@ -113,8 +106,14 @@ struct Adobe_DownloaderApp: App { VStack(spacing: 12) { HStack { - Toggle("使用默认语言", isOn: $useDefaultLanguage) - .padding(.leading, 5) + Toggle("使用默认语言", isOn: Binding( + get: { useDefaultLanguage }, + set: { + useDefaultLanguage = $0 + StorageData.shared.useDefaultLanguage = $0 + } + )) + .padding(.leading, 5) Spacer() Text(getLanguageName(code: defaultLanguage)) .foregroundColor(.secondary) @@ -127,8 +126,14 @@ struct Adobe_DownloaderApp: App { Divider() HStack { - Toggle("使用默认目录", isOn: $useDefaultDirectory) - .padding(.leading, 5) + Toggle("使用默认目录", isOn: Binding( + get: { useDefaultDirectory }, + set: { + useDefaultDirectory = $0 + StorageData.shared.useDefaultDirectory = $0 + } + )) + .padding(.leading, 5) Spacer() Text(formatPath(defaultDirectory)) .foregroundColor(.secondary) @@ -143,25 +148,36 @@ struct Adobe_DownloaderApp: App { Divider() HStack { - Toggle("重新下载时需要确认", isOn: $confirmRedownload) - .padding(.leading, 5) + Toggle("重新下载时需要确认", isOn: Binding( + get: { confirmRedownload }, + set: { + confirmRedownload = $0 + StorageData.shared.confirmRedownload = $0 + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + )) + .padding(.leading, 5) Spacer() } Divider() HStack { - Toggle("下载 Apple Silicon 架构", isOn: $downloadAppleSilicon) - .padding(.leading, 5) + Toggle("下载 Apple Silicon 架构", isOn: Binding( + get: { downloadAppleSilicon }, + set: { + downloadAppleSilicon = $0 + StorageData.shared.downloadAppleSilicon = $0 + networkManager.updateAllowedPlatform(useAppleSilicon: $0) + } + )) + .padding(.leading, 5) Spacer() Text("当前架构: \(AppStatics.cpuArchitecture)") .foregroundColor(.secondary) .lineLimit(1) .truncationMode(.middle) } - .onChange(of: downloadAppleSilicon) { newValue in - networkManager.updateAllowedPlatform(useAppleSilicon: newValue) - } } .padding() .background(Color(NSColor.controlBackgroundColor)) @@ -180,6 +196,7 @@ struct Adobe_DownloaderApp: App { .sheet(isPresented: $showLanguagePicker) { LanguagePickerView(languages: AppStatics.supportedLanguages) { language in defaultLanguage = language + StorageData.shared.defaultLanguage = language showLanguagePicker = false } } diff --git a/Adobe Downloader/Commons/Enums.swift b/Adobe Downloader/Commons/Enums.swift index 0c5a647..7957d89 100644 --- a/Adobe Downloader/Commons/Enums.swift +++ b/Adobe Downloader/Commons/Enums.swift @@ -15,28 +15,11 @@ enum PackageStatus: Equatable, Codable { var description: LocalizedStringKey { switch self { - case .waiting: return "等待中" - case .downloading: return "下载中" - case .paused: return "已暂停" - case .completed: return "已完成" - case .failed(let message): return "失败: \(message)" - } - } - - static func == (lhs: PackageStatus, rhs: PackageStatus) -> Bool { - switch (lhs, rhs) { - case (.waiting, .waiting): - return true - case (.downloading, .downloading): - return true - case (.paused, .paused): - return true - case (.completed, .completed): - return true - case (.failed(let lhsMessage), .failed(let rhsMessage)): - return lhsMessage == rhsMessage - default: - return false + case .waiting: return LocalizedStringKey("等待中...") + case .downloading: return LocalizedStringKey("下载中...") + case .paused: return LocalizedStringKey("已暂停") + case .completed: return LocalizedStringKey("已完成") + case .failed(let message): return LocalizedStringKey("下载失败: \(message)") } } } @@ -61,6 +44,7 @@ enum NetworkError: Error, LocalizedError { case downloadError(String, Error?) case downloadCancelled case insufficientStorage(Int64, Int64) + case cancelled case fileSystemError(String, Error?) case fileExists(String) @@ -70,97 +54,112 @@ enum NetworkError: Error, LocalizedError { case applicationInfoError(String, Error?) case unsupportedPlatform(String) case incompatibleVersion(String, String) - case cancelled case installError(String) - var errorCode: Int { + private var errorGroup: Int { switch self { - case .noConnection: return 1001 - case .timeout: return 1002 - case .serverUnreachable: return 1003 - case .invalidURL: return 2001 - case .invalidRequest: return 2002 - case .invalidResponse: return 2003 - case .invalidData: return 3001 - case .parsingError: return 3002 - case .dataValidationError: return 3003 - case .httpError: return 4001 - case .serverError: return 4002 - case .clientError: return 4003 - case .downloadError: return 5001 - case .downloadCancelled: return 5002 - case .insufficientStorage: return 5003 - case .fileSystemError: return 6001 - case .fileExists: return 6002 - case .fileNotFound: return 6003 - case .filePermissionDenied: return 6004 - case .applicationInfoError: return 7001 - case .unsupportedPlatform: return 7002 - case .incompatibleVersion: return 7003 - case .cancelled: return 5004 - case .installError: return 8001 + case .noConnection, .timeout, .serverUnreachable: return 1000 + case .invalidURL, .invalidRequest, .invalidResponse: return 2000 + case .invalidData, .parsingError, .dataValidationError: return 3000 + case .httpError, .serverError, .clientError: return 4000 + case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000 + case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000 + case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError: return 7000 } } + private var errorOffset: Int { + switch self { + case .noConnection: return 1 + case .timeout: return 2 + case .serverUnreachable: return 3 + case .invalidURL: return 1 + case .invalidRequest: return 2 + case .invalidResponse: return 3 + case .invalidData: return 1 + case .parsingError: return 2 + case .dataValidationError: return 3 + case .httpError: return 1 + case .serverError: return 2 + case .clientError: return 3 + case .downloadError: return 1 + case .downloadCancelled: return 2 + case .insufficientStorage: return 3 + case .cancelled: return 4 + case .fileSystemError: return 1 + case .fileExists: return 2 + case .fileNotFound: return 3 + case .filePermissionDenied: return 4 + case .applicationInfoError: return 1 + case .unsupportedPlatform: return 2 + case .incompatibleVersion: return 3 + case .installError: return 4 + } + } + + var errorCode: Int { + return errorGroup + errorOffset + } + var errorDescription: String? { switch self { - case .noConnection: - return NSLocalizedString("没有网络连接", comment: "Network error") - case .timeout: - return NSLocalizedString("请求超时,请检查网络连接后重试", comment: "Network timeout") - case .serverUnreachable(let server): - return NSLocalizedString("无法连接到服务器: \(server)", comment: "Server unreachable") - case .invalidURL(let url): - return NSLocalizedString("无效的URL: \(url)", comment: "Invalid URL") - case .invalidRequest(let reason): - return NSLocalizedString("无效的请求: \(reason)", comment: "Invalid request") - case .invalidResponse: - return NSLocalizedString("服务器响应无效", comment: "Invalid response") - case .invalidData(let detail): - return NSLocalizedString("数据无效: \(detail)", comment: "Invalid data") - case .parsingError(let error, let context): - return NSLocalizedString("解析错误: \(context) - \(error.localizedDescription)", comment: "Parsing error") - case .dataValidationError(let reason): - return NSLocalizedString("数据验证失败: \(reason)", comment: "Data validation error") - case .httpError(let code, let message): - return NSLocalizedString("HTTP错误 \(code): \(message ?? "")", comment: "HTTP error") - case .serverError(let code): - return NSLocalizedString("服务器错误: \(code)", comment: "Server error") - case .clientError(let code): - return NSLocalizedString("客户端错误: \(code)", comment: "Client error") - case .downloadError(let message, let error): - if let error = error { - return NSLocalizedString("\(message): \(error.localizedDescription)", comment: "Download error") - } - return NSLocalizedString(message, comment: "Download error") - case .downloadCancelled: - return NSLocalizedString("下载已取消", comment: "Download cancelled") - case .insufficientStorage(let needed, let available): - return NSLocalizedString("存储空间不足: 需要 \(needed)字节, 可用 \(available)字节", comment: "Insufficient storage") - case .fileSystemError(let operation, let error): - if let error = error { - return NSLocalizedString("文件系统错误(\(operation)): \(error.localizedDescription)", comment: "File system error") - } - return NSLocalizedString("文件系统错误: \(operation)", comment: "File system error") - case .fileExists(let path): - return NSLocalizedString("文件已存在: \(path)", comment: "File exists") - case .fileNotFound(let path): - return NSLocalizedString("文件不存在: \(path)", comment: "File not found") - case .filePermissionDenied(let path): - return NSLocalizedString("文件访问权限被拒绝: \(path)", comment: "File permission denied") - case .applicationInfoError(let message, let error): - if let error = error { - return NSLocalizedString("应用信息错误(\(message)): \(error.localizedDescription)", comment: "Application info error") - } - return NSLocalizedString("应用信息错误: \(message)", comment: "Application info error") - case .unsupportedPlatform(let platform): - return NSLocalizedString("不支持的平台: \(platform)", comment: "Unsupported platform") - case .incompatibleVersion(let current, let required): - return NSLocalizedString("版本不兼容: 当前版本 \(current), 需要版本 \(required)", comment: "Incompatible version") - case .cancelled: - return NSLocalizedString("下载已取消", comment: "Download cancelled") - case .installError(let message): - return NSLocalizedString("安装错误: \(message)", comment: "Install error") + case .noConnection: + return NSLocalizedString("网络无连接", value: "Network error", comment: "Network error") + case .timeout: + return NSLocalizedString("请求超时,请检查网络连接后重试", value: "请求超时,请检查网络连接后重试", comment: "Network timeout") + case .serverUnreachable(let server): + return String(format: NSLocalizedString("无法连接到服务器: %@", value: "无法连接到服务器: %@",comment: "Server unreachable"), server) + case .invalidURL(let url): + return String(format: NSLocalizedString("无效的URL: %@", value: "无效的URL: %@", comment: "Invalid URL"), url) + case .invalidRequest(let reason): + return String(format: NSLocalizedString("无效的请求: %@", value: "无效的请求: %@", comment: "Invalid request"), reason) + case .invalidResponse: + return NSLocalizedString("服务器响应无效", value: "服务器响应无效", comment: "Invalid response") + case .invalidData(let detail): + return String(format: NSLocalizedString("数据无效: %@", value: "数据无效: %@", comment: "Invalid data"), detail) + case .parsingError(let error, let context): + return String(format: NSLocalizedString("解析错误: %@ - %@", value: "Parsing error: %@ - %@", comment: "Parsing error"), context, error.localizedDescription) + case .dataValidationError(let reason): + return String(format: NSLocalizedString("数据验证失败: %@", value: "数据验证失败: %@", comment: "Data validation error"), reason) + case .httpError(let code, let message): + return String(format: NSLocalizedString("HTTP错误 %d: %@", value: "HTTP错误 %d: %@", comment: "HTTP error"), code, message ?? "") + case .serverError(let code): + return String(format: NSLocalizedString("服务器错误: %d", value: "服务器错误: %d", comment: "Server error"), code) + case .clientError(let code): + return String(format: NSLocalizedString("客户端错误: %d", value: "客户端错误: %d", comment: "Client error"), code) + case .downloadError(let message, let error): + if let error = error { + return String(format: NSLocalizedString("下载错误, 错误原因: %@, %@", value: "%@: %@", comment: "Download error with cause"), message, error.localizedDescription) + } + return NSLocalizedString(message, value: message, comment: "Download error") + case .downloadCancelled: + return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled") + case .insufficientStorage(let needed, let available): + return String(format: NSLocalizedString("存储空间不足: 需要 %lld字节, 可用 %lld字节", value: "存储空间不足: 需要 %lld字节, 可用 %lld字节", comment: "Insufficient storage"), needed, available) + case .fileSystemError(let operation, let error): + if let error = error { + return String(format: NSLocalizedString("文件系统错误(%@): %@", value: "文件系统错误(%@): %@", comment: "File system error with cause"), operation, error.localizedDescription) + } + return String(format: NSLocalizedString("文件系统错误: %@", value: "文件系统错误: %@", comment: "File system error"), operation) + case .fileExists(let path): + return String(format: NSLocalizedString("文件已存在: %@", value: "文件已存在: %@", comment: "File exists"), path) + case .fileNotFound(let path): + return String(format: NSLocalizedString("文件不存在: %@", value: "文件不存在: %@", comment: "File not found"), path) + case .filePermissionDenied(let path): + return String(format: NSLocalizedString("文件访问权限被拒绝: %@", value: "文件访问权限被拒绝: %@", comment: "File permission denied"), path) + case .applicationInfoError(let message, let error): + if let error = error { + return String(format: NSLocalizedString("应用信息错误(%@): %@", value: "应用信息错误(%@): %@", comment: "Application info error with cause"), message, error.localizedDescription) + } + return String(format: NSLocalizedString("应用信息错误: %@", value: "应用信息错误: %@", comment: "Application info error"), message) + case .unsupportedPlatform(let platform): + return String(format: NSLocalizedString("不支持的平台: %@", value: "不支持的平台: %@", comment: "Unsupported platform"), platform) + case .incompatibleVersion(let current, let required): + return String(format: NSLocalizedString("版本不兼容: 当前版本 %@, 需要版本 %@", value: "版本不兼容: 当前版本 %@, 需要版本 %@", comment: "Incompatible version"), current, required) + case .cancelled: + return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled") + case .installError(let message): + return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message) } } @@ -334,30 +333,31 @@ enum DownloadStatus: Equatable, Codable { var description: String { switch self { case .waiting: - return NSLocalizedString("等待中", comment: "Download status waiting") + return NSLocalizedString("等待中", value: "等待中", comment: "Download status waiting") case .preparing(let info): - return NSLocalizedString("准备中: \(info.message)", comment: "Download status preparing") + return String(format: NSLocalizedString("准备中: %@", value: "准备中: %@", comment: "Download status preparing"), info.message) case .downloading(let info): - return String(format: NSLocalizedString("正在下载 %@ (%d/%d)", comment: "Download status downloading"), + return String(format: NSLocalizedString("正在下载 %@ (%d/%d)", value: "正在下载 %@ (%d/%d)", comment: "Download status downloading"), info.fileName, info.currentPackageIndex + 1, info.totalPackages) case .paused(let info): switch info.reason { case .userRequested: - return NSLocalizedString("已暂停", comment: "Download status paused") + return NSLocalizedString("已暂停", value: "已暂停", comment: "Download status paused") case .networkIssue: - return NSLocalizedString("网络中断", comment: "Download status network paused") + return NSLocalizedString("网络中断", value: "网络中断", comment: "Download status network paused") case .systemSleep: - return NSLocalizedString("系统休眠", comment: "Download status system sleep") + return NSLocalizedString("系统休眠", value: "系统休眠", comment: "Download status system sleep") case .other(let reason): - return NSLocalizedString("已暂停: \(reason)", comment: "Download status paused with reason") + return String(format: NSLocalizedString("已暂停: %@", value: "已暂停: %@", comment: "Download status paused with reason"), reason) } case .completed(let info): - let duration = formatDuration(info.totalTime) - return NSLocalizedString("已完成 (用时: \(duration))", comment: "Download status completed") + return String(format: NSLocalizedString("已完成 (用时: %@)", value: "已完成 (用时: %@)", comment: "Download status completed"), + info.totalTime.formatDuration()) case .failed(let info): - return NSLocalizedString("失败: \(info.message)", comment: "Download status failed") + return String(format: NSLocalizedString("失败: %@", value: "失败: %@", comment: "Download status failed"), + info.message) case .retrying(let info): - return String(format: NSLocalizedString("重试中 (%d/%d)", comment: "Download status retrying"), + return String(format: NSLocalizedString("重试中 (%d/%d)", value: "重试中 (%d/%d)", comment: "Download status retrying"), info.attempt, info.maxAttempts) } } @@ -393,12 +393,10 @@ enum DownloadStatus: Equatable, Codable { } var canRetry: Bool { - switch self { - case .failed(let info): + if case .failed(let info) = self { return info.recoverable - default: - return false } + return false } var canPause: Bool { @@ -411,92 +409,26 @@ enum DownloadStatus: Equatable, Codable { } var canResume: Bool { - switch self { - case .paused(let info): + if case .paused(let info) = self { return info.resumable - default: - return false } + return false } } -extension DownloadStatus { - static func == (lhs: DownloadStatus, rhs: DownloadStatus) -> Bool { - switch (lhs, rhs) { - case (.waiting, .waiting): - return true - case (.preparing(let lInfo), .preparing(let rInfo)): - return lInfo.message == rInfo.message && - lInfo.timestamp == rInfo.timestamp && - lInfo.stage == rInfo.stage - case (.downloading(let lInfo), .downloading(let rInfo)): - return lInfo.fileName == rInfo.fileName && - lInfo.currentPackageIndex == rInfo.currentPackageIndex && - lInfo.totalPackages == rInfo.totalPackages - case (.paused(let lInfo), .paused(let rInfo)): - return lInfo.reason == rInfo.reason && - lInfo.timestamp == rInfo.timestamp && - lInfo.resumable == rInfo.resumable - case (.completed(let lInfo), .completed(let rInfo)): - return lInfo.timestamp == rInfo.timestamp && - lInfo.totalTime == rInfo.totalTime && - lInfo.totalSize == rInfo.totalSize - case (.failed(let lInfo), .failed(let rInfo)): - return lInfo.message == rInfo.message && - lInfo.timestamp == rInfo.timestamp && - lInfo.recoverable == rInfo.recoverable - case (.retrying(let lInfo), .retrying(let rInfo)): - return lInfo.attempt == rInfo.attempt && - lInfo.maxAttempts == rInfo.maxAttempts && - lInfo.reason == rInfo.reason && - lInfo.nextRetryDate == rInfo.nextRetryDate - default: - return false - } - } -} +extension DownloadStatus.PrepareInfo: Equatable {} +extension DownloadStatus.PrepareInfo.PrepareStage: Equatable {} +extension DownloadStatus.PauseInfo.PauseReason: Equatable {} +extension DownloadStatus.DownloadInfo: Equatable {} +extension DownloadStatus.PauseInfo: Equatable {} +extension DownloadStatus.CompletionInfo: Equatable {} +extension DownloadStatus.RetryInfo: Equatable {} -extension DownloadStatus.PrepareInfo: Equatable { - static func == (lhs: DownloadStatus.PrepareInfo, rhs: DownloadStatus.PrepareInfo) -> Bool { +extension DownloadStatus.FailureInfo: Equatable { + static func == (lhs: DownloadStatus.FailureInfo, rhs: DownloadStatus.FailureInfo) -> Bool { return lhs.message == rhs.message && lhs.timestamp == rhs.timestamp && - lhs.stage == rhs.stage - } -} - -extension DownloadStatus.PrepareInfo.PrepareStage: Equatable { - static func == (lhs: DownloadStatus.PrepareInfo.PrepareStage, rhs: DownloadStatus.PrepareInfo.PrepareStage) -> Bool { - switch (lhs, rhs) { - case (.initializing, .initializing): - return true - case (.creatingInstaller, .creatingInstaller): - return true - case (.signingApp, .signingApp): - return true - case (.fetchingInfo, .fetchingInfo): - return true - case (.validatingSetup, .validatingSetup): - return true - default: - return false - } - } -} - -extension DownloadStatus.PauseInfo.PauseReason: Equatable { - static func == (lhs: DownloadStatus.PauseInfo.PauseReason, rhs: DownloadStatus.PauseInfo.PauseReason) -> Bool { - switch (lhs, rhs) { - case (.userRequested, .userRequested): - return true - case (.networkIssue, .networkIssue): - return true - case (.systemSleep, .systemSleep): - return true - case (.other(let lhsReason), .other(let rhsReason)): - return lhsReason == rhsReason - default: - return false - } + lhs.recoverable == rhs.recoverable } } @@ -508,13 +440,9 @@ enum LoadingState: Equatable { static func == (lhs: LoadingState, rhs: LoadingState) -> Bool { switch (lhs, rhs) { - case (.idle, .idle): + case (.idle, .idle), (.loading, .loading), (.success, .success): return true - case (.loading, .loading): - return true - case (.success, .success): - return true - case (.failed(let lError), .failed(let rError)): + case let (.failed(lError), .failed(rError)): return lError.localizedDescription == rError.localizedDescription default: return false @@ -522,60 +450,19 @@ enum LoadingState: Equatable { } } -private func formatDuration(_ seconds: TimeInterval) -> String { - if seconds < 60 { - return String(format: "%.1f秒", seconds) - } else if seconds < 3600 { - let minutes = Int(seconds / 60) - let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60)) - return "\(minutes)分\(remainingSeconds)秒" - } else { - let hours = Int(seconds / 3600) - let minutes = Int((seconds.truncatingRemainder(dividingBy: 3600)) / 60) - let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60)) - return "\(hours)小时\(minutes)分\(remainingSeconds)秒" - } -} - -extension DownloadStatus.DownloadInfo: Equatable { - static func == (lhs: DownloadStatus.DownloadInfo, rhs: DownloadStatus.DownloadInfo) -> Bool { - return lhs.fileName == rhs.fileName && - lhs.currentPackageIndex == rhs.currentPackageIndex && - lhs.totalPackages == rhs.totalPackages && - lhs.startTime == rhs.startTime && - lhs.estimatedTimeRemaining == rhs.estimatedTimeRemaining - } -} - -extension DownloadStatus.PauseInfo: Equatable { - static func == (lhs: DownloadStatus.PauseInfo, rhs: DownloadStatus.PauseInfo) -> Bool { - return lhs.reason == rhs.reason && - lhs.timestamp == rhs.timestamp && - lhs.resumable == rhs.resumable - } -} - -extension DownloadStatus.CompletionInfo: Equatable { - static func == (lhs: DownloadStatus.CompletionInfo, rhs: DownloadStatus.CompletionInfo) -> Bool { - return lhs.timestamp == rhs.timestamp && - lhs.totalTime == rhs.totalTime && - lhs.totalSize == rhs.totalSize - } -} - -extension DownloadStatus.FailureInfo: Equatable { - static func == (lhs: DownloadStatus.FailureInfo, rhs: DownloadStatus.FailureInfo) -> Bool { - return lhs.message == rhs.message && - lhs.timestamp == rhs.timestamp && - lhs.recoverable == rhs.recoverable - } -} - -extension DownloadStatus.RetryInfo: Equatable { - static func == (lhs: DownloadStatus.RetryInfo, rhs: DownloadStatus.RetryInfo) -> Bool { - return lhs.attempt == rhs.attempt && - lhs.maxAttempts == rhs.maxAttempts && - lhs.reason == rhs.reason && - lhs.nextRetryDate == rhs.nextRetryDate +private extension TimeInterval { + func formatDuration() -> String { + if self < 60 { + return String(format: "%.1f秒", self) + } else if self < 3600 { + let minutes = Int(self / 60) + let remainingSeconds = Int(self.truncatingRemainder(dividingBy: 60)) + return "\(minutes)分\(remainingSeconds)秒" + } else { + let hours = Int(self / 3600) + let minutes = Int((self.truncatingRemainder(dividingBy: 3600)) / 60) + let remainingSeconds = Int(self.truncatingRemainder(dividingBy: 60)) + return "\(hours)小时\(minutes)分\(remainingSeconds)秒" + } } } diff --git a/Adobe Downloader/Commons/Extensions.swift b/Adobe Downloader/Commons/Extensions.swift index 3faf826..555bb4d 100644 --- a/Adobe Downloader/Commons/Extensions.swift +++ b/Adobe Downloader/Commons/Extensions.swift @@ -9,20 +9,13 @@ import AppKit extension NewDownloadTask { var startTime: Date { switch totalStatus { - case .downloading(let info): - return info.startTime - case .completed(let info): - return info.timestamp.addingTimeInterval(-info.totalTime) - case .preparing(let info): - return info.timestamp - case .paused(let info): - return info.timestamp - case .failed(let info): - return info.timestamp - case .retrying(let info): - return info.nextRetryDate.addingTimeInterval(-60) - case .waiting, .none: - return createAt + case .downloading(let info): return info.startTime + case .completed(let info): return info.timestamp - info.totalTime + case .preparing(let info): return info.timestamp + case .paused(let info): return info.timestamp + case .failed(let info): return info.timestamp + case .retrying(let info): return info.nextRetryDate - 60 + case .waiting, .none: return createAt } } } @@ -30,36 +23,40 @@ extension NewDownloadTask { extension NetworkManager { func configureNetworkMonitor() { monitor.pathUpdateHandler = { [weak self] path in - Task { @MainActor in - guard let self = self else { return } + Task { @MainActor [weak self] in + guard let self else { return } let wasConnected = self.isConnected self.isConnected = path.status == .satisfied - - if !wasConnected && self.isConnected { - for task in self.downloadTasks { - if case .paused(let info) = task.status, - info.reason == .networkIssue { - await self.downloadUtils.resumeDownloadTask(taskId: task.id) - } - } - } else if wasConnected && !self.isConnected { - for task in self.downloadTasks { - if case .downloading = task.status { - await self.downloadUtils.pauseDownloadTask( - taskId: task.id, - reason: .networkIssue - ) - } - } + switch (wasConnected, self.isConnected) { + case (false, true): await resumePausedTasks() + case (true, false): await pauseActiveTasks() + default: break } } } - monitor.start(queue: DispatchQueue.global(qos: .utility)) + monitor.start(queue: .global(qos: .utility)) + } + + private func resumePausedTasks() async { + for task in downloadTasks { + if case .paused(let info) = task.status, + info.reason == .networkIssue { + await downloadUtils.resumeDownloadTask(taskId: task.id) + } + } + } + + private func pauseActiveTasks() async { + for task in downloadTasks { + if case .downloading = task.status { + await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue) + } + } } func generateCookie() -> String { - let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - let randomString = String((0..<26).map { _ in letters.randomElement()! }) - return "fg=\(randomString)======" + let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let randomString = (0..<26).map { _ in chars.randomElement()! } + return "fg=\(String(randomString))======" } } diff --git a/Adobe Downloader/ContentView.swift b/Adobe Downloader/ContentView.swift index f1ca21d..37e6394 100644 --- a/Adobe Downloader/ContentView.swift +++ b/Adobe Downloader/ContentView.swift @@ -6,7 +6,15 @@ struct ContentView: View { @State private var errorMessage: String? @State private var showDownloadManager = false @State private var searchText = "" - @AppStorage("apiVersion") private var apiVersion: String = "6" + @State private var currentApiVersion = StorageData.shared.apiVersion + + private var apiVersion: String { + get { StorageData.shared.apiVersion } + set { + StorageData.shared.apiVersion = newValue + refreshData() + } + } private var filteredProducts: [Sap] { let products = networkManager.saps.values @@ -36,18 +44,37 @@ struct ContentView: View { .fixedSize() Spacer() + + HStack { + Toggle(isOn: Binding( + get: { StorageData.shared.downloadAppleSilicon }, + set: { newValue in + StorageData.shared.downloadAppleSilicon = newValue + networkManager.updateAllowedPlatform(useAppleSilicon: newValue) + Task { + await networkManager.fetchProducts() + } + } + )) { + Text("Apple Silicon") + } + .toggleStyle(.switch) + .tint(.green) + .disabled(isRefreshing) + } + .padding(.horizontal, 10) HStack { Text("API:") - .foregroundColor(.secondary) - Picker("", selection: $apiVersion) { + Picker("", selection: $currentApiVersion) { Text("v4").tag("4") Text("v5").tag("5") Text("v6").tag("6") } .pickerStyle(.segmented) .frame(width: 150) - .onChange(of: apiVersion) { newValue in + .onChange(of: currentApiVersion) { newValue in + StorageData.shared.apiVersion = newValue refreshData() } } diff --git a/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift b/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift index f93abe2..930c2b3 100644 --- a/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift +++ b/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift @@ -31,26 +31,56 @@ class PrivilegedHelperManager: NSObject { private var useLegacyInstall = false private var connection: NSXPCConnection? - @Published private(set) var connectionState: ConnectionState = .disconnected + @Published private(set) var connectionState: ConnectionState = .disconnected { + didSet { + if oldValue != connectionState { + print("Helper 连接状态: \(connectionState.description)") + if connectionState == .disconnected { + connection?.invalidate() + connection = nil + } + } + } + } enum ConnectionState { case connected case disconnected case connecting + + var description: String { + switch self { + case .connected: + return String(localized: "已连接") + case .disconnected: + return String(localized: "未连接") + case .connecting: + return String(localized: "正在连接") + } + } } + private var isInitializing = false + + private let connectionQueue = DispatchQueue(label: "com.x1a0he.helper.connection") + override init() { super.init() initAuthorizationRef() - - DispatchQueue.main.async { [weak self] in - _ = self?.connectToHelper() - } + setupAutoReconnect() } func checkInstall() { + if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, + let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") { + if currentBuild != installedBuild { + notifyInstall() + return + } + } + getHelperStatus { [weak self] status in - guard let self = self else {return} + guard let self = self else { return } switch status { case .noFound: if #available(macOS 13, *) { @@ -58,8 +88,8 @@ class PrivilegedHelperManager: NSObject { let status = SMAppService.statusForLegacyPlist(at: url) if status == .requiresApproval { let alert = NSAlert() - let notice = "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件,请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App" - let addition = "如果在设置里没找到当前App,可以尝试重置守护程序" + let notice = String(localized: "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件,请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App") + let addition = String(localized: "如果在设置里没找到当前App,可以尝试重置守护程序") alert.messageText = notice + "\n" + addition alert.addButton(withTitle: "打开系统登录项设置") alert.addButton(withTitle: "重置守护程序") @@ -124,8 +154,12 @@ class PrivilegedHelperManager: NSObject { NSAlert.alert(with: "SMJobBless failed with error: \(blessError)\nError domain: \(nsError.domain)\nError code: \(nsError.code)\nError description: \(nsError.localizedDescription)\nError user info: \(nsError.userInfo)") return .blessError(nsError.code) } + return .blessError(-1) + } + + if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { + UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild") } - return .success } @@ -133,26 +167,32 @@ class PrivilegedHelperManager: NSObject { var called = false let reply: ((HelperStatus) -> Void) = { status in - if called {return} + if called { return } called = true callback(status) } let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName) guard - let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any], - let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String else { + CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { reply(.noFound) return } + let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") if !helperFileExists { reply(.noFound) return } + + if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, + let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild"), + currentBuild != installedBuild { + reply(.needUpdate) + return + } reply(.installed) - } static var getHelperStatus: Bool { @@ -163,7 +203,6 @@ class PrivilegedHelperManager: NSObject { status = helperStatus == .installed semaphore.signal() } - semaphore.wait() return status } @@ -178,30 +217,30 @@ class PrivilegedHelperManager: NSObject { guard let self = self else { return } guard let connection = connectToHelper() else { - completion(false, "无法连接到Helper") + completion(false, String(localized: "无法连接到Helper")) return } guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else { - completion(false, "无法获取Helper代理") + completion(false, String(localized: "无法获取Helper代理")) return } helper.executeCommand("whoami") { result in if result.contains("root") { - completion(true, "Helper 重新安装成功") + completion(true, String(localized: "Helper 重新安装成功")) } else { - completion(false, "Helper未能获取root权限") + completion(false, String(localized: "Helper 安装失败")) } } } case .authorizationFail: - completion(false, "获取授权失败") + completion(false, String(localized: "获取授权失败")) case .getAdminFail: - completion(false, "获取管理员权限失败") + completion(false, String(localized: "获取管理员权限失败")) case .blessError(_): - completion(false, "安装失败: \(result.alertContent)") + completion(false, String(localized: "安装失败: \(result.alertContent)")) } } @@ -210,118 +249,119 @@ class PrivilegedHelperManager: NSObject { try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist") } - private func connectToHelper() -> NSXPCConnection? { + func connectToHelper() -> NSXPCConnection? { connectionState = .connecting - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - if let existingConnection = connection, - existingConnection.remoteObjectProxy != nil { - connectionState = .connected - return existingConnection - } - - connection?.invalidate() - connection = nil - - let newConnection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, - options: .privileged) - - newConnection.remoteObjectInterface = NSXPCInterface(with: HelperToolProtocol.self) - - newConnection.interruptionHandler = { [weak self] in - DispatchQueue.main.async { - self?.connectionState = .disconnected - self?.connection?.invalidate() - self?.connection = nil - } - } - - newConnection.invalidationHandler = { [weak self] in - DispatchQueue.main.async { - self?.connectionState = .disconnected - self?.connection?.invalidate() - self?.connection = nil - } - } - - newConnection.resume() - connection = newConnection - - if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol { - helper.executeCommand("whoami") { [weak self] result in - if result == "root" { - DispatchQueue.main.async { - self?.connectionState = .connected - } - } else { - DispatchQueue.main.async { - self?.connectionState = .disconnected - } + return connectionQueue.sync { + connection?.invalidate() + connection = nil + + let newConnection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, + options: .privileged) + + newConnection.remoteObjectInterface = NSXPCInterface(with: HelperToolProtocol.self) + + newConnection.interruptionHandler = { [weak self] in + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection?.invalidate() + self?.connection = nil } } + + newConnection.invalidationHandler = { [weak self] in + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection?.invalidate() + self?.connection = nil + } + } + + newConnection.resume() + connection = newConnection + + let semaphore = DispatchSemaphore(value: 0) + var isConnected = false + + if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol { + helper.executeCommand("whoami") { [weak self] result in + if result == "root" { + isConnected = true + DispatchQueue.main.async { + self?.connectionState = .connected + } + } else { + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection?.invalidate() + self?.connection = nil + } + } + semaphore.signal() + } + + _ = semaphore.wait(timeout: .now() + 1.0) + + if !isConnected { + connectionState = .disconnected + connection?.invalidate() + connection = nil + return nil + } + } else { + connectionState = .disconnected + return nil + } + + return connection } - - return newConnection } func executeCommand(_ command: String, completion: @escaping (String) -> Void) { - guard let connection = connectToHelper() else { - connectionState = .disconnected - completion("Error: Could not connect to helper") - return - } - - guard let helper = connection.remoteObjectProxyWithErrorHandler({ error in - self.connectionState = .disconnected - }) as? HelperToolProtocol else { - connectionState = .disconnected - completion("Error: Could not get helper proxy") - return - } - - helper.executeCommand(command) { [weak self] result in - DispatchQueue.main.async { - if self?.connection == nil { - self?.connectionState = .disconnected - completion("Error: Connection lost") - return + do { + let helper = try getHelperProxy() + helper.executeCommand(command) { [weak self] result in + DispatchQueue.main.async { + if result.starts(with: "Error:") { + self?.connectionState = .disconnected + } else { + self?.connectionState = .connected + } + completion(result) } - - if result.starts(with: "Error:") { - self?.connectionState = .disconnected - } else { - self?.connectionState = .connected - } - - completion(result) } + } catch { + connectionState = .disconnected + completion("Error: \(error.localizedDescription)") } } func reconnectHelper(completion: @escaping (Bool, String) -> Void) { + connectionState = .disconnected connection?.invalidate() connection = nil - guard let newConnection = connectToHelper() else { - print("重新连接失败") - completion(false, "无法连接到 Helper") - return - } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + + guard let newConnection = self.connectToHelper() else { + completion(false, String(localized: "无法连接到 Helper")) + return + } - guard let helper = newConnection.remoteObjectProxyWithErrorHandler({ error in - completion(false, "连接出现错误: \(error.localizedDescription)") - }) as? HelperToolProtocol else { - completion(false, "无法获取 Helper 代理") - return - } + guard let helper = newConnection.remoteObjectProxyWithErrorHandler({ error in + completion(false, String(localized: "连接出现错误: \(error.localizedDescription)")) + }) as? HelperToolProtocol else { + completion(false, String(localized: "无法获取 Helper 代理")) + return + } - helper.executeCommand("whoami") { result in - if result == "root" { - completion(true, "Helper 重新连接成功") - } else { - completion(false, "Helper 响应异常") + helper.executeCommand("whoami") { result in + if result == "root" { + completion(true, String(localized: "Helper 重新连接成功")) + } else { + completion(false, String(localized: "Helper 响应异常")) + } } } } @@ -363,15 +403,27 @@ class PrivilegedHelperManager: NSObject { try await Task.sleep(nanoseconds: 100_000_000) } } + + func forceReinstallHelper() { + guard !isInitializing else { return } + isInitializing = true + + removeInstallHelper() + notifyInstall() + + isInitializing = false + } + + func disconnectHelper() { + connection?.invalidate() + connection = nil + connectionState = .disconnected + } } extension PrivilegedHelperManager { private func notifyInstall() { - if useLegacyInstall { - useLegacyInstall = false - checkInstall() - return - } + guard !isInitializing else { return } let result = installHelperDaemon() if case .success = result { @@ -385,7 +437,7 @@ extension PrivilegedHelperManager { if !isCancle, useLegacyInstall { checkInstall() } else if isCancle, !useLegacyInstall { - NSAlert.alert(with: "获取管理员授权失败,用户主动取消授权!") + NSAlert.alert(with: String(localized: "获取管理员授权失败,用户主动取消授权!")) } } } @@ -459,3 +511,54 @@ extension NSAlert { alert.runModal() } } + +extension PrivilegedHelperManager { + private func setupAutoReconnect() { + Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + if self.connectionState == .disconnected { + print("尝试重新连接 Helper...") + _ = self.connectToHelper() + } + } + } +} + +enum HelperError: LocalizedError { + case connectionFailed + case proxyError + case authorizationFailed + case installationFailed(String) + + var errorDescription: String? { + switch self { + case .connectionFailed: + return "无法连接到 Helper" + case .proxyError: + return "无法获取 Helper 代理" + case .authorizationFailed: + return "获取授权失败" + case .installationFailed(let reason): + return "安装失败: \(reason)" + } + } +} + +extension PrivilegedHelperManager { + private func getHelperProxy() throws -> HelperToolProtocol { + if connectionState != .connected { + guard let newConnection = connectToHelper() else { + throw HelperError.connectionFailed + } + connection = newConnection + } + + guard let helper = connection?.remoteObjectProxyWithErrorHandler({ [weak self] error in + self?.connectionState = .disconnected + }) as? HelperToolProtocol else { + throw HelperError.proxyError + } + + return helper + } +} diff --git a/Adobe Downloader/Info.plist b/Adobe Downloader/Info.plist index 9e02583..9891aff 100644 --- a/Adobe Downloader/Info.plist +++ b/Adobe Downloader/Info.plist @@ -12,7 +12,7 @@ SMPrivilegedExecutables com.x1a0he.macOS.Adobe-Downloader.helper - identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he0907@gmail.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ + identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ SUFeedURL https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml diff --git a/Adobe Downloader/NetworkManager.swift b/Adobe Downloader/NetworkManager.swift index 0a602c0..dda5cb2 100644 --- a/Adobe Downloader/NetworkManager.swift +++ b/Adobe Downloader/NetworkManager.swift @@ -23,9 +23,21 @@ class NetworkManager: ObservableObject { internal var monitor = NWPathMonitor() internal var isFetchingProducts = false private let installManager = InstallManager() - @AppStorage("defaultDirectory") private var defaultDirectory: String = "" - @AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true - @AppStorage("apiVersion") private var apiVersion: String = "6" + + private var defaultDirectory: String { + get { StorageData.shared.defaultDirectory } + set { StorageData.shared.defaultDirectory = newValue } + } + + private var useDefaultDirectory: Bool { + get { StorageData.shared.useDefaultDirectory } + set { StorageData.shared.useDefaultDirectory = newValue } + } + + private var apiVersion: String { + get { StorageData.shared.apiVersion } + set { StorageData.shared.apiVersion = newValue } + } enum InstallationState { case idle @@ -38,8 +50,9 @@ class NetworkManager: ObservableObject { init(networkService: NetworkService = NetworkService(), downloadUtils: DownloadUtils? = nil) { - let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon") - self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"] + self.allowedPlatform = StorageData.shared.downloadAppleSilicon ? + ["macuniversal", "macarm64"] : + ["macuniversal", "osx10-64", "osx10"] self.networkService = networkService self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker) @@ -51,7 +64,10 @@ class NetworkManager: ObservableObject { func fetchProducts() async { loadingState = .loading do { - let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(version: apiVersion) + let (saps, cdn, sapCodes) = try await networkService.fetchProductsData( + version: apiVersion, + platform: allowedPlatform.joined(separator: ",") + ) await MainActor.run { self.saps = saps self.cdn = cdn @@ -68,7 +84,7 @@ class NetworkManager: ObservableObject { guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else { throw NetworkError.invalidData("无法获取产品信息") } - + print(productInfo.apPlatform) let task = NewDownloadTask( sapCode: sap.sapCode, version: selectedVersion, @@ -157,7 +173,7 @@ class NetworkManager: ObservableObject { while retryCount < maxRetries { do { - let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(version: apiVersion) + let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(version: apiVersion, platform: allowedPlatform.joined(separator: ",")) await MainActor.run { self.saps = saps @@ -345,11 +361,14 @@ class NetworkManager: ObservableObject { } func updateAllowedPlatform(useAppleSilicon: Bool) { - allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"] + allowedPlatform = useAppleSilicon ? + ["macuniversal", "macarm64"] : + ["macuniversal", "osx10-64", "osx10"] } func saveTask(_ task: NewDownloadTask) { TaskPersistenceManager.shared.saveTask(task) + objectWillChange.send() } func loadSavedTasks() { diff --git a/Adobe Downloader/Services/NetworkService.swift b/Adobe Downloader/Services/NetworkService.swift index add4752..a5bafef 100644 --- a/Adobe Downloader/Services/NetworkService.swift +++ b/Adobe Downloader/Services/NetworkService.swift @@ -1,7 +1,9 @@ import Foundation class NetworkService { - func fetchProductsData(version: String) async throws -> ([String: Sap], String, [SapCodes]) { + typealias ProductsData = (products: [String: Sap], cdn: String, sapCodes: [SapCodes]) + + private func makeProductsURL(version: String) throws -> URL { var components = URLComponents(string: NetworkConstants.productsXmlURL) components?.queryItems = [ URLQueryItem(name: "_type", value: "xml"), @@ -15,10 +17,19 @@ class NetworkService { guard let url = components?.url else { throw NetworkError.invalidURL(NetworkConstants.productsXmlURL) } + print(url) + return url + } + private func configureRequest(_ request: inout URLRequest, headers: [String: String]) { + headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + } + + func fetchProductsData(version: String, platform: String) async throws -> ProductsData { + let url = try makeProductsURL(version: version) var request = URLRequest(url: url) request.httpMethod = "GET" - NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders) let (data, response) = try await URLSession.shared.data(for: request) @@ -34,7 +45,7 @@ class NetworkService { throw NetworkError.invalidData("无法解码XML数据") } - let result: ([String: Sap], String, [SapCodes]) = try await Task.detached(priority: .userInitiated) { + let result: ProductsData = try await Task.detached(priority: .userInitiated) { let parseResult = try XHXMLParser.parse(xmlString: xmlString) let products = parseResult.products, cdn = parseResult.cdn var sapCodes: [SapCodes] = [] @@ -98,4 +109,4 @@ class NetworkService { let random = Int.random(in: 100000...999999) return "s_cc=true; s_sq=; AMCV_9E1005A551ED61CA0A490D45%40AdobeOrg=1075005958%7CMCIDTS%7C\(timestamp)%7CMCMID%7C\(random)%7CMCAAMLH-1683925272%7C11%7CMCAAMB-1683925272%7CRKhpRz8krg2tLO6pguXWp5olkAcUniQYPHaMWWgdJ3xzPWQmdj0y%7CMCOPTOUT-1683327672s%7CNONE%7CvVersion%7C4.4.1; gpv=cc-search-desktop; s_ppn=cc-search-desktop" } -} \ No newline at end of file +} diff --git a/Adobe Downloader/Storages/StorageData.swift b/Adobe Downloader/Storages/StorageData.swift new file mode 100644 index 0000000..20d1ae2 --- /dev/null +++ b/Adobe Downloader/Storages/StorageData.swift @@ -0,0 +1,125 @@ +// +// StorageData.swift +// Adobe Downloader +// +// Created by X1a0He on 11/14/24. +// + +import SwiftUI + +final class StorageData: ObservableObject { + static let shared = StorageData() + + @Published var installedHelperBuild: String { + didSet { + UserDefaults.standard.set(installedHelperBuild, forKey: "InstalledHelperBuild") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var downloadAppleSilicon: Bool { + didSet { + UserDefaults.standard.set(downloadAppleSilicon, forKey: "downloadAppleSilicon") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var useDefaultLanguage: Bool { + didSet { + UserDefaults.standard.set(useDefaultLanguage, forKey: "useDefaultLanguage") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var defaultLanguage: String { + didSet { + UserDefaults.standard.set(defaultLanguage, forKey: "defaultLanguage") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var useDefaultDirectory: Bool { + didSet { + UserDefaults.standard.set(useDefaultDirectory, forKey: "useDefaultDirectory") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var defaultDirectory: String { + didSet { + UserDefaults.standard.set(defaultDirectory, forKey: "defaultDirectory") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var confirmRedownload: Bool { + didSet { + UserDefaults.standard.set(confirmRedownload, forKey: "confirmRedownload") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var apiVersion: String { + didSet { + UserDefaults.standard.set(apiVersion, forKey: "apiVersion") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + @Published var isFirstLaunch: Bool { + didSet { + UserDefaults.standard.set(isFirstLaunch, forKey: "isFirstLaunch") + objectWillChange.send() + NotificationCenter.default.post(name: .storageDidChange, object: nil) + } + } + + private init() { + self.installedHelperBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") ?? "0" + self.downloadAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon") + self.useDefaultLanguage = UserDefaults.standard.bool(forKey: "useDefaultLanguage") + self.defaultLanguage = UserDefaults.standard.string(forKey: "defaultLanguage") ?? "ALL" + self.useDefaultDirectory = UserDefaults.standard.bool(forKey: "useDefaultDirectory") + self.defaultDirectory = UserDefaults.standard.string(forKey: "defaultDirectory") ?? "" + self.confirmRedownload = UserDefaults.standard.bool(forKey: "confirmRedownload") + self.apiVersion = UserDefaults.standard.string(forKey: "apiVersion") ?? "6" + self.isFirstLaunch = UserDefaults.standard.bool(forKey: "isFirstLaunch") + } +} + +@propertyWrapper +struct StorageValue: DynamicProperty { + @ObservedObject private var storage = StorageData.shared + private let keyPath: ReferenceWritableKeyPath + + var wrappedValue: T { + get { storage[keyPath: keyPath] } + nonmutating set { + storage[keyPath: keyPath] = newValue + } + } + + var projectedValue: Binding { + Binding( + get: { storage[keyPath: keyPath] }, + set: { storage[keyPath: keyPath] = $0 } + ) + } + + init(_ keyPath: ReferenceWritableKeyPath) { + self.keyPath = keyPath + } +} + +extension Notification.Name { + static let storageDidChange = Notification.Name("storageDidChange") +} + diff --git a/Adobe Downloader/Utils/DownloadUtils.swift b/Adobe Downloader/Utils/DownloadUtils.swift index 11cf360..308c42b 100644 --- a/Adobe Downloader/Utils/DownloadUtils.swift +++ b/Adobe Downloader/Utils/DownloadUtils.swift @@ -26,6 +26,8 @@ class DownloadUtils { var fileName: String private var hasCompleted = false private let completionLock = NSLock() + private var lastUpdateTime = Date() + private var lastBytes: Int64 = 0 init(destinationDirectory: URL, fileName: String, @@ -92,14 +94,30 @@ class DownloadUtils { totalBytesExpectedToWrite: Int64) { guard totalBytesExpectedToWrite > 0 else { return } guard bytesWritten > 0 else { return } - - progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + + handleProgressUpdate( + bytesWritten: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite + ) } func cleanup() { completionHandler = { _, _, _ in } progressHandler = nil } + + private func handleProgressUpdate(bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + let now = Date() + let timeDiff = now.timeIntervalSince(lastUpdateTime) + + guard timeDiff >= NetworkConstants.progressUpdateInterval else { return } + + progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + + lastUpdateTime = now + lastBytes = totalBytesWritten + } } func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async { @@ -194,32 +212,15 @@ class DownloadUtils { """ } - func clearExtendedAttributes(at url: URL) async throws { - let escapedPath = url.path.replacingOccurrences(of: "'", with: "'\\''") - let script = """ - do shell script "sudo xattr -cr '\(escapedPath)'" with administrator privileges - """ - - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") - process.arguments = ["-e", script] - - let pipe = Pipe() - process.standardOutput = pipe - process.standardError = pipe - - do { - try process.run() - process.waitUntilExit() - - if process.terminationStatus != 0 { - let data = try pipe.fileHandleForReading.readToEnd() ?? Data() - if let output = String(data: data, encoding: .utf8) { - print("xattr command output:", output) + private func executePrivilegedCommand(_ command: String) async throws -> String { + return await withCheckedContinuation { continuation in + PrivilegedHelperManager.shared.executeCommand(command) { result in + if result.starts(with: "Error:") { + continuation.resume(returning: result) + } else { + continuation.resume(returning: result) } } - } catch { - print("Error executing xattr command:", error.localizedDescription) } } @@ -505,11 +506,6 @@ class DownloadUtils { let (manifestData, _) = try await URLSession.shared.data(for: request) - #if DEBUG - if let manifestString = String(data: manifestData, encoding: .utf8) { - print("Manifest内容: \(manifestString)") - } - #endif let manifestDoc = try XMLDocument(data: manifestData) guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue, @@ -662,28 +658,23 @@ class DownloadUtils { for dependency in productInfo.dependencies { if let dependencyVersions = saps[dependency.sapCode]?.versions { - let sortedVersions = dependencyVersions.sorted { first, second in - first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending + + let matchingVersions = dependencyVersions.filter { + $0.value.baseVersion == dependency.version } - var firstGuid = "", buildGuid = "" - for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version { - if firstGuid.isEmpty { firstGuid = versionInfo.buildGuid } - - if allowedPlatform.contains(versionInfo.apPlatform) { - buildGuid = versionInfo.buildGuid - break - } + var selectedVersion: (key: String, value: Sap.Versions)? = matchingVersions.first { + allowedPlatform.contains($0.value.apPlatform) } + + selectedVersion = selectedVersion ?? matchingVersions.first - if buildGuid.isEmpty { buildGuid = firstGuid } - - if !buildGuid.isEmpty { + if let version = selectedVersion { productsToDownload.append(ProductsToDownload( sapCode: dependency.sapCode, version: dependency.version, - buildGuid: buildGuid + buildGuid: version.value.buildGuid )) } } @@ -1132,4 +1123,67 @@ class DownloadUtils { throw error } } + + private func handleDownloadError(_ error: Error, task: URLSessionTask) -> Error { + let nsError = error as NSError + switch nsError.code { + case NSURLErrorCancelled: + return NetworkError.cancelled + case NSURLErrorTimedOut: + return NetworkError.timeout + case NSURLErrorNotConnectedToInternet: + return NetworkError.noConnection + case NSURLErrorCannotWriteToFile: + if let expectedSize = task.response?.expectedContentLength { + let fileManager = FileManager.default + if let availableSpace = try? fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())[.systemFreeSize] as? Int64 { + return NetworkError.insufficientStorage(expectedSize, availableSpace) + } + } + return NetworkError.downloadError("存储空间不足", error) + default: + return NetworkError.downloadError("下载失败: \(error.localizedDescription)", error) + } + } + + private func moveDownloadedFile(from location: URL, to destination: URL) throws { + let fileManager = FileManager.default + let destinationDirectory = destination.deletingLastPathComponent() + + do { + if !fileManager.fileExists(atPath: destinationDirectory.path) { + try fileManager.createDirectory(at: destinationDirectory, withIntermediateDirectories: true) + } + + if fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + + try fileManager.moveItem(at: location, to: destination) + } catch { + switch (error as NSError).code { + case NSFileWriteNoPermissionError: + throw NetworkError.filePermissionDenied(destination.path) + case NSFileWriteOutOfSpaceError: + throw NetworkError.insufficientStorage( + try fileManager.attributesOfItem(atPath: location.path)[.size] as? Int64 ?? 0, + try fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())[.systemFreeSize] as? Int64 ?? 0 + ) + default: + throw NetworkError.fileSystemError("移动文件失败", error) + } + } + } + + private func createDownloadTask(url: URL?, resumeData: Data?, session: URLSession) throws -> URLSessionDownloadTask { + if let resumeData = resumeData { + return session.downloadTask(withResumeData: resumeData) + } else if let url = url { + var request = URLRequest(url: url) + NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + return session.downloadTask(with: request) + } else { + throw NetworkError.invalidData("Neither URL nor resume data provided") + } + } } diff --git a/Adobe Downloader/Utils/InstallManager.swift b/Adobe Downloader/Utils/InstallManager.swift index 87d838f..eae59e2 100644 --- a/Adobe Downloader/Utils/InstallManager.swift +++ b/Adobe Downloader/Utils/InstallManager.swift @@ -5,9 +5,10 @@ // /* Adobe Exit Code - 107: 架构或者版本不一致 + 107: 架构不一致或安装文件被损坏 103: 权限问题 182: 可能是文件不全或者出错了 + 133: 磁盘空间不足 */ import Foundation @@ -32,6 +33,44 @@ actor InstallManager { private var progressHandler: ((Double, String) -> Void)? private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" + actor InstallationState { + var isCompleted = false + var error: Error? + var hasExitCode0 = false + var lastOutputTime = Date() + + func markCompleted() { + isCompleted = true + } + + func setError(_ error: Error) { + if !isCompleted { + self.error = error + isCompleted = true + } + } + + func setExitCode0() { + hasExitCode0 = true + } + + func updateLastOutputTime() { + lastOutputTime = Date() + } + + func getTimeSinceLastOutput() -> TimeInterval { + return Date().timeIntervalSince(lastOutputTime) + } + + var shouldContinue: Bool { + !isCompleted + } + + var hasReceivedExitCode0: Bool { + hasExitCode0 + } + } + private func executeInstallation( at appPath: URL, progressHandler: @escaping (Double, String) -> Void @@ -41,7 +80,7 @@ actor InstallManager { } let driverPath = appPath.appendingPathComponent("driver.xml").path - let installCommand = "\"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\"" + let installCommand = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\"" await MainActor.run { progressHandler(0.0, String(localized: "正在准备安装...")) @@ -64,7 +103,7 @@ actor InstallManager { let errorMessage: String switch exitCode { case 107: - errorMessage = String(localized: "安装失败: 架构或版本不一致 (退出代码: \(exitCode))") + errorMessage = String(localized: "安装失败: 架构或版本不一致 (退出代码: \(exitCode))") case 103: errorMessage = String(localized: "安装失败: 权限问题 (退出代码: \(exitCode))") case 182: @@ -123,6 +162,9 @@ actor InstallManager { at appPath: URL, progressHandler: @escaping (Double, String) -> Void ) async throws { + cancel() + try await Task.sleep(nanoseconds: 1_000_000_000) + try await executeInstallation( at: appPath, progressHandler: progressHandler diff --git a/Adobe Downloader/Utils/XHXMLParser.swift b/Adobe Downloader/Utils/XHXMLParser.swift index c44a5b3..86c8a36 100644 --- a/Adobe Downloader/Utils/XHXMLParser.swift +++ b/Adobe Downloader/Utils/XHXMLParser.swift @@ -11,29 +11,61 @@ struct ParseResult { } class XHXMLParser { + private static let xpathCache = [ + "cdn": "//channels/channel/cdn/secure", + "products": "//channels/channel/products/product", + "icons": "productIcons/icon", + "platforms": "platforms/platform", + "languageSet": "languageSet", + "dependencies": "dependencies/dependency", + "sapCode": "sapCode", + "baseVersion": "baseVersion", + "builds": "//builds/build", + "appVersion": "nglLicensingInfo/appVersion", + "manifestURL": "urls/manifestURL" + ] + + private static func createParentMap(_ root: XMLNode?) -> [XMLNode: XMLNode] { + var parentMap = [XMLNode: XMLNode](minimumCapacity: 500) + + func traverse(_ node: XMLNode) { + guard let children = node.children else { return } + for child in children { + parentMap[child] = node + traverse(child) + } + } + + if let root = root { + traverse(root) + } + return parentMap + } static func parseProductsXML(xmlData: Data) throws -> ParseResult { let xml = try XMLDocument(data: xmlData) - guard let cdn = try xml.nodes(forXPath: "//channels/channel/cdn/secure").first?.stringValue else { + guard let cdn = try xml.nodes(forXPath: xpathCache["cdn"]!).first?.stringValue else { throw ParserError.missingCDN } - var products: [String: Sap] = [:] - - let productNodes = try xml.nodes(forXPath: "//channels/channel/products/product") + var products = [String: Sap](minimumCapacity: 100) + + let productNodes = try xml.nodes(forXPath: xpathCache["products"]!) let parentMap = createParentMap(xml.rootElement()) + for productNode in productNodes { guard let element = productNode as? XMLElement else { continue } - + let sap = element.attribute(forName: "id")?.stringValue ?? "" let parentElement = parentMap[parentMap[element] ?? element] let hidden = (parentElement as? XMLElement)?.attribute(forName: "name")?.stringValue != "ccm" + let displayName = try element.nodes(forXPath: "displayName").first?.stringValue ?? "" var productVersion = element.attribute(forName: "version")?.stringValue ?? "" - + if products[sap] == nil { - let icons = try element.nodes(forXPath: "productIcons/icon").compactMap { node -> Sap.ProductIcon? in + let icons = try element.nodes(forXPath: xpathCache["icons"]!).compactMap { node -> Sap.ProductIcon? in guard let element = node as? XMLElement, let size = element.attribute(forName: "size")?.stringValue, let url = element.stringValue else { @@ -51,45 +83,40 @@ class XHXMLParser { ) } - let platforms = try element.nodes(forXPath: "platforms/platform") + let platforms = try element.nodes(forXPath: xpathCache["platforms"]!) for platformNode in platforms { guard let platform = platformNode as? XMLElement, - let languageSet = try platform.nodes(forXPath: "languageSet").first as? XMLElement else { continue } + let languageSet = try platform.nodes(forXPath: xpathCache["languageSet"]!).first as? XMLElement else { continue } var baseVersion = languageSet.attribute(forName: "baseVersion")?.stringValue ?? "" var buildGuid = languageSet.attribute(forName: "buildGuid")?.stringValue ?? "" let appPlatform = platform.attribute(forName: "id")?.stringValue ?? "" - if let existingVersion = products[sap]?.versions[productVersion] { - if existingVersion.apPlatform == "macuniversal" { - break - } + if let existingVersion = products[sap]?.versions[productVersion], + existingVersion.apPlatform == "macuniversal" { + break } - - let dependencies = try languageSet.nodes(forXPath: "dependencies/dependency").compactMap { node -> Sap.Versions.Dependencies? in - guard let element = node as? XMLElement, - let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue, - let version = try element.nodes(forXPath: "baseVersion").first?.stringValue else { - return nil - } + + let dependencies = try languageSet.nodes(forXPath: xpathCache["dependencies"]!).compactMap { node -> Sap.Versions.Dependencies? in + guard let element = node as? XMLElement else { return nil } + let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue ?? "" + let version = try element.nodes(forXPath: "baseVersion").first?.stringValue ?? "" + guard !sapCode.isEmpty, !version.isEmpty else { return nil } return Sap.Versions.Dependencies(sapCode: sapCode, version: version) } if sap == "APRO" { baseVersion = productVersion - let buildNodes = try xml.nodes(forXPath: "//builds/build") - for buildNode in buildNodes { - guard let buildElement = buildNode as? XMLElement, - buildElement.attribute(forName: "id")?.stringValue == sap, - buildElement.attribute(forName: "version")?.stringValue == baseVersion else { - continue - } - if let appVersion = try buildElement.nodes(forXPath: "nglLicensingInfo/appVersion").first?.stringValue { + if let buildNode = try xml.nodes(forXPath: xpathCache["builds"]!).first(where: { node in + guard let element = node as? XMLElement else { return false } + return element.attribute(forName: "id")?.stringValue == sap && + element.attribute(forName: "version")?.stringValue == baseVersion + }) as? XMLElement { + if let appVersion = try buildNode.nodes(forXPath: xpathCache["appVersion"]!).first?.stringValue { productVersion = appVersion - break } } - buildGuid = try languageSet.nodes(forXPath: "urls/manifestURL").first?.stringValue ?? "" + buildGuid = try languageSet.nodes(forXPath: xpathCache["manifestURL"]!).first?.stringValue ?? "" } if !buildGuid.isEmpty { @@ -108,23 +135,6 @@ class XHXMLParser { return ParseResult(products: products, cdn: cdn) } - - private static func createParentMap(_ root: XMLNode?) -> [XMLNode: XMLNode] { - var parentMap: [XMLNode: XMLNode] = [:] - - func traverse(_ node: XMLNode) { - for child in node.children ?? [] { - parentMap[child] = node - traverse(child) - } - } - - if let root = root { - traverse(root) - } - - return parentMap - } } enum ParserError: Error { diff --git a/Adobe Downloader/Views/AboutView.swift b/Adobe Downloader/Views/AboutView.swift index a983da4..8072fa1 100644 --- a/Adobe Downloader/Views/AboutView.swift +++ b/Adobe Downloader/Views/AboutView.swift @@ -8,10 +8,113 @@ import SwiftUI import Sparkle import Combine + +private enum AboutViewConstants { + static let appIconSize: CGFloat = 96 + static let titleFontSize: CGFloat = 18 + static let subtitleFontSize: CGFloat = 14 + static let linkFontSize: CGFloat = 14 + static let licenseFontSize: CGFloat = 12 + + static let verticalSpacing: CGFloat = 12 + static let formPadding: CGFloat = 8 + + static let links: [(title: String, url: String)] = [ + ("@X1a0He", "https://t.me/X1a0He"), + ("Github: Adobe Downloader", "https://github.com/X1a0He/Adobe-Downloader"), + ("Drovosek01: adobe-packager", "https://github.com/Drovosek01/adobe-packager"), + ("QiuChenly: InjectLib", "https://github.com/QiuChenly/InjectLib") + ] +} + +struct ExternalLinkView: View { + let title: String + let url: String + + var body: some View { + Link(title, destination: URL(string: url)!) + .font(.system(size: AboutViewConstants.linkFontSize)) + .foregroundColor(.blue) + } +} + +struct AboutView: View { + private let updater: SPUUpdater + + init(updater: SPUUpdater) { + self.updater = updater + } + + var body: some View { + TabView { + GeneralSettingsView(updater: updater) + .tabItem { + Label("通用", systemImage: "gear") + } + .id("general_settings") + + AboutAppView() + .tabItem { + Label("关于", systemImage: "info.circle") + } + .id("about_app") + } + .background(Color(NSColor.windowBackgroundColor)) + .frame(width: 600) + } +} + +struct AboutAppView: View { + private var appVersion: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + } + + var body: some View { + VStack(spacing: AboutViewConstants.verticalSpacing) { + appIconSection + appInfoSection + linksSection + licenseSection + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var appIconSection: some View { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .frame(width: AboutViewConstants.appIconSize, height: AboutViewConstants.appIconSize) + } + + private var appInfoSection: some View { + Group { + Text("Adobe Downloader \(appVersion)") + .font(.system(size: AboutViewConstants.titleFontSize)) + .bold() + + Text("By X1a0He. ❤️ Love from China. 🇨🇳") + .font(.system(size: AboutViewConstants.subtitleFontSize)) + .foregroundColor(.secondary) + } + } + + private var linksSection: some View { + ForEach(AboutViewConstants.links, id: \.url) { link in + ExternalLinkView(title: link.title, url: link.url) + } + } + + private var licenseSection: some View { + Text("GNU通用公共许可证GPL v3.") + .font(.system(size: AboutViewConstants.licenseFontSize)) + .foregroundColor(.secondary) + } +} + struct PulsingCircle: View { let color: Color @State private var scale: CGFloat = 1.0 - + var body: some View { Circle() .fill(color) @@ -28,32 +131,6 @@ struct PulsingCircle: View { } } -struct AboutView: View { - private let updater: SPUUpdater - - init(updater: SPUUpdater) { - self.updater = updater - } - - var body: some View { - TabView { - GeneralSettingsView(updater: updater) - .tabItem { - Label("通用", systemImage: "gear") - } - .id("general_settings") - - AboutAppView() - .tabItem { - Label("关于", systemImage: "info.circle") - } - .id("about_app") - } - .background(Color(NSColor.windowBackgroundColor)) - .frame(width: 600) - } -} - final class GeneralSettingsViewModel: ObservableObject { @Published var setupVersion: String = "" @Published var isDownloadingSetup = false @@ -67,18 +144,40 @@ final class GeneralSettingsViewModel: ObservableObject { @Published var showDownloadConfirmAlert = false @Published var showReprocessConfirmAlert = false @Published var isProcessing = false - @Published var helperConnectionStatus: HelperConnectionStatus = .connecting + @Published var helperConnectionStatus: HelperConnectionStatus = .disconnected @Published var downloadAppleSilicon: Bool { didSet { - UserDefaults.standard.set(downloadAppleSilicon, forKey: "downloadAppleSilicon") + StorageData.shared.downloadAppleSilicon = downloadAppleSilicon } } - @AppStorage("defaultLanguage") var defaultLanguage: String = "ALL" - @AppStorage("defaultDirectory") var defaultDirectory: String = "" - @AppStorage("useDefaultLanguage") var useDefaultLanguage: Bool = true - @AppStorage("useDefaultDirectory") var useDefaultDirectory: Bool = true - @AppStorage("confirmRedownload") var confirmRedownload: Bool = true + var defaultLanguage: String { + get { StorageData.shared.defaultLanguage } + set { StorageData.shared.defaultLanguage = newValue } + } + + var defaultDirectory: String { + get { StorageData.shared.defaultDirectory } + set { StorageData.shared.defaultDirectory = newValue } + } + + var useDefaultLanguage: Bool { + get { StorageData.shared.useDefaultLanguage } + set { StorageData.shared.useDefaultLanguage = newValue } + } + + var useDefaultDirectory: Bool { + get { StorageData.shared.useDefaultDirectory } + set { StorageData.shared.useDefaultDirectory = newValue } + } + + var confirmRedownload: Bool { + get { StorageData.shared.confirmRedownload } + set { + StorageData.shared.confirmRedownload = newValue + objectWillChange.send() + } + } @Published var automaticallyChecksForUpdates: Bool @Published var automaticallyDownloadsUpdates: Bool @@ -99,10 +198,10 @@ final class GeneralSettingsViewModel: ObservableObject { self.updater = updater self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates - self.downloadAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon") - + self.downloadAppleSilicon = StorageData.shared.downloadAppleSilicon + self.helperConnectionStatus = .connecting - + PrivilegedHelperManager.shared.$connectionState .receive(on: DispatchQueue.main) .sink { [weak self] state in @@ -116,10 +215,15 @@ final class GeneralSettingsViewModel: ObservableObject { } } .store(in: &cancellables) - - DispatchQueue.main.async { - PrivilegedHelperManager.shared.executeCommand("whoami") { _ in } - } + + PrivilegedHelperManager.shared.executeCommand("whoami") { _ in } + + NotificationCenter.default.publisher(for: .storageDidChange) + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) } deinit { @@ -159,7 +263,6 @@ struct GeneralSettingsView: View { var body: some View { Form { DownloadSettingsView(viewModel: viewModel) - HelperSettingsView(viewModel: viewModel, showHelperAlert: $showHelperAlert, helperAlertMessage: $helperAlertMessage, @@ -259,6 +362,9 @@ struct GeneralSettingsView: View { viewModel.setupVersion = ModifySetup.checkComponentVersion() networkManager.updateAllowedPlatform(useAppleSilicon: viewModel.downloadAppleSilicon) } + .onReceive(NotificationCenter.default.publisher(for: .storageDidChange)) { _ in + viewModel.objectWillChange.send() + } } } @@ -327,56 +433,6 @@ struct UpdateSettingsView: View { } } -struct AboutAppView: View { - private var appVersion: String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" - // let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" - // return "Version \(version) (\(build))" - return "\(version)" - } - - var body: some View { - VStack(spacing: 12) { - Image(nsImage: NSApp.applicationIconImage) - .resizable() - .frame(width: 96, height: 96) - - Text("Adobe Downloader \(appVersion)") - .font(.title2) - .bold() - - Text("By X1a0He. ❤️ Love from China. ❤️") - .font(.subheadline) - .foregroundColor(.secondary) - - Link("联系 @X1a0He", - destination: URL(string: "https://t.me/X1a0He")!) - .font(.caption) - .foregroundColor(.blue) - Link("Github: Adobe Downloader", - destination: URL(string: "https://github.com/X1a0He/Adobe-Downloader")!) - .font(.caption) - .foregroundColor(.blue) - - Link("感谢 Drovosek01: adobe-packager", - destination: URL(string: "https://github.com/Drovosek01/adobe-packager")!) - .font(.caption) - .foregroundColor(.blue) - - Link("感谢 QiuChenly: InjectLib", - destination: URL(string: "https://github.com/QiuChenly/InjectLib")!) - .font(.caption) - .foregroundColor(.blue) - - Text("GNU通用公共许可证GPL v3.") - .font(.caption) - .foregroundColor(.secondary) - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - #Preview("About Tab") { AboutAppView() } @@ -394,7 +450,7 @@ private class PreviewUpdater: SPUUpdater { let hostBundle = Bundle.main let applicationBundle = Bundle.main let userDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil) - + super.init( hostBundle: hostBundle, applicationBundle: applicationBundle, @@ -402,12 +458,12 @@ private class PreviewUpdater: SPUUpdater { delegate: nil ) } - + override var automaticallyChecksForUpdates: Bool { get { true } set { } } - + override var automaticallyDownloadsUpdates: Bool { get { true } set { } @@ -419,7 +475,10 @@ struct LanguageSettingRow: View { var body: some View { HStack { - Toggle("使用默认语言", isOn: $viewModel.useDefaultLanguage) + Toggle("使用默认语言", isOn: Binding( + get: { viewModel.useDefaultLanguage }, + set: { viewModel.useDefaultLanguage = $0 } + )) .padding(.leading, 5) Spacer() Text(getLanguageName(code: viewModel.defaultLanguage)) @@ -500,6 +559,7 @@ struct ArchitectureSettingRow: View { HStack { Toggle("下载 Apple Silicon 架构", isOn: $viewModel.downloadAppleSilicon) .padding(.leading, 5) + .disabled(networkManager.loadingState == .loading) Spacer() Text("当前架构: \(AppStatics.cpuArchitecture)") .foregroundColor(.secondary) @@ -508,6 +568,9 @@ struct ArchitectureSettingRow: View { } .onChange(of: viewModel.downloadAppleSilicon) { newValue in networkManager.updateAllowedPlatform(useAppleSilicon: newValue) + Task { + await networkManager.fetchProducts() + } } } } @@ -518,6 +581,7 @@ struct HelperStatusRow: View { @Binding var helperAlertMessage: String @Binding var helperAlertSuccess: Bool @State private var isReinstallingHelper = false + @State private var installationTask: Task? var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -526,7 +590,7 @@ struct HelperStatusRow: View { if PrivilegedHelperManager.getHelperStatus { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) - Text("已安装") + Text("已安装 (build \(UserDefaults.standard.string(forKey: "InstalledHelperBuild") ?? "0"))") } else { Image(systemName: "xmark.circle.fill") .foregroundColor(.red) @@ -594,10 +658,10 @@ struct HelperStatusRow: View { private var helperStatusText: String { switch viewModel.helperConnectionStatus { - case .connected: return "运行正常" - case .connecting: return "正在连接" - case .checking: return "检查中" - case .disconnected: return "连接断开" + case .connected: return String(localized: "运行正常") + case .connecting: return String(localized: "正在连接") + case .checking: return String(localized: "检查中") + case .disconnected: return String(localized: "连接断开") } } } @@ -618,17 +682,6 @@ struct SetupComponentRow: View { .foregroundColor(.red) Text("(可能导致处理 Setup 组件失败)") } - Spacer() - - Button(action: { - if !ModifySetup.isSetupExists() { - viewModel.showDownloadAlert = true - } else { - viewModel.showReprocessConfirmAlert = true - } - }) { - Text("重新备份") - } } Divider() HStack { @@ -656,7 +709,7 @@ struct SetupComponentRow: View { } Divider() HStack { - Text("X1a0He CC 版本信息: \(viewModel.setupVersion)") + Text("X1a0He CC 版本信息: \(viewModel.setupVersion) [\(AppStatics.cpuArchitecture)]") Spacer() if viewModel.isDownloadingSetup { @@ -706,3 +759,4 @@ struct AutoDownloadRow: View { .disabled(viewModel.isAutomaticallyDownloadsUpdatesDisabled) } } + diff --git a/Adobe Downloader/Views/AppCardView.swift b/Adobe Downloader/Views/AppCardView.swift index b93ed41..bbbba03 100644 --- a/Adobe Downloader/Views/AppCardView.swift +++ b/Adobe Downloader/Views/AppCardView.swift @@ -7,7 +7,23 @@ import SwiftUI import Combine -class IconCache { +private enum AppCardConstants { + static let cardWidth: CGFloat = 250 + static let cardHeight: CGFloat = 200 + static let iconSize: CGFloat = 64 + static let cornerRadius: CGFloat = 10 + static let buttonHeight: CGFloat = 32 + static let titleFontSize: CGFloat = 16 + static let buttonFontSize: CGFloat = 14 + + static let shadowOpacity: Double = 0.05 + static let shadowRadius: CGFloat = 2 + static let strokeOpacity: Double = 0.1 + static let strokeWidth: CGFloat = 2 + static let backgroundOpacity: Double = 0.05 +} + +final class IconCache { static let shared = IconCache() private var cache = NSCache() @@ -20,7 +36,8 @@ class IconCache { } } -class AppCardViewModel: ObservableObject { +@MainActor +final class AppCardViewModel: ObservableObject { @Published var iconImage: NSImage? @Published var showError = false @Published var errorMessage = "" @@ -40,12 +57,12 @@ class AppCardViewModel: ObservableObject { @Published var isDownloading = false private let userDefaults = UserDefaults.standard - var useDefaultDirectory: Bool { - get { userDefaults.bool(forKey: "useDefaultDirectory") } + private var useDefaultDirectory: Bool { + StorageData.shared.useDefaultDirectory } - var defaultDirectory: String { - get { userDefaults.string(forKey: "defaultDirectory") ?? "" } + private var defaultDirectory: String { + StorageData.shared.defaultDirectory } private var cancellables = Set() @@ -54,10 +71,28 @@ class AppCardViewModel: ObservableObject { self.sap = sap self.networkManager = networkManager - setupObservers() + Task { @MainActor in + setupObservers() + } } + @MainActor private func setupObservers() { + networkManager?.$downloadTasks + .receive(on: RunLoop.main) + .sink { [weak self] tasks in + guard let self = self else { return } + let hasActiveTask = tasks.contains { + $0.sapCode == self.sap.sapCode && self.isTaskActive($0.status) + } + + if hasActiveTask != self.isDownloading { + self.isDownloading = hasActiveTask + self.objectWillChange.send() + } + } + .store(in: &cancellables) + networkManager?.objectWillChange .receive(on: RunLoop.main) .sink { [weak self] _ in @@ -66,31 +101,34 @@ class AppCardViewModel: ObservableObject { .store(in: &cancellables) } - func updateDownloadingStatus() { - guard let networkManager = networkManager else { - Task { @MainActor in - self.isDownloading = false - } - return - } - - Task { @MainActor in - let isActive = networkManager.downloadTasks.contains { task in - task.sapCode == sap.sapCode && isTaskActive(task.status) - } - self.isDownloading = isActive - } - } - private func isTaskActive(_ status: DownloadStatus) -> Bool { switch status { - case .downloading, .preparing, .paused, .waiting, .retrying(_): + case .downloading, .preparing, .waiting, .retrying: return true + case .paused: + return false case .completed, .failed: return false } } - + + @MainActor + func updateDownloadingStatus() { + guard let networkManager = networkManager else { + self.isDownloading = false + return + } + + let hasActiveTask = networkManager.downloadTasks.contains { + $0.sapCode == sap.sapCode && isTaskActive($0.status) + } + + if hasActiveTask != self.isDownloading { + self.isDownloading = hasActiveTask + self.objectWillChange.send() + } + } + func getDestinationURL(version: String, language: String) async throws -> URL { let platform = sap.versions[version]?.apPlatform ?? "unknown" let installerName = sap.sapCode == "APRO" @@ -185,7 +223,7 @@ class AppCardViewModel: ObservableObject { func checkAndStartDownload(version: String, language: String) async { if let networkManager = networkManager { - if let existingPath = await networkManager.isVersionDownloaded(sap: sap, version: version, language: language) { + if let existingPath = networkManager.isVersionDownloaded(sap: sap, version: version, language: language) { await MainActor.run { existingFilePath = existingPath pendingVersion = version @@ -212,7 +250,7 @@ class AppCardViewModel: ObservableObject { guard let networkManager = networkManager, let productInfo = sap.versions[pendingVersion] else { return } - let existingTask = await networkManager.downloadTasks.first { task in + let existingTask = networkManager.downloadTasks.first { task in return task.sapCode == sap.sapCode && task.version == pendingVersion && task.language == pendingLanguage && @@ -232,14 +270,14 @@ class AppCardViewModel: ObservableObject { productsToDownload.append(mainProduct) for dependency in productInfo.dependencies { - if let dependencyVersions = await networkManager.saps[dependency.sapCode]?.versions { + if let dependencyVersions = networkManager.saps[dependency.sapCode]?.versions { let sortedVersions = dependencyVersions.sorted { first, second in first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending } var buildGuid = "" for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version { - if await networkManager.allowedPlatform.contains(versionInfo.apPlatform) { + if networkManager.allowedPlatform.contains(versionInfo.apPlatform) { buildGuid = versionInfo.buildGuid break } @@ -289,95 +327,81 @@ class AppCardViewModel: ObservableObject { } return 0 } + + var hasValidIcon: Bool { + iconImage != nil + } + + var canDownload: Bool { + !isDownloading + } + + var downloadButtonTitle: String { + isDownloading ? String(localized: "下载中") : String(localized: "下载") + } + + var downloadButtonIcon: String { + isDownloading ? "hourglass.circle.fill" : "arrow.down.circle" + } } struct AppCardView: View { @StateObject private var viewModel: AppCardViewModel @EnvironmentObject private var networkManager: NetworkManager - @AppStorage("useDefaultLanguage") private var useDefaultLanguage = true - @AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN" + @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage + @StorageValue(\.defaultLanguage) private var defaultLanguage init(sap: Sap) { _viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil)) } var body: some View { - CardContent( - sap: viewModel.sap, - iconImage: viewModel.iconImage, - loadIcon: viewModel.loadIcon, - dependenciesCount: viewModel.dependenciesCount, - isDownloading: viewModel.isDownloading, - showVersionPicker: $viewModel.showVersionPicker - ) - .padding() - .frame(width: 250, height: 200) - .background(RoundedRectangle(cornerRadius: 10).fill(Color.black.opacity(0.05))) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(Color.gray.opacity(0.1), lineWidth: 2) - ) + CardContainer { + VStack { + IconView(viewModel: viewModel) + ProductInfoView(viewModel: viewModel) + Spacer() + DownloadButtonView(viewModel: viewModel) + } + } + .modifier(CardModifier()) + .modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager)) .modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true)) - .sheet(isPresented: $viewModel.showVersionPicker) { - VersionPickerView(sap: viewModel.sap) { version in - Task { - await viewModel.handleDownloadRequest(version, useDefaultLanguage: useDefaultLanguage, defaultLanguage: defaultLanguage) - } - } - .environmentObject(networkManager) - } - .sheet(isPresented: $viewModel.showLanguagePicker) { - LanguagePickerView(languages: AppStatics.supportedLanguages) { language in - Task { - await viewModel.checkAndStartDownload(version: viewModel.selectedVersion, language: language) - } - } - } - .onAppear { - viewModel.networkManager = networkManager - viewModel.updateDownloadingStatus() - } - .onChange(of: networkManager.downloadTasks) { _ in - viewModel.updateDownloadingStatus() - } + .onAppear(perform: setupViewModel) + .onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus) + } + + private func setupViewModel() { + viewModel.networkManager = networkManager + viewModel.updateDownloadingStatus() + } + + private func updateDownloadStatus(_ _: [NewDownloadTask]) { + viewModel.updateDownloadingStatus() } } -private struct CardContent: View { - let sap: Sap - let iconImage: NSImage? - let loadIcon: () -> Void - let dependenciesCount: Int - let isDownloading: Bool - @Binding var showVersionPicker: Bool +private struct CardContainer: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } var body: some View { - VStack { - IconView(iconImage: iconImage, loadIcon: loadIcon) - ProductInfoView(sap: sap, dependenciesCount: dependenciesCount) - Spacer() - DownloadButton( - isDownloading: isDownloading, - showVersionPicker: $showVersionPicker - ) - } - } -} - -private extension View { - func applyModifiers(viewModel: AppCardViewModel) -> some View { - self.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true)) + content + .padding() + .frame(width: AppCardConstants.cardWidth, height: AppCardConstants.cardHeight) } } private struct IconView: View { - let iconImage: NSImage? - let loadIcon: () -> Void + @ObservedObject var viewModel: AppCardViewModel var body: some View { Group { - if let iconImage = iconImage { - Image(nsImage: iconImage) + if viewModel.hasValidIcon { + Image(nsImage: viewModel.iconImage!) .resizable() .interpolation(.high) .scaledToFit() @@ -388,27 +412,26 @@ private struct IconView: View { .foregroundColor(.secondary) } } - .frame(width: 64, height: 64) - .onAppear(perform: loadIcon) + .frame(width: AppCardConstants.iconSize, height: AppCardConstants.iconSize) + .onAppear(perform: viewModel.loadIcon) } } private struct ProductInfoView: View { - let sap: Sap - let dependenciesCount: Int + @ObservedObject var viewModel: AppCardViewModel var body: some View { VStack { - Text(sap.displayName) - .font(.system(size: 16)) + Text(viewModel.sap.displayName) + .font(.system(size: AppCardConstants.titleFontSize)) .fontWeight(.bold) .lineLimit(2) .multilineTextAlignment(.center) - + HStack(spacing: 4) { - Text("可用版本: \(sap.versions.count)") + Text("可用版本: \(viewModel.sap.versions.count)") Text("|") - Text("依赖包: \(dependenciesCount)") + Text("依赖包: \(viewModel.dependenciesCount)") } .font(.caption) .foregroundColor(.secondary) @@ -417,21 +440,74 @@ private struct ProductInfoView: View { } } -private struct DownloadButton: View { - let isDownloading: Bool - @Binding var showVersionPicker: Bool +private struct DownloadButtonView: View { + @ObservedObject var viewModel: AppCardViewModel var body: some View { - Button(action: { showVersionPicker = true }) { - Label(isDownloading ? "下载中" : "下载", - systemImage: isDownloading ? "hourglass.circle.fill" : "arrow.down.circle") - .font(.system(size: 14)) + Button(action: { viewModel.showVersionPicker = true }) { + Label(viewModel.downloadButtonTitle, + systemImage: viewModel.downloadButtonIcon) + .font(.system(size: AppCardConstants.buttonFontSize)) .frame(minWidth: 0, maxWidth: .infinity) - .frame(height: 32) + .frame(height: AppCardConstants.buttonHeight) } .buttonStyle(.borderedProminent) - .tint(isDownloading ? .gray : .blue) - .disabled(isDownloading) + .tint(viewModel.isDownloading ? .gray : .blue) + .disabled(!viewModel.canDownload) + } +} + +private struct CardModifier: ViewModifier { + func body(content: Content) -> some View { + content + .background( + RoundedRectangle(cornerRadius: AppCardConstants.cornerRadius) + .fill(Color.black.opacity(AppCardConstants.backgroundOpacity)) + ) + .overlay( + RoundedRectangle(cornerRadius: AppCardConstants.cornerRadius) + .stroke(Color.gray.opacity(AppCardConstants.strokeOpacity), + lineWidth: AppCardConstants.strokeWidth) + ) + .shadow( + color: Color.primary.opacity(AppCardConstants.shadowOpacity), + radius: AppCardConstants.shadowRadius, + x: 0, + y: 1 + ) + } +} + +private struct SheetModifier: ViewModifier { + @ObservedObject var viewModel: AppCardViewModel + let networkManager: NetworkManager + @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage + @StorageValue(\.defaultLanguage) private var defaultLanguage + + func body(content: Content) -> some View { + content + .sheet(isPresented: $viewModel.showVersionPicker) { + VersionPickerView(sap: viewModel.sap) { version in + Task { + await viewModel.handleDownloadRequest( + version, + useDefaultLanguage: useDefaultLanguage, + defaultLanguage: defaultLanguage + ) + } + } + .environmentObject(networkManager) + } + .sheet(isPresented: $viewModel.showLanguagePicker) { + LanguagePickerView(languages: AppStatics.supportedLanguages) { language in + Task { + await viewModel.checkAndStartDownload( + version: viewModel.selectedVersion, + language: language + ) + } + } + } } } @@ -481,7 +557,14 @@ struct AlertModifier: ViewModifier { viewModel.showExistingFileAlert = false if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty { Task { - await viewModel.createCompletedTask(path) + if let networkManager = viewModel.networkManager, + !networkManager.downloadTasks.contains(where: { task in + task.sapCode == viewModel.sap.sapCode && + task.version == viewModel.pendingVersion && + task.language == viewModel.pendingLanguage + }) { + await viewModel.createCompletedTask(path) + } } } }, @@ -492,10 +575,7 @@ struct AlertModifier: ViewModifier { viewModel.showRedownloadConfirm = true } else { Task { - await viewModel.checkAndStartDownload( - version: viewModel.pendingVersion, - language: viewModel.pendingLanguage - ) + await startRedownload() } } } @@ -505,20 +585,13 @@ struct AlertModifier: ViewModifier { }, iconImage: viewModel.iconImage ) - .background(Color.black.opacity(0.3)) - .ignoresSafeArea() } } .alert("确认重新下载", isPresented: $viewModel.showRedownloadConfirm) { Button("取消", role: .cancel) { } Button("确认") { - if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty { - Task { - await viewModel.checkAndStartDownload( - version: viewModel.pendingVersion, - language: viewModel.pendingLanguage - ) - } + Task { + await startRedownload() } } } message: { @@ -540,4 +613,34 @@ struct AlertModifier: ViewModifier { Text(viewModel.errorMessage) } } + + private func startRedownload() async { + guard let networkManager = viewModel.networkManager else { return } + + do { + networkManager.downloadTasks.removeAll { task in + task.sapCode == viewModel.sap.sapCode && + task.version == viewModel.pendingVersion && + task.language == viewModel.pendingLanguage + } + + if let existingPath = viewModel.existingFilePath { + try? FileManager.default.removeItem(at: existingPath) + } + + let destinationURL = try await viewModel.getDestinationURL( + version: viewModel.pendingVersion, + language: viewModel.pendingLanguage + ) + + try await networkManager.startDownload( + sap: viewModel.sap, + selectedVersion: viewModel.pendingVersion, + language: viewModel.pendingLanguage, + destinationURL: destinationURL + ) + } catch { + viewModel.handleError(error) + } + } } diff --git a/Adobe Downloader/Views/DownloadProgressView.swift b/Adobe Downloader/Views/DownloadProgressView.swift index 5e2a6dd..a22a466 100644 --- a/Adobe Downloader/Views/DownloadProgressView.swift +++ b/Adobe Downloader/Views/DownloadProgressView.swift @@ -120,10 +120,17 @@ struct DownloadProgressView: View { if !ModifySetup.isSetupBackup() { showSetupBackupAlert = true } else { - showInstallPrompt = false - isInstalling = true - Task { - await networkManager.installProduct(at: task.directory) + print("正在连接 Helper...") + if PrivilegedHelperManager.shared.connectToHelper() != nil { + print("Helper 连接成功,开始安装...") + showInstallPrompt = false + isInstalling = true + Task { + await networkManager.installProduct(at: task.directory) + } + } else { + print("Helper 连接失败") + showSetupBackupAlert = true } } }) { @@ -134,8 +141,13 @@ struct DownloadProgressView: View { .alert("Setup 组件未处理", isPresented: $showSetupBackupAlert) { Button("确定") { } } message: { - Text("未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理") - .font(.system(size: 18)) + if !ModifySetup.isSetupBackup() { + Text("未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理") + .font(.system(size: 18)) + } else { + Text("Helper 未安装或未连接,请先在设置中安装并连接 Helper") + .font(.system(size: 18)) + } } } diff --git a/Adobe Downloader/Views/ExistingFileAlertView.swift b/Adobe Downloader/Views/ExistingFileAlertView.swift index 562916d..ca82f7f 100644 --- a/Adobe Downloader/Views/ExistingFileAlertView.swift +++ b/Adobe Downloader/Views/ExistingFileAlertView.swift @@ -1,5 +1,4 @@ // -// ShouldExistsSetUpView.swift // Adobe Downloader // // Created by X1a0He. @@ -7,6 +6,18 @@ import SwiftUI +private enum AlertConstants { + static let iconSize: CGFloat = 64 + static let warningIconSize: CGFloat = 24 + static let warningIconOffset: CGFloat = 10 + static let verticalSpacing: CGFloat = 20 + static let buttonHeight: CGFloat = 32 + static let buttonWidth: CGFloat = 260 + static let buttonFontSize: CGFloat = 14 + static let cornerRadius: CGFloat = 12 + static let shadowRadius: CGFloat = 10 +} + struct ExistingFileAlertView: View { let path: URL let onUseExisting: () -> Void @@ -15,99 +26,185 @@ struct ExistingFileAlertView: View { let iconImage: NSImage? var body: some View { - VStack(spacing: 20) { - ZStack(alignment: .bottomTrailing) { - Group { - if let iconImage = iconImage { - Image(nsImage: iconImage) - .resizable() - .interpolation(.high) - .scaledToFit() - } else { - Image(systemName: "app.fill") - .resizable() - .scaledToFit() - .foregroundColor(.secondary) - } - } - .frame(width: 64, height: 64) - - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 24)) - .foregroundColor(.orange) - .offset(x: 10, y: 4) - } - .padding(.bottom, 5) + VStack(spacing: AlertConstants.verticalSpacing) { + IconSection(iconImage: iconImage) Text("安装程序已存在") .font(.headline) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(path.path) - .foregroundColor(.blue) - .onTapGesture { - NSWorkspace.shared.activateFileViewerSelecting([path]) - } - } - } - - VStack(spacing: 16) { - Button(action: onUseExisting) { - Label("使用现有程序", systemImage: "checkmark.circle") - .frame(minWidth: 0,maxWidth: 260) - .frame(height: 32) - .font(.system(size: 14)) - } - .buttonStyle(.borderedProminent) - .tint(.blue) - - Button(action: onRedownload) { - Label("重新下载", systemImage: "arrow.down.circle") - .frame(minWidth: 0,maxWidth: 260) - .frame(height: 32) - .font(.system(size: 14)) - } - .buttonStyle(.borderedProminent) - .tint(.green) - - Button(action: onCancel) { - Label("取消", systemImage: "xmark.circle") - .frame(minWidth: 0, maxWidth: 260) - .frame(height: 32) - .font(.system(size: 14)) - } - .buttonStyle(.borderedProminent) - .tint(.red) - .keyboardShortcut(.cancelAction) - } + PathSection(path: path) + ButtonSection( + onUseExisting: onUseExisting, + onRedownload: onRedownload, + onCancel: onCancel + ) } .padding() - .background(Color(NSColor.windowBackgroundColor)) - .cornerRadius(12) - .shadow(radius: 10) + .background(BackgroundView()) } } -#Preview { - ExistingFileAlertView( - path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), - onUseExisting: {}, - onRedownload: {}, - onCancel: {}, - iconImage: NSImage(named: "PHSP") - ) - .background(Color.black.opacity(0.3)) +private struct IconSection: View { + let iconImage: NSImage? + + var body: some View { + ZStack(alignment: .bottomTrailing) { + AppIcon(iconImage: iconImage) + WarningIcon() + } + .padding(.bottom, 5) + } } -#Preview("Dark Mode") { - ExistingFileAlertView( - path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), - onUseExisting: {}, - onRedownload: {}, - onCancel: {}, - iconImage: NSImage(named: "PHSP") - ) - .background(Color.black.opacity(0.3)) - .preferredColorScheme(.dark) +private struct AppIcon: View { + let iconImage: NSImage? + + var body: some View { + Group { + if let iconImage = iconImage { + Image(nsImage: iconImage) + .resizable() + .interpolation(.high) + .scaledToFit() + } else { + Image(systemName: "app.fill") + .resizable() + .scaledToFit() + .foregroundColor(.secondary) + } + } + .frame(width: AlertConstants.iconSize, height: AlertConstants.iconSize) + } +} + +private struct WarningIcon: View { + var body: some View { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: AlertConstants.warningIconSize)) + .foregroundColor(.orange) + .offset(x: AlertConstants.warningIconOffset, y: 4) + } +} + +private struct PathSection: View { + let path: URL + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(path.path) + .foregroundColor(.blue) + .onTapGesture { + openInFinder(path) + } + } + } + } + + private func openInFinder(_ path: URL) { + NSWorkspace.shared.activateFileViewerSelecting([path]) + } +} + +private struct ButtonSection: View { + let onUseExisting: () -> Void + let onRedownload: () -> Void + let onCancel: () -> Void + + var body: some View { + VStack(spacing: 16) { + ActionButton( + title: "使用现有程序", + icon: "checkmark.circle", + color: .blue, + action: onUseExisting + ) + + ActionButton( + title: "重新下载", + icon: "arrow.down.circle", + color: .green, + action: onRedownload + ) + + ActionButton( + title: "取消", + icon: "xmark.circle", + color: .red, + action: onCancel, + isCancel: true + ) + } + } +} + +private struct ActionButton: View { + let title: String + let icon: String + let color: Color + let action: () -> Void + var isCancel: Bool = false + + var body: some View { + Button(action: action) { + Label(title, systemImage: icon) + .frame(minWidth: 0, maxWidth: AlertConstants.buttonWidth) + .frame(height: AlertConstants.buttonHeight) + .font(.system(size: AlertConstants.buttonFontSize)) + } + .buttonStyle(.borderedProminent) + .tint(color) + .if(isCancel) { view in + view.keyboardShortcut(.cancelAction) + } + } +} + +private struct BackgroundView: View { + var body: some View { + Color(NSColor.windowBackgroundColor) + .cornerRadius(AlertConstants.cornerRadius) + .shadow(radius: AlertConstants.shadowRadius) + } +} + +extension View { + @ViewBuilder + fileprivate func `if`( + _ condition: Bool, + transform: (Self) -> Transform + ) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + +struct ExistingFileAlertView_Previews: PreviewProvider { + static var previews: some View { + Group { + ExistingFileAlertView( + path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), + onUseExisting: {}, + onRedownload: {}, + onCancel: {}, + iconImage: NSImage(named: "PHSP") + ) + .background(Color.black.opacity(0.3)) + .previewDisplayName("Light Mode") + + ExistingFileAlertView( + path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), + onUseExisting: {}, + onRedownload: {}, + onCancel: {}, + iconImage: NSImage(named: "PHSP") + ) + .background(Color.black.opacity(0.3)) + .preferredColorScheme(.dark) + .previewDisplayName("Dark Mode") + } + } } diff --git a/Adobe Downloader/Views/InstallProgressView.swift b/Adobe Downloader/Views/InstallProgressView.swift index 3c819a7..36d692b 100644 --- a/Adobe Downloader/Views/InstallProgressView.swift +++ b/Adobe Downloader/Views/InstallProgressView.swift @@ -18,7 +18,7 @@ struct InstallProgressView: View { } private var isFailed: Bool { - status.contains(String(localized: "失败")) + status.contains(String(localized: "安装失败")) } private var progressText: String { @@ -77,7 +77,8 @@ struct InstallProgressView: View { if isFailed { ErrorSection( - status: status, isFailed: isFailed + status: status, + isFailed: true ) } @@ -119,7 +120,6 @@ private struct ErrorSection: View { let isFailed: Bool var body: some View { - VStack(alignment: .leading, spacing: 8) { Text("错误详情:") .font(.caption) @@ -163,7 +163,7 @@ private struct CommandSection: View { .font(.caption) .foregroundColor(.secondary) .textSelection(.enabled) - .frame(maxWidth: .infinity,alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) .frame(minHeight: 200) .padding(8) .background(Color.secondary.opacity(0.1)) diff --git a/Adobe Downloader/Views/ShouldExistsSetUpView.swift b/Adobe Downloader/Views/ShouldExistsSetUpView.swift index 30d827e..ddc589f 100644 --- a/Adobe Downloader/Views/ShouldExistsSetUpView.swift +++ b/Adobe Downloader/Views/ShouldExistsSetUpView.swift @@ -118,7 +118,7 @@ private struct ButtonsView: View { if isDownloading { downloadProgressView } else { - Label("下载 X1a0He CC 组件", systemImage: "arrow.down") + Label("下载 X1a0He CC", systemImage: "arrow.down") .frame(minWidth: 0, maxWidth: 360) .frame(height: 32) .font(.system(size: 14)) diff --git a/Adobe Downloader/Views/VersionPickerView.swift b/Adobe Downloader/Views/VersionPickerView.swift index db96e40..6c30c95 100644 --- a/Adobe Downloader/Views/VersionPickerView.swift +++ b/Adobe Downloader/Views/VersionPickerView.swift @@ -5,11 +5,26 @@ // import SwiftUI + +private enum VersionPickerConstants { + static let headerPadding: CGFloat = 5 + static let viewWidth: CGFloat = 400 + static let viewHeight: CGFloat = 500 + static let iconSize: CGFloat = 32 + static let verticalSpacing: CGFloat = 8 + static let horizontalSpacing: CGFloat = 12 + static let cornerRadius: CGFloat = 8 + static let buttonPadding: CGFloat = 8 + + static let titleFontSize: CGFloat = 14 + static let captionFontSize: CGFloat = 12 +} + struct VersionPickerView: View { @EnvironmentObject private var networkManager: NetworkManager @Environment(\.dismiss) private var dismiss - @AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN" - @AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true + @StorageValue(\.defaultLanguage) private var defaultLanguage + @StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon @State private var expandedVersions: Set = [] private let sap: Sap @@ -22,143 +37,287 @@ struct VersionPickerView: View { var body: some View { VStack(spacing: 0) { - VStack { - HStack { - Text("\(sap.displayName)") - .font(.headline) - Text("选择版本") - .foregroundColor(.secondary) - Spacer() - Button("取消") { - dismiss() - } - } - .padding(.bottom, 5) - Text("🔔 即将下载 \(downloadAppleSilicon ? "Apple Silicon" : "Intel") (\(networkManager.allowedPlatform.joined(separator: ", "))) 版本 🔔") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(.horizontal) - .padding(.top) - .background(Color(NSColor.windowBackgroundColor)) - - ScrollView(showsIndicators: false) { - LazyVStack(spacing: 12) { - ForEach(Array(sap.versions.sorted { $0.key > $1.key }), id: \.key) { version, info in - if networkManager.allowedPlatform.contains(info.apPlatform) { - VStack(spacing: 0) { - Button(action: { - if info.dependencies.isEmpty { - onSelect(version) - dismiss() - } else { - withAnimation { - if expandedVersions.contains(version) { - expandedVersions.remove(version) - } else { - expandedVersions.insert(version) - } - } - } - }) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(version) - .font(.headline) - Text(info.apPlatform) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - if let existingPath = networkManager.isVersionDownloaded( - sap: sap, - version: version, - language: defaultLanguage - ) { - Button(action: { - let path = existingPath.path - NSWorkspace.shared.selectFile( - path, - inFileViewerRootedAtPath: URL(fileURLWithPath: path).deletingLastPathComponent().path - ) - }) { - Text("已存在") - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue) - .cornerRadius(4) - } - .buttonStyle(.plain) - } - - if !info.dependencies.isEmpty { - Image(systemName: expandedVersions.contains(version) ? "chevron.down" : "chevron.right") - .foregroundColor(.secondary) - } else { - Image(systemName: "chevron.right") - .foregroundColor(.secondary) - } - } - .padding(.vertical, 8) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - - if expandedVersions.contains(version) { - VStack(alignment: .leading, spacing: 8) { - Text("依赖包:") - .font(.caption) - .foregroundColor(.secondary) - .padding(.top, 8) - .padding(.leading, 16) - - ForEach(info.dependencies, id: \.sapCode) { dependency in - HStack(spacing: 8) { - Image(systemName: "cube.box") - .foregroundColor(.blue) - .frame(width: 16) - Text("\(dependency.sapCode) (\(dependency.version))") - .font(.caption) - Spacer() - } - .padding(.leading, 24) - } - - Button("下载此版本") { - onSelect(version) - dismiss() - } - .buttonStyle(.borderedProminent) - .padding(.top, 8) - .padding(.leading, 16) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 8) - } - } - .padding(.horizontal) - .background(Color(NSColor.controlBackgroundColor)) - .cornerRadius(8) - } - } - } - .padding() - } - .background(Color(NSColor.windowBackgroundColor)) + HeaderView(sap: sap, downloadAppleSilicon: downloadAppleSilicon) + VersionListView( + sap: sap, + expandedVersions: $expandedVersions, + onSelect: onSelect, + dismiss: dismiss + ) } - .frame(width: 400, height: 500) + .frame(width: VersionPickerConstants.viewWidth, height: VersionPickerConstants.viewHeight) } } -#Preview { - let networkManager = NetworkManager() +private struct HeaderView: View { + let sap: Sap + let downloadAppleSilicon: Bool + @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var networkManager: NetworkManager - return VersionPickerView( - sap: Sap( + var body: some View { + VStack { + HStack { + Text("\(sap.displayName)") + .font(.headline) + Text("选择版本") + .foregroundColor(.secondary) + Spacer() + Button("取消") { + dismiss() + } + } + .padding(.bottom, VersionPickerConstants.headerPadding) + + Text("🔔 即将下载 \(downloadAppleSilicon ? "Apple Silicon" : "Intel") (\(platformText)) 版本 🔔") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.horizontal) + .padding(.top) + .background(Color(NSColor.windowBackgroundColor)) + } + + private var platformText: String { + networkManager.allowedPlatform.joined(separator: ", ") + } +} + +private struct VersionListView: View { + @EnvironmentObject private var networkManager: NetworkManager + let sap: Sap + @Binding var expandedVersions: Set + let onSelect: (String) -> Void + let dismiss: DismissAction + + var body: some View { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: VersionPickerConstants.verticalSpacing) { + ForEach(filteredVersions, id: \.key) { version, info in + VersionRow( + sap: sap, + version: version, + info: info, + isExpanded: expandedVersions.contains(version), + onSelect: handleVersionSelect, + onToggle: handleVersionToggle + ) + } + } + .padding() + } + .background(Color(NSColor.windowBackgroundColor)) + } + + private var filteredVersions: [(key: String, value: Sap.Versions)] { + sap.versions + .filter { networkManager.allowedPlatform.contains($0.value.apPlatform) } + .sorted { $0.key > $1.key } + } + + private func handleVersionSelect(_ version: String) { + onSelect(version) + dismiss() + } + + private func handleVersionToggle(_ version: String) { + withAnimation { + if expandedVersions.contains(version) { + expandedVersions.remove(version) + } else { + expandedVersions.insert(version) + } + } + } +} + +private struct VersionRow: View { + @EnvironmentObject private var networkManager: NetworkManager + @StorageValue(\.defaultLanguage) private var defaultLanguage + + let sap: Sap + let version: String + let info: Sap.Versions + let isExpanded: Bool + let onSelect: (String) -> Void + let onToggle: (String) -> Void + + private var existingPath: URL? { + networkManager.isVersionDownloaded( + sap: sap, + version: version, + language: defaultLanguage + ) + } + + var body: some View { + VStack(spacing: 0) { + VersionHeader( + version: version, + info: info, + isExpanded: isExpanded, + hasExistingPath: existingPath != nil, + onSelect: handleSelect, + onToggle: { onToggle(version) } + ) + + if isExpanded { + VersionDetails( + info: info, + version: version, + onSelect: onSelect + ) + } + } + .padding(.horizontal) + .background(Color(NSColor.controlBackgroundColor)) + .cornerRadius(VersionPickerConstants.cornerRadius) + } + + private func handleSelect() { + if info.dependencies.isEmpty { + onSelect(version) + } else { + onToggle(version) + } + } +} + +private struct VersionHeader: View { + let version: String + let info: Sap.Versions + let isExpanded: Bool + let hasExistingPath: Bool + let onSelect: () -> Void + let onToggle: () -> Void + + var body: some View { + Button(action: onSelect) { + HStack { + VersionInfo(version: version, platform: info.apPlatform) + Spacer() + ExistingPathButton(isVisible: hasExistingPath) + ExpandButton( + isExpanded: isExpanded, + hasDependencies: !info.dependencies.isEmpty + ) + } + .padding(.vertical, VersionPickerConstants.buttonPadding) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} + +private struct VersionInfo: View { + let version: String + let platform: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(version) + .font(.headline) + Text(platform) + .font(.caption) + .foregroundColor(.secondary) + } + } +} + +private struct ExistingPathButton: View { + let isVisible: Bool + + var body: some View { + if isVisible { + Text("已存在") + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.blue) + .cornerRadius(4) + } + } +} + +private struct ExpandButton: View { + let isExpanded: Bool + let hasDependencies: Bool + + var body: some View { + Image(systemName: iconName) + .foregroundColor(.secondary) + } + + private var iconName: String { + if !hasDependencies { + return "chevron.right" + } + return isExpanded ? "chevron.down" : "chevron.right" + } +} + +private struct VersionDetails: View { + let info: Sap.Versions + let version: String + let onSelect: (String) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: VersionPickerConstants.verticalSpacing) { + Text("依赖包:") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 8) + .padding(.leading, 16) + + DependenciesList(dependencies: info.dependencies) + + DownloadButton(version: version, onSelect: onSelect) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 8) + } +} + +private struct DependenciesList: View { + let dependencies: [Sap.Versions.Dependencies] + + var body: some View { + ForEach(dependencies, id: \.sapCode) { dependency in + HStack(spacing: 8) { + Image(systemName: "cube.box") + .foregroundColor(.blue) + .frame(width: 16) + Text("\(dependency.sapCode) (\(dependency.version))") + .font(.caption) + Spacer() + } + .padding(.leading, 24) + } + } +} + +private struct DownloadButton: View { + let version: String + let onSelect: (String) -> Void + + var body: some View { + Button("下载此版本") { + onSelect(version) + } + .buttonStyle(.borderedProminent) + .padding(.top, 8) + .padding(.leading, 16) + } +} + +struct VersionPickerView_Previews: PreviewProvider { + static var previews: some View { + let networkManager = NetworkManager() + networkManager.allowedPlatform = ["macuniversal", "macarm64"] + networkManager.cdn = "https://example.cdn.adobe.com" + + let previewSap = Sap( hidden: false, displayName: "Photoshop", sapCode: "PHSP", @@ -195,14 +354,11 @@ struct VersionPickerView: View { buildGuid: "q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6" ) ], - icons: [ - Sap.ProductIcon( - size: "192x192", - url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/26.0.0/192x192.png" - ) - ] - ), - onSelect: { version in } - ) - .environmentObject(networkManager) + icons: [] + ) + + return VersionPickerView(sap: previewSap) { _ in } + .environmentObject(networkManager) + .previewDisplayName("Version Picker") + } } diff --git a/AdobeDownloaderHelperTool/Info.plist b/AdobeDownloaderHelperTool/Info.plist index afa17e3..c75c176 100644 --- a/AdobeDownloaderHelperTool/Info.plist +++ b/AdobeDownloaderHelperTool/Info.plist @@ -1,25 +1,19 @@ - - CFBundleIdentifier - com.x1a0he.macOS.Adobe-Downloader.helper - CFBundleInfoDictionaryVersion - 1.0 - CFBundleName - com.x1a0he.macOS.Adobe-Downloader.helper - CFBundleShortVersionString - 1.0.0 - CFBundleVersion - 100 - SMAuthorizedClients - - identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he0907@gmail.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ - - MachServices - - com.x1a0he.macOS.Adobe-Downloader.helper - - - - \ No newline at end of file + + CFBundleIdentifier + com.x1a0he.macOS.Adobe-Downloader.helper + CFBundleName + com.x1a0he.macOS.Adobe-Downloader.helper + SMAuthorizedClients + + identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ + + MachServices + + com.x1a0he.macOS.Adobe-Downloader.helper + + + + diff --git a/Localizables/Localizable.xcstrings b/Localizables/Localizable.xcstrings index ff7cf6a..29e0bdf 100644 --- a/Localizables/Localizable.xcstrings +++ b/Localizables/Localizable.xcstrings @@ -5,7 +5,14 @@ }, "(可能导致处理 Setup 组件失败)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "(May failed to processing the Setup)" + } + } + } }, "(将导致无法使用安装功能)" : { "localizations" : { @@ -132,13 +139,23 @@ } } }, + "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件,请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adobe Downloader needs to use the background Daemon process to install and move files. Please allow the current App in \"System Preferences->Login Items->Allow in the Background\"" + } + } + } + }, "API:" : { }, - "By X1a0He. ❤️ Love from China. ❤️" : { + "Apple Silicon" : { }, - "Github: Adobe Downloader" : { + "By X1a0He. ❤️ Love from China. 🇨🇳" : { }, "GNU通用公共许可证GPL v3." : { @@ -151,17 +168,99 @@ } } }, + "Helper 响应异常" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper response exception" + } + } + } + }, + "Helper 安装失败" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper installation failed" + } + } + } + }, "Helper 安装状态: " : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper installation status:" + } + } + } }, "Helper 当前状态: " : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper Current status:" + } + } + } + }, + "Helper 未安装或未连接,请先在设置中安装并连接 Helper" : { }, "Helper 设置" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper Settings" + } + } + } + }, + "Helper 重新安装成功" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper reinstalled successfully" + } + } + } + }, + "Helper 重新连接成功" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper reconnected successfully" + } + } + } }, "Helper未安装将导致无法执行需要管理员权限的操作" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helper is not installed and cannot perform operations that require administrator privileges" + } + } + } + }, + "HTTP错误 %d: %@" : { + "comment" : "HTTP error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP error %1$d: %2$@" + } + } + } }, "OK" : { @@ -176,28 +275,6 @@ } } }, - "Setup 组件版本: %@" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Setup version: %@" - } - } - } - }, - "Setup 组件状态: " : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Setup status:" - } - } - } - }, "Setup未备份提示" : { "localizations" : { "en" : { @@ -218,16 +295,44 @@ }, "X1a0He CC 处理状态: " : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "X1a0He CC processing status:" + } + } + } }, "X1a0He CC 备份状态: " : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "X1a0He CC backup status:" + } + } + } }, - "X1a0He CC 版本信息: %@" : { - + "X1a0He CC 版本信息: %@ [%@]" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "X1a0He CC version info: %1$@ [%2$@]" + } + } + } }, "X1a0He CC设置" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "X1a0He CC settings" + } + } + } }, "下载" : { "localizations" : { @@ -250,9 +355,6 @@ } }, "下载 X1a0He CC" : { - - }, - "下载 X1a0He CC 组件" : { "localizations" : { "en" : { "stringUnit" : { @@ -272,6 +374,16 @@ } } }, + "下载中..." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Downloading…" + } + } + } + }, "下载失败" : { "localizations" : { "en" : { @@ -282,6 +394,16 @@ } } }, + "下载失败: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download failed: %@" + } + } + } + }, "下载已取消" : { "comment" : "Download cancelled", "localizations" : { @@ -345,6 +467,18 @@ } } }, + "下载错误, 错误原因: %@, %@" : { + "comment" : "Download error with cause", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@: %2$@" + } + } + } + }, "不使用安装功能" : { "localizations" : { "en" : { @@ -355,6 +489,17 @@ } } }, + "不支持的平台: %@" : { + "comment" : "Unsupported platform", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unsupported platform: %@" + } + } + } + }, "产品和包列表" : { "localizations" : { "en" : { @@ -365,17 +510,6 @@ } } }, - "从 GitHub 下载 Setup 组件" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download Setup from GitHub" - } - } - } - }, "你可以在设置中随时更改以上选项" : { "localizations" : { "en" : { @@ -396,16 +530,6 @@ } } }, - "使用现有程序" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use existing" - } - } - } - }, "使用默认目录" : { "localizations" : { "en" : { @@ -486,13 +610,13 @@ } } }, - "其他设置" : { - "extractionState" : "stale", + "准备中: %@" : { + "comment" : "Download status preparing", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Other settings" + "value" : "Preparing: %@" } } } @@ -588,6 +712,7 @@ } }, "失败" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -598,6 +723,7 @@ } }, "失败: %@" : { + "comment" : "Download status failed", "localizations" : { "en" : { "stringUnit" : { @@ -607,6 +733,16 @@ } } }, + "如果在设置里没找到当前App,可以尝试重置守护程序" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you can't find the current app in the settings, you can try resetting the guardian" + } + } + } + }, "存储空间不足" : { "localizations" : { "en" : { @@ -617,6 +753,17 @@ } } }, + "存储空间不足: 需要 %lld字节, 可用 %lld字节" : { + "comment" : "Insufficient storage", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insufficient storage space: %1$lld bytes needed, %2$lld bytes available" + } + } + } + }, "安装" : { "localizations" : { "en" : { @@ -627,20 +774,83 @@ } } }, - "安装失败 (退出代码: %lld)" : { + "安装失败" : { + }, + "安装失败 (退出代码: %lld)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed (Exit Code: %lld)" + } + } + } + }, + "安装失败: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed: %@" + } + } + } }, "安装失败: Setup 组件未被处理 (退出代码: %lld)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed: Setup was not processed. (Exit Code: %lld)" + } + } + } }, "安装失败: 安装文件不完整或损坏 (退出代码: %lld)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed: Incomplete file (Exit Code: %lld)" + } + } + } }, "安装失败: 权限问题 (退出代码: %lld)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed: Permission denied (Exit Code: %lld)" + } + } + } + }, + "安装失败: 架构不一致或安装文件被损坏 (退出代码: %lld)" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed: architecture inconsistency or installation file is corrupted (exit code: %lld)" + } + } + } }, "安装失败: 架构或版本不一致 (退出代码: %lld)" : { + }, + "安装失败: 磁盘空间不足 (退出代码: %lld)" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install failed: insufficient disk space (exit code: %lld)" + } + } + } }, "安装完成" : { "localizations" : { @@ -672,6 +882,28 @@ } } }, + "安装错误: %@" : { + "comment" : "Install error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install error: %@" + } + } + } + }, + "客户端错误: %d" : { + "comment" : "Client error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Client error: %@" + } + } + } + }, "尝试使用不同的搜索关键词" : { "localizations" : { "en" : { @@ -693,10 +925,24 @@ } }, "已处理" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Processed" + } + } + } }, "已备份" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Backed up" + } + } + } }, "已复制" : { "localizations" : { @@ -718,8 +964,15 @@ } } }, - "已安装" : { - + "已安装 (build %@)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installed (build %@)" + } + } + } }, "已完成" : { "localizations" : { @@ -731,6 +984,17 @@ } } }, + "已完成 (用时: %@)" : { + "comment" : "Download status completed", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Completed (Time: %@)" + } + } + } + }, "已暂停" : { "comment" : "Download status paused", "localizations" : { @@ -742,6 +1006,49 @@ } } }, + "已暂停: %@" : { + "comment" : "Download status paused with reason", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paused: %@" + } + } + } + }, + "已连接" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connected" + } + } + } + }, + "应用信息错误: %@" : { + "comment" : "Application info error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Application info error: %@" + } + } + } + }, + "应用信息错误(%@): %@" : { + "comment" : "Application info error with cause", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Application info error with cause(%1$@): %2$@" + } + } + } + }, "当前架构: %@" : { "localizations" : { "en" : { @@ -752,26 +1059,6 @@ } } }, - "感谢 Drovosek01: adobe-packager" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thanks Drovosek01: adobe-packager" - } - } - } - }, - "感谢 QiuChenly: InjectLib" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thanks QiuChenly: InjectLib" - } - } - } - }, "找不到安装程序" : { "localizations" : { "en" : { @@ -852,6 +1139,105 @@ } } }, + "数据无效: %@" : { + "comment" : "Invalid data", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invalid data: %@" + } + } + } + }, + "数据验证失败: %@" : { + "comment" : "Data validation error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data validation error: %@" + } + } + } + }, + "文件不存在: %@" : { + "comment" : "File not found", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "File not found: %@" + } + } + } + }, + "文件已存在: %@" : { + "comment" : "File exists", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "File exists: %@" + } + } + } + }, + "文件系统错误: %@" : { + "comment" : "File system error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "File system error: %@" + } + } + } + }, + "文件系统错误(%@): %@" : { + "comment" : "File system error with cause", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "文件系统错误(%1$@): %2$@" + } + } + } + }, + "文件访问权限被拒绝: %@" : { + "comment" : "File permission denied", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "File permission denied: %@" + } + } + } + }, + "无效的URL: %@" : { + "comment" : "Invalid URL", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invalid URL: %@" + } + } + } + }, + "无效的请求: %@" : { + "comment" : "Invalid request", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invalid request: %@" + } + } + } + }, "无法将响应数据转换为json字符串" : { "localizations" : { "en" : { @@ -862,6 +1248,57 @@ } } }, + "无法获取 Helper 代理" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to get Helper proxy" + } + } + } + }, + "无法获取Helper代理" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to get Helper proxy" + } + } + } + }, + "无法连接到 Helper" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to Helper" + } + } + } + }, + "无法连接到Helper" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to Helper" + } + } + } + }, + "无法连接到服务器: %@" : { + "comment" : "Server unreachable", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Server unreachable: %@" + } + } + } + }, "是否确认重新下载?这将覆盖现有的安装程序。" : { "localizations" : { "en" : { @@ -918,7 +1355,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Invalid server response" + "value" : "Invalid response" } } } @@ -933,8 +1370,26 @@ } } }, + "服务器错误: %d" : { + "comment" : "Server error", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Server error: %d" + } + } + } + }, "未安装" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Not Installed" + } + } + } }, "未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理" : { "localizations" : { @@ -967,15 +1422,11 @@ } }, "未检测到 Adobe CC 组件" : { - - }, - "未检测到 Adobe Setup 组件" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Adobe Setup component not detected" + "value" : "Adobe CC components not detected" } } } @@ -1007,6 +1458,16 @@ } } }, + "未连接" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disconnected" + } + } + } + }, "权限被拒绝" : { "localizations" : { "en" : { @@ -1017,6 +1478,16 @@ } } }, + "检查中" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking..." + } + } + } + }, "检查更新..." : { "localizations" : { "en" : { @@ -1053,7 +1524,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Downloading %1$@ (%2$d/%3$d)" + "value" : "Downloadind %1$@ (%2$d/%3$d)" } } } @@ -1099,7 +1570,24 @@ } }, "正在安装..." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installing…" + } + } + } + }, + "正在连接" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connecting…" + } + } + } }, "没有写入权限" : { "localizations" : { @@ -1121,17 +1609,6 @@ } } }, - "没有网络连接" : { - "comment" : "Network error", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Network error" - } - } - } - }, "清理已完成" : { "localizations" : { "en" : { @@ -1142,6 +1619,17 @@ } } }, + "版本不兼容: 当前版本 %@, 需要版本 %@" : { + "comment" : "Incompatible version", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Incompatible version: Current version %1$@, required version %2$@" + } + } + } + }, "确定" : { "localizations" : { "en" : { @@ -1153,10 +1641,24 @@ } }, "确定要下载并安装 X1a0He CC 吗?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to download and install X1a0He CC?" + } + } + } }, "确定要重新处理 Setup 组件吗?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to reprocess the Setup component?" + } + } + } }, "确认" : { "localizations" : { @@ -1239,15 +1741,11 @@ } }, "程序检测到你的系统中不存在 Adobe CC 组件" : { - - }, - "程序检测到你的系统中不存在 Adobe Setup 组件" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Detected that the Adobe Setup component is not present on your system" + "value" : "Detected that Adobe CC components are not present on your Mac" } } } @@ -1264,6 +1762,16 @@ }, "等待中" : { "comment" : "Download status waiting", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waiting" + } + } + } + }, + "等待中..." : { "localizations" : { "en" : { "stringUnit" : { @@ -1279,7 +1787,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "System hibernation" + "value" : "System sleep" } } } @@ -1300,7 +1808,19 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Network interrupt" + "value" : "Network outage" + } + } + } + }, + "网络无连接" : { + "comment" : "Network error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Network error" } } } @@ -1315,16 +1835,6 @@ } } }, - "联系 @X1a0He" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contact @X1a0He" - } - } - } - }, "自动下载最新版本" : { "localizations" : { "en" : { @@ -1355,13 +1865,85 @@ } } }, + "获取授权失败" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to obtain authorization" + } + } + } + }, + "获取管理员授权失败,用户主动取消授权!" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to obtain administrator authorization, the user actively canceled the authorization!" + } + } + } + }, + "获取管理员权限失败" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to obtain administrator privileges" + } + } + } + }, + "解析错误: %@ - %@" : { + "comment" : "Parsing error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Parsing error: %1$@ - %2$@" + } + } + } + }, "请求超时,请检查网络连接后重试" : { "comment" : "Network timeout", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Request timed out, please check the network connection and try again" + "value" : "Network timeout" + } + } + } + }, + "运行正常" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Working…" + } + } + } + }, + "连接出现错误: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error connecting: %@" + } + } + } + }, + "连接断开" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disconnected" } } } @@ -1427,16 +2009,6 @@ } } }, - "重新下载" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Re-download" - } - } - } - }, "重新下载时需要确认" : { "localizations" : { "en" : { @@ -1456,22 +2028,33 @@ } } } - }, - "重新备份" : { - }, "重新安装" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinstall" + } + } + } }, "重新连接Helper" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reconnect to Helper" + } + } + } }, "重试" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "retry" + "value" : "Retry" } } } diff --git a/update-log.md b/update-log.md index 53bd993..6f34e71 100644 --- a/update-log.md +++ b/update-log.md @@ -1,14 +1,30 @@ # Change Log -## 2024-11-13 00:00 更新日志 +## 2024-11-14 15:30 更新日志 ```markdown -1. 新增可选API版本 (v4, v5, v6) +1. 新增可选API版本 (v4, v5, v6)【更老的API意味着更长的等待时间】 2. 引入 Privilege Helper 来处理所有需要权限的操作 3. 修改从 Github 下载 Setup 组件功能,改为从官方下载简化版CC,称为 X1a0He CC 4. 调整 CC 组件备份与处理状态检测,分离二者的检测机制 5. 移除了安装日志显示 6. 调整 Setup 组件版本号的获取方式 +7. 修复了当任务下载完成后,AppCardView 仍显示下载中的问题 +8. 修复了 Intel 架构下,安装时因架构文件错误出现错误代码 107 的问题 + +PS: CC 组件的来源均为 Adobe Creative Cloud 官方提取,可随时下载到最新版,但处理可能会失败 +==================== + +1. Added optional API versions (v4, v5, v6) (Older API means longer waiting time) +2. Introduced Privilege Helper to handle all operations that require permissions +3. Modified the function of downloading the Setup component from Github to downloading a simplified version of CC from + the official website, called X1a0He CC +4. Adjusted the detection of CC component backup and processing status, and separated the detection mechanism of the two +5. Removed the installation log display +6. Adjusted the way to obtain the version number of the Setup component + +PS: CC components are all from Adobe Creative Cloud official extraction, you can download the latest version at any +time, but the processing may fail ``` ## 2024-11-11 21:00 更新日志