perf: Save works

This commit is contained in:
X1a0He
2024-11-15 17:47:15 +08:00
parent 6411b478aa
commit fb33915c94
24 changed files with 2571 additions and 1228 deletions

View File

@@ -230,6 +230,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements; CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEAD_CODE_STRIPPING = NO; DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = TG862GVKHK; DEVELOPMENT_TEAM = TG862GVKHK;
@@ -237,7 +238,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist; INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist;
LOCALIZATION_PREFERS_STRING_CATALOGS = NO; LOCALIZATION_PREFERS_STRING_CATALOGS = NO;
MACOSX_DEPLOYMENT_TARGET = 15.1; MACOSX_DEPLOYMENT_TARGET = 12.0;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-sectcreate", "-sectcreate",
@@ -259,6 +260,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements; CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEAD_CODE_STRIPPING = NO; DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = TG862GVKHK; DEVELOPMENT_TEAM = TG862GVKHK;
@@ -266,7 +268,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist; INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist;
LOCALIZATION_PREFERS_STRING_CATALOGS = NO; LOCALIZATION_PREFERS_STRING_CATALOGS = NO;
MACOSX_DEPLOYMENT_TARGET = 15.1; MACOSX_DEPLOYMENT_TARGET = 12.0;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-sectcreate", "-sectcreate",
__TEXT, __TEXT,
@@ -413,6 +415,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements"; CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 120; CURRENT_PROJECT_VERSION = 120;
@@ -430,7 +433,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0; MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.2; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -445,6 +448,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements"; CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 120; CURRENT_PROJECT_VERSION = 120;
@@ -462,7 +466,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0; MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.2; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;

View File

@@ -14,8 +14,8 @@
filePath = "Adobe Downloader/Utils/DownloadUtils.swift" filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
startingColumnNumber = "9223372036854775807" startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807"
startingLineNumber = "463" startingLineNumber = "450"
endingLineNumber = "463" endingLineNumber = "450"
landmarkName = "startDownloadProcess(task:)" landmarkName = "startDownloadProcess(task:)"
landmarkType = "7"> landmarkType = "7">
</BreakpointContent> </BreakpointContent>
@@ -23,16 +23,48 @@
<BreakpointProxy <BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent <BreakpointContent
uuid = "E5CEF575-C3CE-40C5-8038-C2BE8D9FAEA0" uuid = "A757ED7D-136A-4033-8710-3B3C60074969"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "668"
endingLineNumber = "668"
landmarkName = "handleDownload(task:productInfo:allowedPlatform:saps:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "1C1D406E-5C7C-4A45-812C-48C7562B4156"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Adobe Downloader/Commons/Structs.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "149"
endingLineNumber = "149"
landmarkName = "Sap"
landmarkType = "14">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "1C96B561-A597-47BB-9DD8-F398805CB1B5"
shouldBeEnabled = "No" shouldBeEnabled = "No"
ignoreCount = "0" ignoreCount = "0"
continueAfterRunningActions = "No" continueAfterRunningActions = "No"
filePath = "Adobe Downloader/Utils/InstallManager.swift" filePath = "Adobe Downloader/Utils/InstallManager.swift"
startingColumnNumber = "9223372036854775807" startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807"
startingLineNumber = "128" startingLineNumber = "81"
endingLineNumber = "128" endingLineNumber = "81"
landmarkName = "retry(at:progressHandler:)" landmarkName = "executeInstallation(at:progressHandler:)"
landmarkType = "7"> landmarkType = "7">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>

View File

@@ -9,51 +9,42 @@ struct Adobe_DownloaderApp: App {
@State private var showTipsSheet = false @State private var showTipsSheet = false
@State private var showLanguagePicker = false @State private var showLanguagePicker = false
@State private var showCreativeCloudAlert = false @State private var showCreativeCloudAlert = false
@AppStorage("useDefaultLanguage") private var useDefaultLanguage: Bool = true @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
@AppStorage("defaultLanguage") private var defaultLanguage: String = "ALL" @StorageValue(\.defaultLanguage) private var defaultLanguage
@AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true @StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
@AppStorage("confirmRedownload") private var confirmRedownload: Bool = true @StorageValue(\.confirmRedownload) private var confirmRedownload
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true @StorageValue(\.useDefaultDirectory) private var useDefaultDirectory
@AppStorage("defaultDirectory") private var defaultDirectory: String = "" @StorageValue(\.defaultDirectory) private var defaultDirectory
@State private var showBackupResultAlert = false @State private var showBackupResultAlert = false
@State private var backupResultMessage = "" @State private var backupResultMessage = ""
@State private var backupSuccess = false @State private var backupSuccess = false
private let updaterController: SPUStandardUpdaterController private let updaterController: SPUStandardUpdaterController
init() { init() {
updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) if StorageData.shared.installedHelperBuild == "0" {
StorageData.shared.installedHelperBuild = "0"
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 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 systemLanguage = Locale.current.identifier
let matchedLanguage = AppStatics.supportedLanguages.first { let matchedLanguage = AppStatics.supportedLanguages.first {
systemLanguage.hasPrefix($0.code.prefix(2)) systemLanguage.hasPrefix($0.code.prefix(2))
}?.code ?? "ALL" }?.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 { if let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first {
print(downloadsURL.path) StorageData.shared.defaultDirectory = downloadsURL.path
defaultDirectory = downloadsURL.path StorageData.shared.useDefaultDirectory = true
UserDefaults.standard.set(true, forKey: "useDefaultDirectory")
UserDefaults.standard.set(downloadsURL.path, forKey: "defaultDirectory")
} }
} }
PrivilegedHelperManager.shared.checkInstall()
if UserDefaults.standard.string(forKey: "apiVersion") == nil { if StorageData.shared.apiVersion == "6" {
UserDefaults.standard.set("6", forKey: "apiVersion") StorageData.shared.apiVersion = "6"
} }
} }
@@ -64,6 +55,8 @@ struct Adobe_DownloaderApp: App {
.frame(width: 850, height: 800) .frame(width: 850, height: 800)
.tint(.blue) .tint(.blue)
.task { .task {
PrivilegedHelperManager.shared.checkInstall()
await MainActor.run { await MainActor.run {
appDelegate.networkManager = networkManager appDelegate.networkManager = networkManager
networkManager.loadSavedTasks() networkManager.loadSavedTasks()
@@ -79,9 +72,9 @@ struct Adobe_DownloaderApp: App {
showBackupAlert = true showBackupAlert = true
} }
if UserDefaults.standard.bool(forKey: "isFirstLaunch") { if StorageData.shared.isFirstLaunch {
showTipsSheet = true showTipsSheet = true
UserDefaults.standard.removeObject(forKey: "isFirstLaunch") StorageData.shared.isFirstLaunch = false
} }
} }
} }
@@ -113,7 +106,13 @@ struct Adobe_DownloaderApp: App {
VStack(spacing: 12) { VStack(spacing: 12) {
HStack { HStack {
Toggle("使用默认语言", isOn: $useDefaultLanguage) Toggle("使用默认语言", isOn: Binding(
get: { useDefaultLanguage },
set: {
useDefaultLanguage = $0
StorageData.shared.useDefaultLanguage = $0
}
))
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
Text(getLanguageName(code: defaultLanguage)) Text(getLanguageName(code: defaultLanguage))
@@ -127,7 +126,13 @@ struct Adobe_DownloaderApp: App {
Divider() Divider()
HStack { HStack {
Toggle("使用默认目录", isOn: $useDefaultDirectory) Toggle("使用默认目录", isOn: Binding(
get: { useDefaultDirectory },
set: {
useDefaultDirectory = $0
StorageData.shared.useDefaultDirectory = $0
}
))
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
Text(formatPath(defaultDirectory)) Text(formatPath(defaultDirectory))
@@ -143,7 +148,14 @@ struct Adobe_DownloaderApp: App {
Divider() Divider()
HStack { HStack {
Toggle("重新下载时需要确认", isOn: $confirmRedownload) Toggle("重新下载时需要确认", isOn: Binding(
get: { confirmRedownload },
set: {
confirmRedownload = $0
StorageData.shared.confirmRedownload = $0
NotificationCenter.default.post(name: .storageDidChange, object: nil)
}
))
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
} }
@@ -151,7 +163,14 @@ struct Adobe_DownloaderApp: App {
Divider() Divider()
HStack { HStack {
Toggle("下载 Apple Silicon 架构", isOn: $downloadAppleSilicon) Toggle("下载 Apple Silicon 架构", isOn: Binding(
get: { downloadAppleSilicon },
set: {
downloadAppleSilicon = $0
StorageData.shared.downloadAppleSilicon = $0
networkManager.updateAllowedPlatform(useAppleSilicon: $0)
}
))
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
Text("当前架构: \(AppStatics.cpuArchitecture)") Text("当前架构: \(AppStatics.cpuArchitecture)")
@@ -159,9 +178,6 @@ struct Adobe_DownloaderApp: App {
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle) .truncationMode(.middle)
} }
.onChange(of: downloadAppleSilicon) { newValue in
networkManager.updateAllowedPlatform(useAppleSilicon: newValue)
}
} }
.padding() .padding()
.background(Color(NSColor.controlBackgroundColor)) .background(Color(NSColor.controlBackgroundColor))
@@ -180,6 +196,7 @@ struct Adobe_DownloaderApp: App {
.sheet(isPresented: $showLanguagePicker) { .sheet(isPresented: $showLanguagePicker) {
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
defaultLanguage = language defaultLanguage = language
StorageData.shared.defaultLanguage = language
showLanguagePicker = false showLanguagePicker = false
} }
} }

View File

@@ -15,28 +15,11 @@ enum PackageStatus: Equatable, Codable {
var description: LocalizedStringKey { var description: LocalizedStringKey {
switch self { switch self {
case .waiting: return "等待中" case .waiting: return LocalizedStringKey("等待中...")
case .downloading: return "下载中" case .downloading: return LocalizedStringKey("下载中...")
case .paused: return "已暂停" case .paused: return LocalizedStringKey("已暂停")
case .completed: return "已完成" case .completed: return LocalizedStringKey("已完成")
case .failed(let message): return "失败: \(message)" case .failed(let message): return LocalizedStringKey("下载失败: \(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
} }
} }
} }
@@ -61,6 +44,7 @@ enum NetworkError: Error, LocalizedError {
case downloadError(String, Error?) case downloadError(String, Error?)
case downloadCancelled case downloadCancelled
case insufficientStorage(Int64, Int64) case insufficientStorage(Int64, Int64)
case cancelled
case fileSystemError(String, Error?) case fileSystemError(String, Error?)
case fileExists(String) case fileExists(String)
@@ -70,97 +54,112 @@ enum NetworkError: Error, LocalizedError {
case applicationInfoError(String, Error?) case applicationInfoError(String, Error?)
case unsupportedPlatform(String) case unsupportedPlatform(String)
case incompatibleVersion(String, String) case incompatibleVersion(String, String)
case cancelled
case installError(String) case installError(String)
var errorCode: Int { private var errorGroup: Int {
switch self { switch self {
case .noConnection: return 1001 case .noConnection, .timeout, .serverUnreachable: return 1000
case .timeout: return 1002 case .invalidURL, .invalidRequest, .invalidResponse: return 2000
case .serverUnreachable: return 1003 case .invalidData, .parsingError, .dataValidationError: return 3000
case .invalidURL: return 2001 case .httpError, .serverError, .clientError: return 4000
case .invalidRequest: return 2002 case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000
case .invalidResponse: return 2003 case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000
case .invalidData: return 3001 case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError: return 7000
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
} }
} }
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? { var errorDescription: String? {
switch self { switch self {
case .noConnection: case .noConnection:
return NSLocalizedString("没有网络连接", comment: "Network error") return NSLocalizedString("网络连接", value: "Network error", comment: "Network error")
case .timeout: case .timeout:
return NSLocalizedString("请求超时,请检查网络连接后重试", comment: "Network timeout") return NSLocalizedString("请求超时,请检查网络连接后重试", value: "请求超时,请检查网络连接后重试", comment: "Network timeout")
case .serverUnreachable(let server): case .serverUnreachable(let server):
return NSLocalizedString("无法连接到服务器: \(server)", comment: "Server unreachable") return String(format: NSLocalizedString("无法连接到服务器: %@", value: "无法连接到服务器: %@",comment: "Server unreachable"), server)
case .invalidURL(let url): case .invalidURL(let url):
return NSLocalizedString("无效的URL: \(url)", comment: "Invalid URL") return String(format: NSLocalizedString("无效的URL: %@", value: "无效的URL: %@", comment: "Invalid URL"), url)
case .invalidRequest(let reason): case .invalidRequest(let reason):
return NSLocalizedString("无效的请求: \(reason)", comment: "Invalid request") return String(format: NSLocalizedString("无效的请求: %@", value: "无效的请求: %@", comment: "Invalid request"), reason)
case .invalidResponse: case .invalidResponse:
return NSLocalizedString("服务器响应无效", comment: "Invalid response") return NSLocalizedString("服务器响应无效", value: "服务器响应无效", comment: "Invalid response")
case .invalidData(let detail): case .invalidData(let detail):
return NSLocalizedString("数据无效: \(detail)", comment: "Invalid data") return String(format: NSLocalizedString("数据无效: %@", value: "数据无效: %@", comment: "Invalid data"), detail)
case .parsingError(let error, let context): case .parsingError(let error, let context):
return NSLocalizedString("解析错误: \(context) - \(error.localizedDescription)", comment: "Parsing error") return String(format: NSLocalizedString("解析错误: %@ - %@", value: "Parsing error: %@ - %@", comment: "Parsing error"), context, error.localizedDescription)
case .dataValidationError(let reason): case .dataValidationError(let reason):
return NSLocalizedString("数据验证失败: \(reason)", comment: "Data validation error") return String(format: NSLocalizedString("数据验证失败: %@", value: "数据验证失败: %@", comment: "Data validation error"), reason)
case .httpError(let code, let message): case .httpError(let code, let message):
return NSLocalizedString("HTTP错误 \(code): \(message ?? "")", comment: "HTTP error") return String(format: NSLocalizedString("HTTP错误 %d: %@", value: "HTTP错误 %d: %@", comment: "HTTP error"), code, message ?? "")
case .serverError(let code): case .serverError(let code):
return NSLocalizedString("服务器错误: \(code)", comment: "Server error") return String(format: NSLocalizedString("服务器错误: %d", value: "服务器错误: %d", comment: "Server error"), code)
case .clientError(let code): case .clientError(let code):
return NSLocalizedString("客户端错误: \(code)", comment: "Client error") return String(format: NSLocalizedString("客户端错误: %d", value: "客户端错误: %d", comment: "Client error"), code)
case .downloadError(let message, let error): case .downloadError(let message, let error):
if let error = error { if let error = error {
return NSLocalizedString("\(message): \(error.localizedDescription)", comment: "Download error") return String(format: NSLocalizedString("下载错误, 错误原因: %@, %@", value: "%@: %@", comment: "Download error with cause"), message, error.localizedDescription)
} }
return NSLocalizedString(message, comment: "Download error") return NSLocalizedString(message, value: message, comment: "Download error")
case .downloadCancelled: case .downloadCancelled:
return NSLocalizedString("下载已取消", comment: "Download cancelled") return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
case .insufficientStorage(let needed, let available): case .insufficientStorage(let needed, let available):
return NSLocalizedString("存储空间不足: 需要 \(needed)字节, 可用 \(available)字节", comment: "Insufficient storage") return String(format: NSLocalizedString("存储空间不足: 需要 %lld字节, 可用 %lld字节", value: "存储空间不足: 需要 %lld字节, 可用 %lld字节", comment: "Insufficient storage"), needed, available)
case .fileSystemError(let operation, let error): case .fileSystemError(let operation, let error):
if let error = error { if let error = error {
return NSLocalizedString("文件系统错误(\(operation)): \(error.localizedDescription)", comment: "File system error") return String(format: NSLocalizedString("文件系统错误(%@): %@", value: "文件系统错误(%@): %@", comment: "File system error with cause"), operation, error.localizedDescription)
} }
return NSLocalizedString("文件系统错误: \(operation)", comment: "File system error") return String(format: NSLocalizedString("文件系统错误: %@", value: "文件系统错误: %@", comment: "File system error"), operation)
case .fileExists(let path): case .fileExists(let path):
return NSLocalizedString("文件已存在: \(path)", comment: "File exists") return String(format: NSLocalizedString("文件已存在: %@", value: "文件已存在: %@", comment: "File exists"), path)
case .fileNotFound(let path): case .fileNotFound(let path):
return NSLocalizedString("文件不存在: \(path)", comment: "File not found") return String(format: NSLocalizedString("文件不存在: %@", value: "文件不存在: %@", comment: "File not found"), path)
case .filePermissionDenied(let path): case .filePermissionDenied(let path):
return NSLocalizedString("文件访问权限被拒绝: \(path)", comment: "File permission denied") return String(format: NSLocalizedString("文件访问权限被拒绝: %@", value: "文件访问权限被拒绝: %@", comment: "File permission denied"), path)
case .applicationInfoError(let message, let error): case .applicationInfoError(let message, let error):
if let error = error { if let error = error {
return NSLocalizedString("应用信息错误(\(message)): \(error.localizedDescription)", comment: "Application info error") return String(format: NSLocalizedString("应用信息错误(%@): %@", value: "应用信息错误(%@): %@", comment: "Application info error with cause"), message, error.localizedDescription)
} }
return NSLocalizedString("应用信息错误: \(message)", comment: "Application info error") return String(format: NSLocalizedString("应用信息错误: %@", value: "应用信息错误: %@", comment: "Application info error"), message)
case .unsupportedPlatform(let platform): case .unsupportedPlatform(let platform):
return NSLocalizedString("不支持的平台: \(platform)", comment: "Unsupported platform") return String(format: NSLocalizedString("不支持的平台: %@", value: "不支持的平台: %@", comment: "Unsupported platform"), platform)
case .incompatibleVersion(let current, let required): case .incompatibleVersion(let current, let required):
return NSLocalizedString("版本不兼容: 当前版本 \(current), 需要版本 \(required)", comment: "Incompatible version") return String(format: NSLocalizedString("版本不兼容: 当前版本 %@, 需要版本 %@", value: "版本不兼容: 当前版本 %@, 需要版本 %@", comment: "Incompatible version"), current, required)
case .cancelled: case .cancelled:
return NSLocalizedString("下载已取消", comment: "Download cancelled") return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
case .installError(let message): case .installError(let message):
return NSLocalizedString("安装错误: \(message)", comment: "Install error") return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message)
} }
} }
@@ -334,30 +333,31 @@ enum DownloadStatus: Equatable, Codable {
var description: String { var description: String {
switch self { switch self {
case .waiting: case .waiting:
return NSLocalizedString("等待中", comment: "Download status waiting") return NSLocalizedString("等待中", value: "等待中", comment: "Download status waiting")
case .preparing(let info): 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): 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) info.fileName, info.currentPackageIndex + 1, info.totalPackages)
case .paused(let info): case .paused(let info):
switch info.reason { switch info.reason {
case .userRequested: case .userRequested:
return NSLocalizedString("已暂停", comment: "Download status paused") return NSLocalizedString("已暂停", value: "已暂停", comment: "Download status paused")
case .networkIssue: case .networkIssue:
return NSLocalizedString("网络中断", comment: "Download status network paused") return NSLocalizedString("网络中断", value: "网络中断", comment: "Download status network paused")
case .systemSleep: case .systemSleep:
return NSLocalizedString("系统休眠", comment: "Download status system sleep") return NSLocalizedString("系统休眠", value: "系统休眠", comment: "Download status system sleep")
case .other(let reason): 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): case .completed(let info):
let duration = formatDuration(info.totalTime) return String(format: NSLocalizedString("已完成 (用时: %@)", value: "已完成 (用时: %@)", comment: "Download status completed"),
return NSLocalizedString("已完成 (用时: \(duration))", comment: "Download status completed") info.totalTime.formatDuration())
case .failed(let info): 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): 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) info.attempt, info.maxAttempts)
} }
} }
@@ -393,12 +393,10 @@ enum DownloadStatus: Equatable, Codable {
} }
var canRetry: Bool { var canRetry: Bool {
switch self { if case .failed(let info) = self {
case .failed(let info):
return info.recoverable return info.recoverable
default:
return false
} }
return false
} }
var canPause: Bool { var canPause: Bool {
@@ -411,92 +409,26 @@ enum DownloadStatus: Equatable, Codable {
} }
var canResume: Bool { var canResume: Bool {
switch self { if case .paused(let info) = self {
case .paused(let info):
return info.resumable return info.resumable
default: }
return false return false
} }
} }
}
extension DownloadStatus { extension DownloadStatus.PrepareInfo: Equatable {}
static func == (lhs: DownloadStatus, rhs: DownloadStatus) -> Bool { extension DownloadStatus.PrepareInfo.PrepareStage: Equatable {}
switch (lhs, rhs) { extension DownloadStatus.PauseInfo.PauseReason: Equatable {}
case (.waiting, .waiting): extension DownloadStatus.DownloadInfo: Equatable {}
return true extension DownloadStatus.PauseInfo: Equatable {}
case (.preparing(let lInfo), .preparing(let rInfo)): extension DownloadStatus.CompletionInfo: Equatable {}
return lInfo.message == rInfo.message && extension DownloadStatus.RetryInfo: Equatable {}
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.FailureInfo: Equatable {
static func == (lhs: DownloadStatus.PrepareInfo, rhs: DownloadStatus.PrepareInfo) -> Bool { static func == (lhs: DownloadStatus.FailureInfo, rhs: DownloadStatus.FailureInfo) -> Bool {
return lhs.message == rhs.message && return lhs.message == rhs.message &&
lhs.timestamp == rhs.timestamp && lhs.timestamp == rhs.timestamp &&
lhs.stage == rhs.stage lhs.recoverable == rhs.recoverable
}
}
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
}
} }
} }
@@ -508,13 +440,9 @@ enum LoadingState: Equatable {
static func == (lhs: LoadingState, rhs: LoadingState) -> Bool { static func == (lhs: LoadingState, rhs: LoadingState) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.idle, .idle): case (.idle, .idle), (.loading, .loading), (.success, .success):
return true return true
case (.loading, .loading): case let (.failed(lError), .failed(rError)):
return true
case (.success, .success):
return true
case (.failed(let lError), .failed(let rError)):
return lError.localizedDescription == rError.localizedDescription return lError.localizedDescription == rError.localizedDescription
default: default:
return false return false
@@ -522,60 +450,19 @@ enum LoadingState: Equatable {
} }
} }
private func formatDuration(_ seconds: TimeInterval) -> String { private extension TimeInterval {
if seconds < 60 { func formatDuration() -> String {
return String(format: "%.1f秒", seconds) if self < 60 {
} else if seconds < 3600 { return String(format: "%.1f秒", self)
let minutes = Int(seconds / 60) } else if self < 3600 {
let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60)) let minutes = Int(self / 60)
let remainingSeconds = Int(self.truncatingRemainder(dividingBy: 60))
return "\(minutes)\(remainingSeconds)" return "\(minutes)\(remainingSeconds)"
} else { } else {
let hours = Int(seconds / 3600) let hours = Int(self / 3600)
let minutes = Int((seconds.truncatingRemainder(dividingBy: 3600)) / 60) let minutes = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60)) let remainingSeconds = Int(self.truncatingRemainder(dividingBy: 60))
return "\(hours)小时\(minutes)\(remainingSeconds)" 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
}
} }

View File

@@ -9,20 +9,13 @@ import AppKit
extension NewDownloadTask { extension NewDownloadTask {
var startTime: Date { var startTime: Date {
switch totalStatus { switch totalStatus {
case .downloading(let info): case .downloading(let info): return info.startTime
return info.startTime case .completed(let info): return info.timestamp - info.totalTime
case .completed(let info): case .preparing(let info): return info.timestamp
return info.timestamp.addingTimeInterval(-info.totalTime) case .paused(let info): return info.timestamp
case .preparing(let info): case .failed(let info): return info.timestamp
return info.timestamp case .retrying(let info): return info.nextRetryDate - 60
case .paused(let info): case .waiting, .none: return createAt
return info.timestamp
case .failed(let info):
return info.timestamp
case .retrying(let info):
return info.nextRetryDate.addingTimeInterval(-60)
case .waiting, .none:
return createAt
} }
} }
} }
@@ -30,36 +23,40 @@ extension NewDownloadTask {
extension NetworkManager { extension NetworkManager {
func configureNetworkMonitor() { func configureNetworkMonitor() {
monitor.pathUpdateHandler = { [weak self] path in monitor.pathUpdateHandler = { [weak self] path in
Task { @MainActor in Task { @MainActor [weak self] in
guard let self = self else { return } guard let self else { return }
let wasConnected = self.isConnected let wasConnected = self.isConnected
self.isConnected = path.status == .satisfied self.isConnected = path.status == .satisfied
switch (wasConnected, self.isConnected) {
case (false, true): await resumePausedTasks()
case (true, false): await pauseActiveTasks()
default: break
}
}
}
monitor.start(queue: .global(qos: .utility))
}
if !wasConnected && self.isConnected { private func resumePausedTasks() async {
for task in self.downloadTasks { for task in downloadTasks {
if case .paused(let info) = task.status, if case .paused(let info) = task.status,
info.reason == .networkIssue { info.reason == .networkIssue {
await self.downloadUtils.resumeDownloadTask(taskId: task.id) await downloadUtils.resumeDownloadTask(taskId: task.id)
} }
} }
} else if wasConnected && !self.isConnected { }
for task in self.downloadTasks {
private func pauseActiveTasks() async {
for task in downloadTasks {
if case .downloading = task.status { if case .downloading = task.status {
await self.downloadUtils.pauseDownloadTask( await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
taskId: task.id,
reason: .networkIssue
)
} }
} }
} }
}
}
monitor.start(queue: DispatchQueue.global(qos: .utility))
}
func generateCookie() -> String { func generateCookie() -> String {
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let randomString = String((0..<26).map { _ in letters.randomElement()! }) let randomString = (0..<26).map { _ in chars.randomElement()! }
return "fg=\(randomString)======" return "fg=\(String(randomString))======"
} }
} }

View File

@@ -6,7 +6,15 @@ struct ContentView: View {
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var showDownloadManager = false @State private var showDownloadManager = false
@State private var searchText = "" @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] { private var filteredProducts: [Sap] {
let products = networkManager.saps.values let products = networkManager.saps.values
@@ -37,17 +45,36 @@ struct ContentView: View {
Spacer() 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 { HStack {
Text("API:") Text("API:")
.foregroundColor(.secondary) Picker("", selection: $currentApiVersion) {
Picker("", selection: $apiVersion) {
Text("v4").tag("4") Text("v4").tag("4")
Text("v5").tag("5") Text("v5").tag("5")
Text("v6").tag("6") Text("v6").tag("6")
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.frame(width: 150) .frame(width: 150)
.onChange(of: apiVersion) { newValue in .onChange(of: currentApiVersion) { newValue in
StorageData.shared.apiVersion = newValue
refreshData() refreshData()
} }
} }

View File

@@ -31,24 +31,54 @@ class PrivilegedHelperManager: NSObject {
private var useLegacyInstall = false private var useLegacyInstall = false
private var connection: NSXPCConnection? 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 { enum ConnectionState {
case connected case connected
case disconnected case disconnected
case connecting 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() { override init() {
super.init() super.init()
initAuthorizationRef() initAuthorizationRef()
setupAutoReconnect()
DispatchQueue.main.async { [weak self] in
_ = self?.connectToHelper()
}
} }
func checkInstall() { 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 getHelperStatus { [weak self] status in
guard let self = self else { return } guard let self = self else { return }
switch status { switch status {
@@ -58,8 +88,8 @@ class PrivilegedHelperManager: NSObject {
let status = SMAppService.statusForLegacyPlist(at: url) let status = SMAppService.statusForLegacyPlist(at: url)
if status == .requiresApproval { if status == .requiresApproval {
let alert = NSAlert() let alert = NSAlert()
let notice = "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App" let notice = String(localized: "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App")
let addition = "如果在设置里没找到当前App可以尝试重置守护程序" let addition = String(localized: "如果在设置里没找到当前App可以尝试重置守护程序")
alert.messageText = notice + "\n" + addition alert.messageText = notice + "\n" + addition
alert.addButton(withTitle: "打开系统登录项设置") alert.addButton(withTitle: "打开系统登录项设置")
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)") 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(nsError.code)
} }
return .blessError(-1)
} }
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild")
}
return .success return .success
} }
@@ -140,19 +174,25 @@ class PrivilegedHelperManager: NSObject {
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName) let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName)
guard guard
let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any], CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else {
let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String else {
reply(.noFound) reply(.noFound)
return return
} }
let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)")
if !helperFileExists { if !helperFileExists {
reply(.noFound) reply(.noFound)
return return
} }
reply(.installed) 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 { static var getHelperStatus: Bool {
@@ -163,7 +203,6 @@ class PrivilegedHelperManager: NSObject {
status = helperStatus == .installed status = helperStatus == .installed
semaphore.signal() semaphore.signal()
} }
semaphore.wait() semaphore.wait()
return status return status
} }
@@ -178,30 +217,30 @@ class PrivilegedHelperManager: NSObject {
guard let self = self else { return } guard let self = self else { return }
guard let connection = connectToHelper() else { guard let connection = connectToHelper() else {
completion(false, "无法连接到Helper") completion(false, String(localized: "无法连接到Helper"))
return return
} }
guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else { guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else {
completion(false, "无法获取Helper代理") completion(false, String(localized: "无法获取Helper代理"))
return return
} }
helper.executeCommand("whoami") { result in helper.executeCommand("whoami") { result in
if result.contains("root") { if result.contains("root") {
completion(true, "Helper 重新安装成功") completion(true, String(localized: "Helper 重新安装成功"))
} else { } else {
completion(false, "Helper未能获取root权限") completion(false, String(localized: "Helper 安装失败"))
} }
} }
} }
case .authorizationFail: case .authorizationFail:
completion(false, "获取授权失败") completion(false, String(localized: "获取授权失败"))
case .getAdminFail: case .getAdminFail:
completion(false, "获取管理员权限失败") completion(false, String(localized: "获取管理员权限失败"))
case .blessError(_): case .blessError(_):
completion(false, "安装失败: \(result.alertContent)") completion(false, String(localized: "安装失败: \(result.alertContent)"))
} }
} }
@@ -210,18 +249,10 @@ class PrivilegedHelperManager: NSObject {
try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist") try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist")
} }
private func connectToHelper() -> NSXPCConnection? { func connectToHelper() -> NSXPCConnection? {
connectionState = .connecting connectionState = .connecting
objc_sync_enter(self) return connectionQueue.sync {
defer { objc_sync_exit(self) }
if let existingConnection = connection,
existingConnection.remoteObjectProxy != nil {
connectionState = .connected
return existingConnection
}
connection?.invalidate() connection?.invalidate()
connection = nil connection = nil
@@ -249,79 +280,88 @@ class PrivilegedHelperManager: NSObject {
newConnection.resume() newConnection.resume()
connection = newConnection connection = newConnection
let semaphore = DispatchSemaphore(value: 0)
var isConnected = false
if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol { if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol {
helper.executeCommand("whoami") { [weak self] result in helper.executeCommand("whoami") { [weak self] result in
if result == "root" { if result == "root" {
isConnected = true
DispatchQueue.main.async { DispatchQueue.main.async {
self?.connectionState = .connected self?.connectionState = .connected
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
self?.connectionState = .disconnected self?.connectionState = .disconnected
self?.connection?.invalidate()
self?.connection = nil
} }
} }
} semaphore.signal()
} }
return newConnection _ = semaphore.wait(timeout: .now() + 1.0)
if !isConnected {
connectionState = .disconnected
connection?.invalidate()
connection = nil
return nil
}
} else {
connectionState = .disconnected
return nil
}
return connection
}
} }
func executeCommand(_ command: String, completion: @escaping (String) -> Void) { func executeCommand(_ command: String, completion: @escaping (String) -> Void) {
guard let connection = connectToHelper() else { do {
connectionState = .disconnected let helper = try getHelperProxy()
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 helper.executeCommand(command) { [weak self] result in
DispatchQueue.main.async { DispatchQueue.main.async {
if self?.connection == nil {
self?.connectionState = .disconnected
completion("Error: Connection lost")
return
}
if result.starts(with: "Error:") { if result.starts(with: "Error:") {
self?.connectionState = .disconnected self?.connectionState = .disconnected
} else { } else {
self?.connectionState = .connected self?.connectionState = .connected
} }
completion(result) completion(result)
} }
} }
} catch {
connectionState = .disconnected
completion("Error: \(error.localizedDescription)")
}
} }
func reconnectHelper(completion: @escaping (Bool, String) -> Void) { func reconnectHelper(completion: @escaping (Bool, String) -> Void) {
connectionState = .disconnected
connection?.invalidate() connection?.invalidate()
connection = nil connection = nil
guard let newConnection = connectToHelper() else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
print("重新连接失败") guard let self = self else { return }
completion(false, "无法连接到 Helper")
guard let newConnection = self.connectToHelper() else {
completion(false, String(localized: "无法连接到 Helper"))
return return
} }
guard let helper = newConnection.remoteObjectProxyWithErrorHandler({ error in guard let helper = newConnection.remoteObjectProxyWithErrorHandler({ error in
completion(false, "连接出现错误: \(error.localizedDescription)") completion(false, String(localized: "连接出现错误: \(error.localizedDescription)"))
}) as? HelperToolProtocol else { }) as? HelperToolProtocol else {
completion(false, "无法获取 Helper 代理") completion(false, String(localized: "无法获取 Helper 代理"))
return return
} }
helper.executeCommand("whoami") { result in helper.executeCommand("whoami") { result in
if result == "root" { if result == "root" {
completion(true, "Helper 重新连接成功") completion(true, String(localized: "Helper 重新连接成功"))
} else { } else {
completion(false, "Helper 响应异常") completion(false, String(localized: "Helper 响应异常"))
}
} }
} }
} }
@@ -363,15 +403,27 @@ class PrivilegedHelperManager: NSObject {
try await Task.sleep(nanoseconds: 100_000_000) 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 { extension PrivilegedHelperManager {
private func notifyInstall() { private func notifyInstall() {
if useLegacyInstall { guard !isInitializing else { return }
useLegacyInstall = false
checkInstall()
return
}
let result = installHelperDaemon() let result = installHelperDaemon()
if case .success = result { if case .success = result {
@@ -385,7 +437,7 @@ extension PrivilegedHelperManager {
if !isCancle, useLegacyInstall { if !isCancle, useLegacyInstall {
checkInstall() checkInstall()
} else if isCancle, !useLegacyInstall { } else if isCancle, !useLegacyInstall {
NSAlert.alert(with: "获取管理员授权失败,用户主动取消授权!") NSAlert.alert(with: String(localized: "获取管理员授权失败,用户主动取消授权!"))
} }
} }
} }
@@ -459,3 +511,54 @@ extension NSAlert {
alert.runModal() 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
}
}

View File

@@ -12,7 +12,7 @@
<key>SMPrivilegedExecutables</key> <key>SMPrivilegedExecutables</key>
<dict> <dict>
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key> <key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<string>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 */</string> <string>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 */</string>
</dict> </dict>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string> <string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string>

View File

@@ -23,9 +23,21 @@ class NetworkManager: ObservableObject {
internal var monitor = NWPathMonitor() internal var monitor = NWPathMonitor()
internal var isFetchingProducts = false internal var isFetchingProducts = false
private let installManager = InstallManager() private let installManager = InstallManager()
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true private var defaultDirectory: String {
@AppStorage("apiVersion") private var apiVersion: String = "6" 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 { enum InstallationState {
case idle case idle
@@ -38,8 +50,9 @@ class NetworkManager: ObservableObject {
init(networkService: NetworkService = NetworkService(), init(networkService: NetworkService = NetworkService(),
downloadUtils: DownloadUtils? = nil) { downloadUtils: DownloadUtils? = nil) {
let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon") self.allowedPlatform = StorageData.shared.downloadAppleSilicon ?
self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"] ["macuniversal", "macarm64"] :
["macuniversal", "osx10-64", "osx10"]
self.networkService = networkService self.networkService = networkService
self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker) self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
@@ -51,7 +64,10 @@ class NetworkManager: ObservableObject {
func fetchProducts() async { func fetchProducts() async {
loadingState = .loading loadingState = .loading
do { 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 { await MainActor.run {
self.saps = saps self.saps = saps
self.cdn = cdn self.cdn = cdn
@@ -68,7 +84,7 @@ class NetworkManager: ObservableObject {
guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else { guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else {
throw NetworkError.invalidData("无法获取产品信息") throw NetworkError.invalidData("无法获取产品信息")
} }
print(productInfo.apPlatform)
let task = NewDownloadTask( let task = NewDownloadTask(
sapCode: sap.sapCode, sapCode: sap.sapCode,
version: selectedVersion, version: selectedVersion,
@@ -157,7 +173,7 @@ class NetworkManager: ObservableObject {
while retryCount < maxRetries { while retryCount < maxRetries {
do { 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 { await MainActor.run {
self.saps = saps self.saps = saps
@@ -345,11 +361,14 @@ class NetworkManager: ObservableObject {
} }
func updateAllowedPlatform(useAppleSilicon: Bool) { func updateAllowedPlatform(useAppleSilicon: Bool) {
allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"] allowedPlatform = useAppleSilicon ?
["macuniversal", "macarm64"] :
["macuniversal", "osx10-64", "osx10"]
} }
func saveTask(_ task: NewDownloadTask) { func saveTask(_ task: NewDownloadTask) {
TaskPersistenceManager.shared.saveTask(task) TaskPersistenceManager.shared.saveTask(task)
objectWillChange.send()
} }
func loadSavedTasks() { func loadSavedTasks() {

View File

@@ -1,7 +1,9 @@
import Foundation import Foundation
class NetworkService { 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) var components = URLComponents(string: NetworkConstants.productsXmlURL)
components?.queryItems = [ components?.queryItems = [
URLQueryItem(name: "_type", value: "xml"), URLQueryItem(name: "_type", value: "xml"),
@@ -15,10 +17,19 @@ class NetworkService {
guard let url = components?.url else { guard let url = components?.url else {
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL) 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) var request = URLRequest(url: url)
request.httpMethod = "GET" 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) let (data, response) = try await URLSession.shared.data(for: request)
@@ -34,7 +45,7 @@ class NetworkService {
throw NetworkError.invalidData("无法解码XML数据") 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 parseResult = try XHXMLParser.parse(xmlString: xmlString)
let products = parseResult.products, cdn = parseResult.cdn let products = parseResult.products, cdn = parseResult.cdn
var sapCodes: [SapCodes] = [] var sapCodes: [SapCodes] = []

View File

@@ -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<T>: DynamicProperty {
@ObservedObject private var storage = StorageData.shared
private let keyPath: ReferenceWritableKeyPath<StorageData, T>
var wrappedValue: T {
get { storage[keyPath: keyPath] }
nonmutating set {
storage[keyPath: keyPath] = newValue
}
}
var projectedValue: Binding<T> {
Binding(
get: { storage[keyPath: keyPath] },
set: { storage[keyPath: keyPath] = $0 }
)
}
init(_ keyPath: ReferenceWritableKeyPath<StorageData, T>) {
self.keyPath = keyPath
}
}
extension Notification.Name {
static let storageDidChange = Notification.Name("storageDidChange")
}

View File

@@ -26,6 +26,8 @@ class DownloadUtils {
var fileName: String var fileName: String
private var hasCompleted = false private var hasCompleted = false
private let completionLock = NSLock() private let completionLock = NSLock()
private var lastUpdateTime = Date()
private var lastBytes: Int64 = 0
init(destinationDirectory: URL, init(destinationDirectory: URL,
fileName: String, fileName: String,
@@ -93,13 +95,29 @@ class DownloadUtils {
guard totalBytesExpectedToWrite > 0 else { return } guard totalBytesExpectedToWrite > 0 else { return }
guard bytesWritten > 0 else { return } guard bytesWritten > 0 else { return }
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) handleProgressUpdate(
bytesWritten: bytesWritten,
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite
)
} }
func cleanup() { func cleanup() {
completionHandler = { _, _, _ in } completionHandler = { _, _, _ in }
progressHandler = nil 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 { func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
@@ -194,32 +212,15 @@ class DownloadUtils {
""" """
} }
func clearExtendedAttributes(at url: URL) async throws { private func executePrivilegedCommand(_ command: String) async throws -> String {
let escapedPath = url.path.replacingOccurrences(of: "'", with: "'\\''") return await withCheckedContinuation { continuation in
let script = """ PrivilegedHelperManager.shared.executeCommand(command) { result in
do shell script "sudo xattr -cr '\(escapedPath)'" with administrator privileges if result.starts(with: "Error:") {
""" continuation.resume(returning: result)
} else {
let process = Process() continuation.resume(returning: result)
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)
} }
} }
} catch {
print("Error executing xattr command:", error.localizedDescription)
} }
} }
@@ -505,11 +506,6 @@ class DownloadUtils {
let (manifestData, _) = try await URLSession.shared.data(for: request) 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) let manifestDoc = try XMLDocument(data: manifestData)
guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue, 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 { for dependency in productInfo.dependencies {
if let dependencyVersions = saps[dependency.sapCode]?.versions { 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 { var selectedVersion: (key: String, value: Sap.Versions)? = matchingVersions.first {
if firstGuid.isEmpty { firstGuid = versionInfo.buildGuid } allowedPlatform.contains($0.value.apPlatform)
if allowedPlatform.contains(versionInfo.apPlatform) {
buildGuid = versionInfo.buildGuid
break
}
} }
if buildGuid.isEmpty { buildGuid = firstGuid } selectedVersion = selectedVersion ?? matchingVersions.first
if !buildGuid.isEmpty { if let version = selectedVersion {
productsToDownload.append(ProductsToDownload( productsToDownload.append(ProductsToDownload(
sapCode: dependency.sapCode, sapCode: dependency.sapCode,
version: dependency.version, version: dependency.version,
buildGuid: buildGuid buildGuid: version.value.buildGuid
)) ))
} }
} }
@@ -1132,4 +1123,67 @@ class DownloadUtils {
throw error 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")
}
}
} }

View File

@@ -5,9 +5,10 @@
// //
/* /*
Adobe Exit Code Adobe Exit Code
107: 107:
103: 103:
182: 182:
133:
*/ */
import Foundation import Foundation
@@ -32,6 +33,44 @@ actor InstallManager {
private var progressHandler: ((Double, String) -> Void)? private var progressHandler: ((Double, String) -> Void)?
private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" 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( private func executeInstallation(
at appPath: URL, at appPath: URL,
progressHandler: @escaping (Double, String) -> Void progressHandler: @escaping (Double, String) -> Void
@@ -41,7 +80,7 @@ actor InstallManager {
} }
let driverPath = appPath.appendingPathComponent("driver.xml").path 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 { await MainActor.run {
progressHandler(0.0, String(localized: "正在准备安装...")) progressHandler(0.0, String(localized: "正在准备安装..."))
@@ -123,6 +162,9 @@ actor InstallManager {
at appPath: URL, at appPath: URL,
progressHandler: @escaping (Double, String) -> Void progressHandler: @escaping (Double, String) -> Void
) async throws { ) async throws {
cancel()
try await Task.sleep(nanoseconds: 1_000_000_000)
try await executeInstallation( try await executeInstallation(
at: appPath, at: appPath,
progressHandler: progressHandler progressHandler: progressHandler

View File

@@ -11,29 +11,61 @@ struct ParseResult {
} }
class XHXMLParser { 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 { static func parseProductsXML(xmlData: Data) throws -> ParseResult {
let xml = try XMLDocument(data: xmlData) 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 throw ParserError.missingCDN
} }
var products: [String: Sap] = [:] var products = [String: Sap](minimumCapacity: 100)
let productNodes = try xml.nodes(forXPath: "//channels/channel/products/product") let productNodes = try xml.nodes(forXPath: xpathCache["products"]!)
let parentMap = createParentMap(xml.rootElement()) let parentMap = createParentMap(xml.rootElement())
for productNode in productNodes { for productNode in productNodes {
guard let element = productNode as? XMLElement else { continue } guard let element = productNode as? XMLElement else { continue }
let sap = element.attribute(forName: "id")?.stringValue ?? "" let sap = element.attribute(forName: "id")?.stringValue ?? ""
let parentElement = parentMap[parentMap[element] ?? element] let parentElement = parentMap[parentMap[element] ?? element]
let hidden = (parentElement as? XMLElement)?.attribute(forName: "name")?.stringValue != "ccm" let hidden = (parentElement as? XMLElement)?.attribute(forName: "name")?.stringValue != "ccm"
let displayName = try element.nodes(forXPath: "displayName").first?.stringValue ?? "" let displayName = try element.nodes(forXPath: "displayName").first?.stringValue ?? ""
var productVersion = element.attribute(forName: "version")?.stringValue ?? "" var productVersion = element.attribute(forName: "version")?.stringValue ?? ""
if products[sap] == nil { 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, guard let element = node as? XMLElement,
let size = element.attribute(forName: "size")?.stringValue, let size = element.attribute(forName: "size")?.stringValue,
let url = element.stringValue else { 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 { for platformNode in platforms {
guard let platform = platformNode as? XMLElement, 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 baseVersion = languageSet.attribute(forName: "baseVersion")?.stringValue ?? ""
var buildGuid = languageSet.attribute(forName: "buildGuid")?.stringValue ?? "" var buildGuid = languageSet.attribute(forName: "buildGuid")?.stringValue ?? ""
let appPlatform = platform.attribute(forName: "id")?.stringValue ?? "" let appPlatform = platform.attribute(forName: "id")?.stringValue ?? ""
if let existingVersion = products[sap]?.versions[productVersion] { if let existingVersion = products[sap]?.versions[productVersion],
if existingVersion.apPlatform == "macuniversal" { existingVersion.apPlatform == "macuniversal" {
break break
} }
}
let dependencies = try languageSet.nodes(forXPath: "dependencies/dependency").compactMap { node -> Sap.Versions.Dependencies? in let dependencies = try languageSet.nodes(forXPath: xpathCache["dependencies"]!).compactMap { node -> Sap.Versions.Dependencies? in
guard let element = node as? XMLElement, guard let element = node as? XMLElement else { return nil }
let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue, let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue ?? ""
let version = try element.nodes(forXPath: "baseVersion").first?.stringValue else { let version = try element.nodes(forXPath: "baseVersion").first?.stringValue ?? ""
return nil guard !sapCode.isEmpty, !version.isEmpty else { return nil }
}
return Sap.Versions.Dependencies(sapCode: sapCode, version: version) return Sap.Versions.Dependencies(sapCode: sapCode, version: version)
} }
if sap == "APRO" { if sap == "APRO" {
baseVersion = productVersion baseVersion = productVersion
let buildNodes = try xml.nodes(forXPath: "//builds/build") if let buildNode = try xml.nodes(forXPath: xpathCache["builds"]!).first(where: { node in
for buildNode in buildNodes { guard let element = node as? XMLElement else { return false }
guard let buildElement = buildNode as? XMLElement, return element.attribute(forName: "id")?.stringValue == sap &&
buildElement.attribute(forName: "id")?.stringValue == sap, element.attribute(forName: "version")?.stringValue == baseVersion
buildElement.attribute(forName: "version")?.stringValue == baseVersion else { }) as? XMLElement {
continue if let appVersion = try buildNode.nodes(forXPath: xpathCache["appVersion"]!).first?.stringValue {
}
if let appVersion = try buildElement.nodes(forXPath: "nglLicensingInfo/appVersion").first?.stringValue {
productVersion = appVersion productVersion = appVersion
break
} }
} }
buildGuid = try languageSet.nodes(forXPath: "urls/manifestURL").first?.stringValue ?? "" buildGuid = try languageSet.nodes(forXPath: xpathCache["manifestURL"]!).first?.stringValue ?? ""
} }
if !buildGuid.isEmpty { if !buildGuid.isEmpty {
@@ -108,23 +135,6 @@ class XHXMLParser {
return ParseResult(products: products, cdn: cdn) 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 { enum ParserError: Error {

View File

@@ -8,23 +8,33 @@ import SwiftUI
import Sparkle import Sparkle
import Combine import Combine
struct PulsingCircle: View {
let color: Color private enum AboutViewConstants {
@State private var scale: CGFloat = 1.0 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 { var body: some View {
Circle() Link(title, destination: URL(string: url)!)
.fill(color) .font(.system(size: AboutViewConstants.linkFontSize))
.frame(width: 8, height: 8) .foregroundColor(.blue)
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 1.0)
.repeatForever(autoreverses: true),
value: scale
)
.onAppear {
scale = 1.5
}
} }
} }
@@ -54,6 +64,73 @@ struct AboutView: View {
} }
} }
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)
.frame(width: 8, height: 8)
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 1.0)
.repeatForever(autoreverses: true),
value: scale
)
.onAppear {
scale = 1.5
}
}
}
final class GeneralSettingsViewModel: ObservableObject { final class GeneralSettingsViewModel: ObservableObject {
@Published var setupVersion: String = "" @Published var setupVersion: String = ""
@Published var isDownloadingSetup = false @Published var isDownloadingSetup = false
@@ -67,18 +144,40 @@ final class GeneralSettingsViewModel: ObservableObject {
@Published var showDownloadConfirmAlert = false @Published var showDownloadConfirmAlert = false
@Published var showReprocessConfirmAlert = false @Published var showReprocessConfirmAlert = false
@Published var isProcessing = false @Published var isProcessing = false
@Published var helperConnectionStatus: HelperConnectionStatus = .connecting @Published var helperConnectionStatus: HelperConnectionStatus = .disconnected
@Published var downloadAppleSilicon: Bool { @Published var downloadAppleSilicon: Bool {
didSet { didSet {
UserDefaults.standard.set(downloadAppleSilicon, forKey: "downloadAppleSilicon") StorageData.shared.downloadAppleSilicon = downloadAppleSilicon
} }
} }
@AppStorage("defaultLanguage") var defaultLanguage: String = "ALL" var defaultLanguage: String {
@AppStorage("defaultDirectory") var defaultDirectory: String = "" get { StorageData.shared.defaultLanguage }
@AppStorage("useDefaultLanguage") var useDefaultLanguage: Bool = true set { StorageData.shared.defaultLanguage = newValue }
@AppStorage("useDefaultDirectory") var useDefaultDirectory: Bool = true }
@AppStorage("confirmRedownload") var confirmRedownload: Bool = true
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 automaticallyChecksForUpdates: Bool
@Published var automaticallyDownloadsUpdates: Bool @Published var automaticallyDownloadsUpdates: Bool
@@ -99,7 +198,7 @@ final class GeneralSettingsViewModel: ObservableObject {
self.updater = updater self.updater = updater
self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates
self.downloadAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon") self.downloadAppleSilicon = StorageData.shared.downloadAppleSilicon
self.helperConnectionStatus = .connecting self.helperConnectionStatus = .connecting
@@ -117,9 +216,14 @@ final class GeneralSettingsViewModel: ObservableObject {
} }
.store(in: &cancellables) .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 { deinit {
@@ -159,7 +263,6 @@ struct GeneralSettingsView: View {
var body: some View { var body: some View {
Form { Form {
DownloadSettingsView(viewModel: viewModel) DownloadSettingsView(viewModel: viewModel)
HelperSettingsView(viewModel: viewModel, HelperSettingsView(viewModel: viewModel,
showHelperAlert: $showHelperAlert, showHelperAlert: $showHelperAlert,
helperAlertMessage: $helperAlertMessage, helperAlertMessage: $helperAlertMessage,
@@ -259,6 +362,9 @@ struct GeneralSettingsView: View {
viewModel.setupVersion = ModifySetup.checkComponentVersion() viewModel.setupVersion = ModifySetup.checkComponentVersion()
networkManager.updateAllowedPlatform(useAppleSilicon: viewModel.downloadAppleSilicon) 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") { #Preview("About Tab") {
AboutAppView() AboutAppView()
} }
@@ -419,7 +475,10 @@ struct LanguageSettingRow: View {
var body: some View { var body: some View {
HStack { HStack {
Toggle("使用默认语言", isOn: $viewModel.useDefaultLanguage) Toggle("使用默认语言", isOn: Binding(
get: { viewModel.useDefaultLanguage },
set: { viewModel.useDefaultLanguage = $0 }
))
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
Text(getLanguageName(code: viewModel.defaultLanguage)) Text(getLanguageName(code: viewModel.defaultLanguage))
@@ -500,6 +559,7 @@ struct ArchitectureSettingRow: View {
HStack { HStack {
Toggle("下载 Apple Silicon 架构", isOn: $viewModel.downloadAppleSilicon) Toggle("下载 Apple Silicon 架构", isOn: $viewModel.downloadAppleSilicon)
.padding(.leading, 5) .padding(.leading, 5)
.disabled(networkManager.loadingState == .loading)
Spacer() Spacer()
Text("当前架构: \(AppStatics.cpuArchitecture)") Text("当前架构: \(AppStatics.cpuArchitecture)")
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -508,6 +568,9 @@ struct ArchitectureSettingRow: View {
} }
.onChange(of: viewModel.downloadAppleSilicon) { newValue in .onChange(of: viewModel.downloadAppleSilicon) { newValue in
networkManager.updateAllowedPlatform(useAppleSilicon: newValue) networkManager.updateAllowedPlatform(useAppleSilicon: newValue)
Task {
await networkManager.fetchProducts()
}
} }
} }
} }
@@ -518,6 +581,7 @@ struct HelperStatusRow: View {
@Binding var helperAlertMessage: String @Binding var helperAlertMessage: String
@Binding var helperAlertSuccess: Bool @Binding var helperAlertSuccess: Bool
@State private var isReinstallingHelper = false @State private var isReinstallingHelper = false
@State private var installationTask: Task<Void, Error>?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@@ -526,7 +590,7 @@ struct HelperStatusRow: View {
if PrivilegedHelperManager.getHelperStatus { if PrivilegedHelperManager.getHelperStatus {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green) .foregroundColor(.green)
Text("已安装") Text("已安装 (build \(UserDefaults.standard.string(forKey: "InstalledHelperBuild") ?? "0"))")
} else { } else {
Image(systemName: "xmark.circle.fill") Image(systemName: "xmark.circle.fill")
.foregroundColor(.red) .foregroundColor(.red)
@@ -594,10 +658,10 @@ struct HelperStatusRow: View {
private var helperStatusText: String { private var helperStatusText: String {
switch viewModel.helperConnectionStatus { switch viewModel.helperConnectionStatus {
case .connected: return "运行正常" case .connected: return String(localized: "运行正常")
case .connecting: return "正在连接" case .connecting: return String(localized: "正在连接")
case .checking: return "检查中" case .checking: return String(localized: "检查中")
case .disconnected: return "连接断开" case .disconnected: return String(localized: "连接断开")
} }
} }
} }
@@ -618,17 +682,6 @@ struct SetupComponentRow: View {
.foregroundColor(.red) .foregroundColor(.red)
Text("(可能导致处理 Setup 组件失败)") Text("(可能导致处理 Setup 组件失败)")
} }
Spacer()
Button(action: {
if !ModifySetup.isSetupExists() {
viewModel.showDownloadAlert = true
} else {
viewModel.showReprocessConfirmAlert = true
}
}) {
Text("重新备份")
}
} }
Divider() Divider()
HStack { HStack {
@@ -656,7 +709,7 @@ struct SetupComponentRow: View {
} }
Divider() Divider()
HStack { HStack {
Text("X1a0He CC 版本信息: \(viewModel.setupVersion)") Text("X1a0He CC 版本信息: \(viewModel.setupVersion) [\(AppStatics.cpuArchitecture)]")
Spacer() Spacer()
if viewModel.isDownloadingSetup { if viewModel.isDownloadingSetup {
@@ -706,3 +759,4 @@ struct AutoDownloadRow: View {
.disabled(viewModel.isAutomaticallyDownloadsUpdatesDisabled) .disabled(viewModel.isAutomaticallyDownloadsUpdatesDisabled)
} }
} }

View File

@@ -7,7 +7,23 @@
import SwiftUI import SwiftUI
import Combine 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() static let shared = IconCache()
private var cache = NSCache<NSString, NSImage>() private var cache = NSCache<NSString, NSImage>()
@@ -20,7 +36,8 @@ class IconCache {
} }
} }
class AppCardViewModel: ObservableObject { @MainActor
final class AppCardViewModel: ObservableObject {
@Published var iconImage: NSImage? @Published var iconImage: NSImage?
@Published var showError = false @Published var showError = false
@Published var errorMessage = "" @Published var errorMessage = ""
@@ -40,12 +57,12 @@ class AppCardViewModel: ObservableObject {
@Published var isDownloading = false @Published var isDownloading = false
private let userDefaults = UserDefaults.standard private let userDefaults = UserDefaults.standard
var useDefaultDirectory: Bool { private var useDefaultDirectory: Bool {
get { userDefaults.bool(forKey: "useDefaultDirectory") } StorageData.shared.useDefaultDirectory
} }
var defaultDirectory: String { private var defaultDirectory: String {
get { userDefaults.string(forKey: "defaultDirectory") ?? "" } StorageData.shared.defaultDirectory
} }
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@@ -54,10 +71,28 @@ class AppCardViewModel: ObservableObject {
self.sap = sap self.sap = sap
self.networkManager = networkManager self.networkManager = networkManager
Task { @MainActor in
setupObservers() setupObservers()
} }
}
@MainActor
private func setupObservers() { 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 networkManager?.objectWillChange
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.sink { [weak self] _ in .sink { [weak self] _ in
@@ -66,28 +101,31 @@ class AppCardViewModel: ObservableObject {
.store(in: &cancellables) .store(in: &cancellables)
} }
private func isTaskActive(_ status: DownloadStatus) -> Bool {
switch status {
case .downloading, .preparing, .waiting, .retrying:
return true
case .paused:
return false
case .completed, .failed:
return false
}
}
@MainActor
func updateDownloadingStatus() { func updateDownloadingStatus() {
guard let networkManager = networkManager else { guard let networkManager = networkManager else {
Task { @MainActor in
self.isDownloading = false self.isDownloading = false
}
return return
} }
Task { @MainActor in let hasActiveTask = networkManager.downloadTasks.contains {
let isActive = networkManager.downloadTasks.contains { task in $0.sapCode == sap.sapCode && isTaskActive($0.status)
task.sapCode == sap.sapCode && isTaskActive(task.status)
}
self.isDownloading = isActive
}
} }
private func isTaskActive(_ status: DownloadStatus) -> Bool { if hasActiveTask != self.isDownloading {
switch status { self.isDownloading = hasActiveTask
case .downloading, .preparing, .paused, .waiting, .retrying(_): self.objectWillChange.send()
return true
case .completed, .failed:
return false
} }
} }
@@ -185,7 +223,7 @@ class AppCardViewModel: ObservableObject {
func checkAndStartDownload(version: String, language: String) async { func checkAndStartDownload(version: String, language: String) async {
if let networkManager = networkManager { 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 { await MainActor.run {
existingFilePath = existingPath existingFilePath = existingPath
pendingVersion = version pendingVersion = version
@@ -212,7 +250,7 @@ class AppCardViewModel: ObservableObject {
guard let networkManager = networkManager, guard let networkManager = networkManager,
let productInfo = sap.versions[pendingVersion] else { return } 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 && return task.sapCode == sap.sapCode &&
task.version == pendingVersion && task.version == pendingVersion &&
task.language == pendingLanguage && task.language == pendingLanguage &&
@@ -232,14 +270,14 @@ class AppCardViewModel: ObservableObject {
productsToDownload.append(mainProduct) productsToDownload.append(mainProduct)
for dependency in productInfo.dependencies { 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 let sortedVersions = dependencyVersions.sorted { first, second in
first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending
} }
var buildGuid = "" var buildGuid = ""
for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version { 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 buildGuid = versionInfo.buildGuid
break break
} }
@@ -289,95 +327,81 @@ class AppCardViewModel: ObservableObject {
} }
return 0 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 { struct AppCardView: View {
@StateObject private var viewModel: AppCardViewModel @StateObject private var viewModel: AppCardViewModel
@EnvironmentObject private var networkManager: NetworkManager @EnvironmentObject private var networkManager: NetworkManager
@AppStorage("useDefaultLanguage") private var useDefaultLanguage = true @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN" @StorageValue(\.defaultLanguage) private var defaultLanguage
init(sap: Sap) { init(sap: Sap) {
_viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil)) _viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil))
} }
var body: some View { var body: some View {
CardContent( CardContainer {
sap: viewModel.sap, VStack {
iconImage: viewModel.iconImage, IconView(viewModel: viewModel)
loadIcon: viewModel.loadIcon, ProductInfoView(viewModel: viewModel)
dependenciesCount: viewModel.dependenciesCount, Spacer()
isDownloading: viewModel.isDownloading, DownloadButtonView(viewModel: viewModel)
showVersionPicker: $viewModel.showVersionPicker }
) }
.padding() .modifier(CardModifier())
.frame(width: 250, height: 200) .modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager))
.background(RoundedRectangle(cornerRadius: 10).fill(Color.black.opacity(0.05)))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.1), lineWidth: 2)
)
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true)) .modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
.sheet(isPresented: $viewModel.showVersionPicker) { .onAppear(perform: setupViewModel)
VersionPickerView(sap: viewModel.sap) { version in .onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus)
Task {
await viewModel.handleDownloadRequest(version, useDefaultLanguage: useDefaultLanguage, defaultLanguage: defaultLanguage)
} }
}
.environmentObject(networkManager) private func setupViewModel() {
}
.sheet(isPresented: $viewModel.showLanguagePicker) {
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
Task {
await viewModel.checkAndStartDownload(version: viewModel.selectedVersion, language: language)
}
}
}
.onAppear {
viewModel.networkManager = networkManager viewModel.networkManager = networkManager
viewModel.updateDownloadingStatus() viewModel.updateDownloadingStatus()
} }
.onChange(of: networkManager.downloadTasks) { _ in
private func updateDownloadStatus(_ _: [NewDownloadTask]) {
viewModel.updateDownloadingStatus() viewModel.updateDownloadingStatus()
} }
} }
}
private struct CardContent: View { private struct CardContainer<Content: View>: View {
let sap: Sap let content: Content
let iconImage: NSImage?
let loadIcon: () -> Void init(@ViewBuilder content: () -> Content) {
let dependenciesCount: Int self.content = content()
let isDownloading: Bool }
@Binding var showVersionPicker: Bool
var body: some View { var body: some View {
VStack { content
IconView(iconImage: iconImage, loadIcon: loadIcon) .padding()
ProductInfoView(sap: sap, dependenciesCount: dependenciesCount) .frame(width: AppCardConstants.cardWidth, height: AppCardConstants.cardHeight)
Spacer()
DownloadButton(
isDownloading: isDownloading,
showVersionPicker: $showVersionPicker
)
}
}
}
private extension View {
func applyModifiers(viewModel: AppCardViewModel) -> some View {
self.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
} }
} }
private struct IconView: View { private struct IconView: View {
let iconImage: NSImage? @ObservedObject var viewModel: AppCardViewModel
let loadIcon: () -> Void
var body: some View { var body: some View {
Group { Group {
if let iconImage = iconImage { if viewModel.hasValidIcon {
Image(nsImage: iconImage) Image(nsImage: viewModel.iconImage!)
.resizable() .resizable()
.interpolation(.high) .interpolation(.high)
.scaledToFit() .scaledToFit()
@@ -388,27 +412,26 @@ private struct IconView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
.frame(width: 64, height: 64) .frame(width: AppCardConstants.iconSize, height: AppCardConstants.iconSize)
.onAppear(perform: loadIcon) .onAppear(perform: viewModel.loadIcon)
} }
} }
private struct ProductInfoView: View { private struct ProductInfoView: View {
let sap: Sap @ObservedObject var viewModel: AppCardViewModel
let dependenciesCount: Int
var body: some View { var body: some View {
VStack { VStack {
Text(sap.displayName) Text(viewModel.sap.displayName)
.font(.system(size: 16)) .font(.system(size: AppCardConstants.titleFontSize))
.fontWeight(.bold) .fontWeight(.bold)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
HStack(spacing: 4) { HStack(spacing: 4) {
Text("可用版本: \(sap.versions.count)") Text("可用版本: \(viewModel.sap.versions.count)")
Text("|") Text("|")
Text("依赖包: \(dependenciesCount)") Text("依赖包: \(viewModel.dependenciesCount)")
} }
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -417,21 +440,74 @@ private struct ProductInfoView: View {
} }
} }
private struct DownloadButton: View { private struct DownloadButtonView: View {
let isDownloading: Bool @ObservedObject var viewModel: AppCardViewModel
@Binding var showVersionPicker: Bool
var body: some View { var body: some View {
Button(action: { showVersionPicker = true }) { Button(action: { viewModel.showVersionPicker = true }) {
Label(isDownloading ? "下载中" : "下载", Label(viewModel.downloadButtonTitle,
systemImage: isDownloading ? "hourglass.circle.fill" : "arrow.down.circle") systemImage: viewModel.downloadButtonIcon)
.font(.system(size: 14)) .font(.system(size: AppCardConstants.buttonFontSize))
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 32) .frame(height: AppCardConstants.buttonHeight)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(isDownloading ? .gray : .blue) .tint(viewModel.isDownloading ? .gray : .blue)
.disabled(isDownloading) .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,9 +557,16 @@ struct AlertModifier: ViewModifier {
viewModel.showExistingFileAlert = false viewModel.showExistingFileAlert = false
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty { if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
Task { Task {
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) await viewModel.createCompletedTask(path)
} }
} }
}
}, },
onRedownload: { onRedownload: {
viewModel.showExistingFileAlert = false viewModel.showExistingFileAlert = false
@@ -492,10 +575,7 @@ struct AlertModifier: ViewModifier {
viewModel.showRedownloadConfirm = true viewModel.showRedownloadConfirm = true
} else { } else {
Task { Task {
await viewModel.checkAndStartDownload( await startRedownload()
version: viewModel.pendingVersion,
language: viewModel.pendingLanguage
)
} }
} }
} }
@@ -505,20 +585,13 @@ struct AlertModifier: ViewModifier {
}, },
iconImage: viewModel.iconImage iconImage: viewModel.iconImage
) )
.background(Color.black.opacity(0.3))
.ignoresSafeArea()
} }
} }
.alert("确认重新下载", isPresented: $viewModel.showRedownloadConfirm) { .alert("确认重新下载", isPresented: $viewModel.showRedownloadConfirm) {
Button("取消", role: .cancel) { } Button("取消", role: .cancel) { }
Button("确认") { Button("确认") {
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
Task { Task {
await viewModel.checkAndStartDownload( await startRedownload()
version: viewModel.pendingVersion,
language: viewModel.pendingLanguage
)
}
} }
} }
} message: { } message: {
@@ -540,4 +613,34 @@ struct AlertModifier: ViewModifier {
Text(viewModel.errorMessage) 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)
}
}
} }

View File

@@ -120,11 +120,18 @@ struct DownloadProgressView: View {
if !ModifySetup.isSetupBackup() { if !ModifySetup.isSetupBackup() {
showSetupBackupAlert = true showSetupBackupAlert = true
} else { } else {
print("正在连接 Helper...")
if PrivilegedHelperManager.shared.connectToHelper() != nil {
print("Helper 连接成功,开始安装...")
showInstallPrompt = false showInstallPrompt = false
isInstalling = true isInstalling = true
Task { Task {
await networkManager.installProduct(at: task.directory) await networkManager.installProduct(at: task.directory)
} }
} else {
print("Helper 连接失败")
showSetupBackupAlert = true
}
} }
}) { }) {
Label("安装", systemImage: "square.and.arrow.down.on.square") Label("安装", systemImage: "square.and.arrow.down.on.square")
@@ -134,8 +141,13 @@ struct DownloadProgressView: View {
.alert("Setup 组件未处理", isPresented: $showSetupBackupAlert) { .alert("Setup 组件未处理", isPresented: $showSetupBackupAlert) {
Button("确定") { } Button("确定") { }
} message: { } message: {
if !ModifySetup.isSetupBackup() {
Text("未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理") Text("未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理")
.font(.system(size: 18)) .font(.system(size: 18))
} else {
Text("Helper 未安装或未连接,请先在设置中安装并连接 Helper")
.font(.system(size: 18))
}
} }
} }

View File

@@ -1,5 +1,4 @@
// //
// ShouldExistsSetUpView.swift
// Adobe Downloader // Adobe Downloader
// //
// Created by X1a0He. // Created by X1a0He.
@@ -7,6 +6,18 @@
import SwiftUI 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 { struct ExistingFileAlertView: View {
let path: URL let path: URL
let onUseExisting: () -> Void let onUseExisting: () -> Void
@@ -15,8 +26,40 @@ struct ExistingFileAlertView: View {
let iconImage: NSImage? let iconImage: NSImage?
var body: some View { var body: some View {
VStack(spacing: 20) { VStack(spacing: AlertConstants.verticalSpacing) {
IconSection(iconImage: iconImage)
Text("安装程序已存在")
.font(.headline)
PathSection(path: path)
ButtonSection(
onUseExisting: onUseExisting,
onRedownload: onRedownload,
onCancel: onCancel
)
}
.padding()
.background(BackgroundView())
}
}
private struct IconSection: View {
let iconImage: NSImage?
var body: some View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
AppIcon(iconImage: iconImage)
WarningIcon()
}
.padding(.bottom, 5)
}
}
private struct AppIcon: View {
let iconImage: NSImage?
var body: some View {
Group { Group {
if let iconImage = iconImage { if let iconImage = iconImage {
Image(nsImage: iconImage) Image(nsImage: iconImage)
@@ -30,66 +73,118 @@ struct ExistingFileAlertView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
.frame(width: 64, height: 64) .frame(width: AlertConstants.iconSize, height: AlertConstants.iconSize)
}
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 24))
.foregroundColor(.orange)
.offset(x: 10, y: 4)
} }
.padding(.bottom, 5)
Text("安装程序已存在") private struct WarningIcon: View {
.font(.headline) 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) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
Text(path.path) Text(path.path)
.foregroundColor(.blue) .foregroundColor(.blue)
.onTapGesture { .onTapGesture {
openInFinder(path)
}
}
}
}
private func openInFinder(_ path: URL) {
NSWorkspace.shared.activateFileViewerSelecting([path]) NSWorkspace.shared.activateFileViewerSelecting([path])
} }
} }
}
private struct ButtonSection: View {
let onUseExisting: () -> Void
let onRedownload: () -> Void
let onCancel: () -> Void
var body: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
Button(action: onUseExisting) { ActionButton(
Label("使用现有程序", systemImage: "checkmark.circle") title: "使用现有程序",
.frame(minWidth: 0,maxWidth: 260) icon: "checkmark.circle",
.frame(height: 32) color: .blue,
.font(.system(size: 14)) action: onUseExisting
} )
.buttonStyle(.borderedProminent)
.tint(.blue)
Button(action: onRedownload) { ActionButton(
Label("重新下载", systemImage: "arrow.down.circle") title: "重新下载",
.frame(minWidth: 0,maxWidth: 260) icon: "arrow.down.circle",
.frame(height: 32) color: .green,
.font(.system(size: 14)) action: onRedownload
} )
.buttonStyle(.borderedProminent)
.tint(.green)
Button(action: onCancel) { ActionButton(
Label("取消", systemImage: "xmark.circle") title: "取消",
.frame(minWidth: 0, maxWidth: 260) icon: "xmark.circle",
.frame(height: 32) color: .red,
.font(.system(size: 14)) action: onCancel,
isCancel: true
)
} }
.buttonStyle(.borderedProminent)
.tint(.red)
.keyboardShortcut(.cancelAction)
}
}
.padding()
.background(Color(NSColor.windowBackgroundColor))
.cornerRadius(12)
.shadow(radius: 10)
} }
} }
#Preview { 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`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct ExistingFileAlertView_Previews: PreviewProvider {
static var previews: some View {
Group {
ExistingFileAlertView( ExistingFileAlertView(
path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"),
onUseExisting: {}, onUseExisting: {},
@@ -98,9 +193,8 @@ struct ExistingFileAlertView: View {
iconImage: NSImage(named: "PHSP") iconImage: NSImage(named: "PHSP")
) )
.background(Color.black.opacity(0.3)) .background(Color.black.opacity(0.3))
} .previewDisplayName("Light Mode")
#Preview("Dark Mode") {
ExistingFileAlertView( ExistingFileAlertView(
path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"),
onUseExisting: {}, onUseExisting: {},
@@ -110,4 +204,7 @@ struct ExistingFileAlertView: View {
) )
.background(Color.black.opacity(0.3)) .background(Color.black.opacity(0.3))
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.previewDisplayName("Dark Mode")
}
}
} }

View File

@@ -18,7 +18,7 @@ struct InstallProgressView: View {
} }
private var isFailed: Bool { private var isFailed: Bool {
status.contains(String(localized: "失败")) status.contains(String(localized: "安装失败"))
} }
private var progressText: String { private var progressText: String {
@@ -77,7 +77,8 @@ struct InstallProgressView: View {
if isFailed { if isFailed {
ErrorSection( ErrorSection(
status: status, isFailed: isFailed status: status,
isFailed: true
) )
} }
@@ -119,7 +120,6 @@ private struct ErrorSection: View {
let isFailed: Bool let isFailed: Bool
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("错误详情:") Text("错误详情:")
.font(.caption) .font(.caption)

View File

@@ -118,7 +118,7 @@ private struct ButtonsView: View {
if isDownloading { if isDownloading {
downloadProgressView downloadProgressView
} else { } else {
Label("下载 X1a0He CC 组件", systemImage: "arrow.down") Label("下载 X1a0He CC", systemImage: "arrow.down")
.frame(minWidth: 0, maxWidth: 360) .frame(minWidth: 0, maxWidth: 360)
.frame(height: 32) .frame(height: 32)
.font(.system(size: 14)) .font(.system(size: 14))

View File

@@ -5,11 +5,26 @@
// //
import SwiftUI 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 { struct VersionPickerView: View {
@EnvironmentObject private var networkManager: NetworkManager @EnvironmentObject private var networkManager: NetworkManager
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN" @StorageValue(\.defaultLanguage) private var defaultLanguage
@AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true @StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
@State private var expandedVersions: Set<String> = [] @State private var expandedVersions: Set<String> = []
private let sap: Sap private let sap: Sap
@@ -22,6 +37,25 @@ struct VersionPickerView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HeaderView(sap: sap, downloadAppleSilicon: downloadAppleSilicon)
VersionListView(
sap: sap,
expandedVersions: $expandedVersions,
onSelect: onSelect,
dismiss: dismiss
)
}
.frame(width: VersionPickerConstants.viewWidth, height: VersionPickerConstants.viewHeight)
}
}
private struct HeaderView: View {
let sap: Sap
let downloadAppleSilicon: Bool
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var networkManager: NetworkManager
var body: some View {
VStack { VStack {
HStack { HStack {
Text("\(sap.displayName)") Text("\(sap.displayName)")
@@ -33,25 +67,60 @@ struct VersionPickerView: View {
dismiss() dismiss()
} }
} }
.padding(.bottom, 5) .padding(.bottom, VersionPickerConstants.headerPadding)
Text("🔔 即将下载 \(downloadAppleSilicon ? "Apple Silicon" : "Intel") (\(networkManager.allowedPlatform.joined(separator: ", "))) 版本 🔔")
Text("🔔 即将下载 \(downloadAppleSilicon ? "Apple Silicon" : "Intel") (\(platformText)) 版本 🔔")
.font(.caption) .font(.caption)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
.padding(.horizontal) .padding(.horizontal)
.padding(.top) .padding(.top)
.background(Color(NSColor.windowBackgroundColor)) .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<String>
let onSelect: (String) -> Void
let dismiss: DismissAction
var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
LazyVStack(spacing: 12) { LazyVStack(spacing: VersionPickerConstants.verticalSpacing) {
ForEach(Array(sap.versions.sorted { $0.key > $1.key }), id: \.key) { version, info in ForEach(filteredVersions, id: \.key) { version, info in
if networkManager.allowedPlatform.contains(info.apPlatform) { VersionRow(
VStack(spacing: 0) { sap: sap,
Button(action: { version: version,
if info.dependencies.isEmpty { 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) onSelect(version)
dismiss() dismiss()
} else { }
private func handleVersionToggle(_ version: String) {
withAnimation { withAnimation {
if expandedVersions.contains(version) { if expandedVersions.contains(version) {
expandedVersions.remove(version) expandedVersions.remove(version)
@@ -60,30 +129,106 @@ struct VersionPickerView: View {
} }
} }
} }
}) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(version)
.font(.headline)
Text(info.apPlatform)
.font(.caption)
.foregroundColor(.secondary)
} }
Spacer() private struct VersionRow: View {
@EnvironmentObject private var networkManager: NetworkManager
@StorageValue(\.defaultLanguage) private var defaultLanguage
if let existingPath = networkManager.isVersionDownloaded( 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, sap: sap,
version: version, version: version,
language: defaultLanguage language: defaultLanguage
) {
Button(action: {
let path = existingPath.path
NSWorkspace.shared.selectFile(
path,
inFileViewerRootedAtPath: URL(fileURLWithPath: path).deletingLastPathComponent().path
) )
}) { }
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("已存在") Text("已存在")
.font(.caption) .font(.caption)
.foregroundColor(.white) .foregroundColor(.white)
@@ -92,31 +237,53 @@ struct VersionPickerView: View {
.background(Color.blue) .background(Color.blue)
.cornerRadius(4) .cornerRadius(4)
} }
.buttonStyle(.plain) }
} }
if !info.dependencies.isEmpty { private struct ExpandButton: View {
Image(systemName: expandedVersions.contains(version) ? "chevron.down" : "chevron.right") let isExpanded: Bool
.foregroundColor(.secondary) let hasDependencies: Bool
} else {
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if expandedVersions.contains(version) { var body: some View {
VStack(alignment: .leading, spacing: 8) { 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("依赖包:") Text("依赖包:")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.top, 8) .padding(.top, 8)
.padding(.leading, 16) .padding(.leading, 16)
ForEach(info.dependencies, id: \.sapCode) { dependency in 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) { HStack(spacing: 8) {
Image(systemName: "cube.box") Image(systemName: "cube.box")
.foregroundColor(.blue) .foregroundColor(.blue)
@@ -127,38 +294,30 @@ struct VersionPickerView: View {
} }
.padding(.leading, 24) .padding(.leading, 24)
} }
}
}
private struct DownloadButton: View {
let version: String
let onSelect: (String) -> Void
var body: some View {
Button("下载此版本") { Button("下载此版本") {
onSelect(version) onSelect(version)
dismiss()
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.padding(.top, 8) .padding(.top, 8)
.padding(.leading, 16) .padding(.leading, 16)
} }
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 8)
}
}
.padding(.horizontal)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
}
.padding()
}
.background(Color(NSColor.windowBackgroundColor))
}
.frame(width: 400, height: 500)
}
} }
#Preview { struct VersionPickerView_Previews: PreviewProvider {
static var previews: some View {
let networkManager = NetworkManager() let networkManager = NetworkManager()
networkManager.allowedPlatform = ["macuniversal", "macarm64"]
networkManager.cdn = "https://example.cdn.adobe.com"
return VersionPickerView( let previewSap = Sap(
sap: Sap(
hidden: false, hidden: false,
displayName: "Photoshop", displayName: "Photoshop",
sapCode: "PHSP", sapCode: "PHSP",
@@ -195,14 +354,11 @@ struct VersionPickerView: View {
buildGuid: "q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6" buildGuid: "q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6"
) )
], ],
icons: [ icons: []
Sap.ProductIcon(
size: "192x192",
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/26.0.0/192x192.png"
)
]
),
onSelect: { version in }
) )
return VersionPickerView(sap: previewSap) { _ in }
.environmentObject(networkManager) .environmentObject(networkManager)
.previewDisplayName("Version Picker")
}
} }

View File

@@ -4,17 +4,11 @@
<dict> <dict>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string> <string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>1.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string> <string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>100</string>
<key>SMAuthorizedClients</key> <key>SMAuthorizedClients</key>
<array> <array>
<string>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 */</string> <string>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 */</string>
</array> </array>
<key>MachServices</key> <key>MachServices</key>
<dict> <dict>

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,30 @@
# Change Log # Change Log
## 2024-11-13 00:00 更新日志 ## 2024-11-14 15:30 更新日志
```markdown ```markdown
1. 新增可选API版本 (v4, v5, v6) 1. 新增可选API版本 (v4, v5, v6)【更老的API意味着更长的等待时间】
2. 引入 Privilege Helper 来处理所有需要权限的操作 2. 引入 Privilege Helper 来处理所有需要权限的操作
3. 修改从 Github 下载 Setup 组件功能改为从官方下载简化版CC称为 X1a0He CC 3. 修改从 Github 下载 Setup 组件功能改为从官方下载简化版CC称为 X1a0He CC
4. 调整 CC 组件备份与处理状态检测,分离二者的检测机制 4. 调整 CC 组件备份与处理状态检测,分离二者的检测机制
5. 移除了安装日志显示 5. 移除了安装日志显示
6. 调整 Setup 组件版本号的获取方式 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 更新日志 ## 2024-11-11 21:00 更新日志