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 更新日志