mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 11:18:53 +08:00
perf: Save works
This commit is contained in:
@@ -230,6 +230,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = TG862GVKHK;
|
||||
@@ -237,7 +238,7 @@
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = NO;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"-sectcreate",
|
||||
@@ -259,6 +260,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = TG862GVKHK;
|
||||
@@ -266,7 +268,7 @@
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = NO;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-sectcreate",
|
||||
__TEXT,
|
||||
@@ -413,6 +415,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 120;
|
||||
@@ -430,7 +433,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.2;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -445,6 +448,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 120;
|
||||
@@ -462,7 +466,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.2;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "463"
|
||||
endingLineNumber = "463"
|
||||
startingLineNumber = "450"
|
||||
endingLineNumber = "450"
|
||||
landmarkName = "startDownloadProcess(task:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
@@ -23,16 +23,48 @@
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<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"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Adobe Downloader/Utils/InstallManager.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "128"
|
||||
endingLineNumber = "128"
|
||||
landmarkName = "retry(at:progressHandler:)"
|
||||
startingLineNumber = "81"
|
||||
endingLineNumber = "81"
|
||||
landmarkName = "executeInstallation(at:progressHandler:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
|
||||
@@ -9,51 +9,42 @@ struct Adobe_DownloaderApp: App {
|
||||
@State private var showTipsSheet = false
|
||||
@State private var showLanguagePicker = false
|
||||
@State private var showCreativeCloudAlert = false
|
||||
@AppStorage("useDefaultLanguage") private var useDefaultLanguage: Bool = true
|
||||
@AppStorage("defaultLanguage") private var defaultLanguage: String = "ALL"
|
||||
@AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true
|
||||
@AppStorage("confirmRedownload") private var confirmRedownload: Bool = true
|
||||
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true
|
||||
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
|
||||
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
||||
@StorageValue(\.confirmRedownload) private var confirmRedownload
|
||||
@StorageValue(\.useDefaultDirectory) private var useDefaultDirectory
|
||||
@StorageValue(\.defaultDirectory) private var defaultDirectory
|
||||
@State private var showBackupResultAlert = false
|
||||
@State private var backupResultMessage = ""
|
||||
@State private var backupSuccess = false
|
||||
private let updaterController: SPUStandardUpdaterController
|
||||
|
||||
init() {
|
||||
updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
|
||||
|
||||
let isFirstRun = UserDefaults.standard.object(forKey: "downloadAppleSilicon") == nil ||
|
||||
UserDefaults.standard.object(forKey: "useDefaultLanguage") == nil
|
||||
|
||||
UserDefaults.standard.set(isFirstRun, forKey: "isFirstLaunch")
|
||||
|
||||
if UserDefaults.standard.object(forKey: "downloadAppleSilicon") == nil {
|
||||
UserDefaults.standard.set(AppStatics.isAppleSilicon, forKey: "downloadAppleSilicon")
|
||||
if StorageData.shared.installedHelperBuild == "0" {
|
||||
StorageData.shared.installedHelperBuild = "0"
|
||||
}
|
||||
|
||||
if UserDefaults.standard.object(forKey: "useDefaultLanguage") == nil {
|
||||
updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
|
||||
|
||||
if StorageData.shared.isFirstLaunch {
|
||||
StorageData.shared.downloadAppleSilicon = AppStatics.isAppleSilicon
|
||||
|
||||
let systemLanguage = Locale.current.identifier
|
||||
let matchedLanguage = AppStatics.supportedLanguages.first {
|
||||
systemLanguage.hasPrefix($0.code.prefix(2))
|
||||
}?.code ?? "ALL"
|
||||
StorageData.shared.defaultLanguage = matchedLanguage
|
||||
StorageData.shared.useDefaultLanguage = true
|
||||
|
||||
UserDefaults.standard.set(true, forKey: "useDefaultLanguage")
|
||||
UserDefaults.standard.set(matchedLanguage, forKey: "defaultLanguage")
|
||||
}
|
||||
|
||||
if UserDefaults.standard.object(forKey: "useDefaultDirectory") == nil {
|
||||
if let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first {
|
||||
print(downloadsURL.path)
|
||||
defaultDirectory = downloadsURL.path
|
||||
UserDefaults.standard.set(true, forKey: "useDefaultDirectory")
|
||||
UserDefaults.standard.set(downloadsURL.path, forKey: "defaultDirectory")
|
||||
StorageData.shared.defaultDirectory = downloadsURL.path
|
||||
StorageData.shared.useDefaultDirectory = true
|
||||
}
|
||||
}
|
||||
PrivilegedHelperManager.shared.checkInstall()
|
||||
|
||||
if UserDefaults.standard.string(forKey: "apiVersion") == nil {
|
||||
UserDefaults.standard.set("6", forKey: "apiVersion")
|
||||
if StorageData.shared.apiVersion == "6" {
|
||||
StorageData.shared.apiVersion = "6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +55,8 @@ struct Adobe_DownloaderApp: App {
|
||||
.frame(width: 850, height: 800)
|
||||
.tint(.blue)
|
||||
.task {
|
||||
PrivilegedHelperManager.shared.checkInstall()
|
||||
|
||||
await MainActor.run {
|
||||
appDelegate.networkManager = networkManager
|
||||
networkManager.loadSavedTasks()
|
||||
@@ -79,9 +72,9 @@ struct Adobe_DownloaderApp: App {
|
||||
showBackupAlert = true
|
||||
}
|
||||
|
||||
if UserDefaults.standard.bool(forKey: "isFirstLaunch") {
|
||||
if StorageData.shared.isFirstLaunch {
|
||||
showTipsSheet = true
|
||||
UserDefaults.standard.removeObject(forKey: "isFirstLaunch")
|
||||
StorageData.shared.isFirstLaunch = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +106,13 @@ struct Adobe_DownloaderApp: App {
|
||||
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
Toggle("使用默认语言", isOn: $useDefaultLanguage)
|
||||
Toggle("使用默认语言", isOn: Binding(
|
||||
get: { useDefaultLanguage },
|
||||
set: {
|
||||
useDefaultLanguage = $0
|
||||
StorageData.shared.useDefaultLanguage = $0
|
||||
}
|
||||
))
|
||||
.padding(.leading, 5)
|
||||
Spacer()
|
||||
Text(getLanguageName(code: defaultLanguage))
|
||||
@@ -127,7 +126,13 @@ struct Adobe_DownloaderApp: App {
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Toggle("使用默认目录", isOn: $useDefaultDirectory)
|
||||
Toggle("使用默认目录", isOn: Binding(
|
||||
get: { useDefaultDirectory },
|
||||
set: {
|
||||
useDefaultDirectory = $0
|
||||
StorageData.shared.useDefaultDirectory = $0
|
||||
}
|
||||
))
|
||||
.padding(.leading, 5)
|
||||
Spacer()
|
||||
Text(formatPath(defaultDirectory))
|
||||
@@ -143,7 +148,14 @@ struct Adobe_DownloaderApp: App {
|
||||
Divider()
|
||||
|
||||
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)
|
||||
Spacer()
|
||||
}
|
||||
@@ -151,7 +163,14 @@ struct Adobe_DownloaderApp: App {
|
||||
Divider()
|
||||
|
||||
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)
|
||||
Spacer()
|
||||
Text("当前架构: \(AppStatics.cpuArchitecture)")
|
||||
@@ -159,9 +178,6 @@ struct Adobe_DownloaderApp: App {
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
.onChange(of: downloadAppleSilicon) { newValue in
|
||||
networkManager.updateAllowedPlatform(useAppleSilicon: newValue)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
@@ -180,6 +196,7 @@ struct Adobe_DownloaderApp: App {
|
||||
.sheet(isPresented: $showLanguagePicker) {
|
||||
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
||||
defaultLanguage = language
|
||||
StorageData.shared.defaultLanguage = language
|
||||
showLanguagePicker = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,28 +15,11 @@ enum PackageStatus: Equatable, Codable {
|
||||
|
||||
var description: LocalizedStringKey {
|
||||
switch self {
|
||||
case .waiting: return "等待中"
|
||||
case .downloading: return "下载中"
|
||||
case .paused: return "已暂停"
|
||||
case .completed: return "已完成"
|
||||
case .failed(let message): return "失败: \(message)"
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: PackageStatus, rhs: PackageStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.waiting, .waiting):
|
||||
return true
|
||||
case (.downloading, .downloading):
|
||||
return true
|
||||
case (.paused, .paused):
|
||||
return true
|
||||
case (.completed, .completed):
|
||||
return true
|
||||
case (.failed(let lhsMessage), .failed(let rhsMessage)):
|
||||
return lhsMessage == rhsMessage
|
||||
default:
|
||||
return false
|
||||
case .waiting: return LocalizedStringKey("等待中...")
|
||||
case .downloading: return LocalizedStringKey("下载中...")
|
||||
case .paused: return LocalizedStringKey("已暂停")
|
||||
case .completed: return LocalizedStringKey("已完成")
|
||||
case .failed(let message): return LocalizedStringKey("下载失败: \(message)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +44,7 @@ enum NetworkError: Error, LocalizedError {
|
||||
case downloadError(String, Error?)
|
||||
case downloadCancelled
|
||||
case insufficientStorage(Int64, Int64)
|
||||
case cancelled
|
||||
|
||||
case fileSystemError(String, Error?)
|
||||
case fileExists(String)
|
||||
@@ -70,97 +54,112 @@ enum NetworkError: Error, LocalizedError {
|
||||
case applicationInfoError(String, Error?)
|
||||
case unsupportedPlatform(String)
|
||||
case incompatibleVersion(String, String)
|
||||
case cancelled
|
||||
case installError(String)
|
||||
|
||||
var errorCode: Int {
|
||||
private var errorGroup: Int {
|
||||
switch self {
|
||||
case .noConnection: return 1001
|
||||
case .timeout: return 1002
|
||||
case .serverUnreachable: return 1003
|
||||
case .invalidURL: return 2001
|
||||
case .invalidRequest: return 2002
|
||||
case .invalidResponse: return 2003
|
||||
case .invalidData: return 3001
|
||||
case .parsingError: return 3002
|
||||
case .dataValidationError: return 3003
|
||||
case .httpError: return 4001
|
||||
case .serverError: return 4002
|
||||
case .clientError: return 4003
|
||||
case .downloadError: return 5001
|
||||
case .downloadCancelled: return 5002
|
||||
case .insufficientStorage: return 5003
|
||||
case .fileSystemError: return 6001
|
||||
case .fileExists: return 6002
|
||||
case .fileNotFound: return 6003
|
||||
case .filePermissionDenied: return 6004
|
||||
case .applicationInfoError: return 7001
|
||||
case .unsupportedPlatform: return 7002
|
||||
case .incompatibleVersion: return 7003
|
||||
case .cancelled: return 5004
|
||||
case .installError: return 8001
|
||||
case .noConnection, .timeout, .serverUnreachable: return 1000
|
||||
case .invalidURL, .invalidRequest, .invalidResponse: return 2000
|
||||
case .invalidData, .parsingError, .dataValidationError: return 3000
|
||||
case .httpError, .serverError, .clientError: return 4000
|
||||
case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000
|
||||
case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000
|
||||
case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError: return 7000
|
||||
}
|
||||
}
|
||||
|
||||
private var errorOffset: Int {
|
||||
switch self {
|
||||
case .noConnection: return 1
|
||||
case .timeout: return 2
|
||||
case .serverUnreachable: return 3
|
||||
case .invalidURL: return 1
|
||||
case .invalidRequest: return 2
|
||||
case .invalidResponse: return 3
|
||||
case .invalidData: return 1
|
||||
case .parsingError: return 2
|
||||
case .dataValidationError: return 3
|
||||
case .httpError: return 1
|
||||
case .serverError: return 2
|
||||
case .clientError: return 3
|
||||
case .downloadError: return 1
|
||||
case .downloadCancelled: return 2
|
||||
case .insufficientStorage: return 3
|
||||
case .cancelled: return 4
|
||||
case .fileSystemError: return 1
|
||||
case .fileExists: return 2
|
||||
case .fileNotFound: return 3
|
||||
case .filePermissionDenied: return 4
|
||||
case .applicationInfoError: return 1
|
||||
case .unsupportedPlatform: return 2
|
||||
case .incompatibleVersion: return 3
|
||||
case .installError: return 4
|
||||
}
|
||||
}
|
||||
|
||||
var errorCode: Int {
|
||||
return errorGroup + errorOffset
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noConnection:
|
||||
return NSLocalizedString("没有网络连接", comment: "Network error")
|
||||
return NSLocalizedString("网络无连接", value: "Network error", comment: "Network error")
|
||||
case .timeout:
|
||||
return NSLocalizedString("请求超时,请检查网络连接后重试", comment: "Network timeout")
|
||||
return NSLocalizedString("请求超时,请检查网络连接后重试", value: "请求超时,请检查网络连接后重试", comment: "Network timeout")
|
||||
case .serverUnreachable(let server):
|
||||
return NSLocalizedString("无法连接到服务器: \(server)", comment: "Server unreachable")
|
||||
return String(format: NSLocalizedString("无法连接到服务器: %@", value: "无法连接到服务器: %@",comment: "Server unreachable"), server)
|
||||
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):
|
||||
return NSLocalizedString("无效的请求: \(reason)", comment: "Invalid request")
|
||||
return String(format: NSLocalizedString("无效的请求: %@", value: "无效的请求: %@", comment: "Invalid request"), reason)
|
||||
case .invalidResponse:
|
||||
return NSLocalizedString("服务器响应无效", comment: "Invalid response")
|
||||
return NSLocalizedString("服务器响应无效", value: "服务器响应无效", comment: "Invalid response")
|
||||
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):
|
||||
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):
|
||||
return NSLocalizedString("数据验证失败: \(reason)", comment: "Data validation error")
|
||||
return String(format: NSLocalizedString("数据验证失败: %@", value: "数据验证失败: %@", comment: "Data validation error"), reason)
|
||||
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):
|
||||
return NSLocalizedString("服务器错误: \(code)", comment: "Server error")
|
||||
return String(format: NSLocalizedString("服务器错误: %d", value: "服务器错误: %d", comment: "Server error"), 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):
|
||||
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:
|
||||
return NSLocalizedString("下载已取消", comment: "Download cancelled")
|
||||
return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
|
||||
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):
|
||||
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):
|
||||
return NSLocalizedString("文件已存在: \(path)", comment: "File exists")
|
||||
return String(format: NSLocalizedString("文件已存在: %@", value: "文件已存在: %@", comment: "File exists"), 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):
|
||||
return NSLocalizedString("文件访问权限被拒绝: \(path)", comment: "File permission denied")
|
||||
return String(format: NSLocalizedString("文件访问权限被拒绝: %@", value: "文件访问权限被拒绝: %@", comment: "File permission denied"), path)
|
||||
case .applicationInfoError(let message, let 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):
|
||||
return NSLocalizedString("不支持的平台: \(platform)", comment: "Unsupported platform")
|
||||
return String(format: NSLocalizedString("不支持的平台: %@", value: "不支持的平台: %@", comment: "Unsupported platform"), platform)
|
||||
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:
|
||||
return NSLocalizedString("下载已取消", comment: "Download cancelled")
|
||||
return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
|
||||
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 {
|
||||
switch self {
|
||||
case .waiting:
|
||||
return NSLocalizedString("等待中", comment: "Download status waiting")
|
||||
return NSLocalizedString("等待中", value: "等待中", comment: "Download status waiting")
|
||||
case .preparing(let info):
|
||||
return NSLocalizedString("准备中: \(info.message)", comment: "Download status preparing")
|
||||
return String(format: NSLocalizedString("准备中: %@", value: "准备中: %@", comment: "Download status preparing"), info.message)
|
||||
case .downloading(let info):
|
||||
return String(format: NSLocalizedString("正在下载 %@ (%d/%d)", comment: "Download status downloading"),
|
||||
return String(format: NSLocalizedString("正在下载 %@ (%d/%d)", value: "正在下载 %@ (%d/%d)", comment: "Download status downloading"),
|
||||
info.fileName, info.currentPackageIndex + 1, info.totalPackages)
|
||||
case .paused(let info):
|
||||
switch info.reason {
|
||||
case .userRequested:
|
||||
return NSLocalizedString("已暂停", comment: "Download status paused")
|
||||
return NSLocalizedString("已暂停", value: "已暂停", comment: "Download status paused")
|
||||
case .networkIssue:
|
||||
return NSLocalizedString("网络中断", comment: "Download status network paused")
|
||||
return NSLocalizedString("网络中断", value: "网络中断", comment: "Download status network paused")
|
||||
case .systemSleep:
|
||||
return NSLocalizedString("系统休眠", comment: "Download status system sleep")
|
||||
return NSLocalizedString("系统休眠", value: "系统休眠", comment: "Download status system sleep")
|
||||
case .other(let reason):
|
||||
return NSLocalizedString("已暂停: \(reason)", comment: "Download status paused with reason")
|
||||
return String(format: NSLocalizedString("已暂停: %@", value: "已暂停: %@", comment: "Download status paused with reason"), reason)
|
||||
}
|
||||
case .completed(let info):
|
||||
let duration = formatDuration(info.totalTime)
|
||||
return NSLocalizedString("已完成 (用时: \(duration))", comment: "Download status completed")
|
||||
return String(format: NSLocalizedString("已完成 (用时: %@)", value: "已完成 (用时: %@)", comment: "Download status completed"),
|
||||
info.totalTime.formatDuration())
|
||||
case .failed(let info):
|
||||
return NSLocalizedString("失败: \(info.message)", comment: "Download status failed")
|
||||
return String(format: NSLocalizedString("失败: %@", value: "失败: %@", comment: "Download status failed"),
|
||||
info.message)
|
||||
case .retrying(let info):
|
||||
return String(format: NSLocalizedString("重试中 (%d/%d)", comment: "Download status retrying"),
|
||||
return String(format: NSLocalizedString("重试中 (%d/%d)", value: "重试中 (%d/%d)", comment: "Download status retrying"),
|
||||
info.attempt, info.maxAttempts)
|
||||
}
|
||||
}
|
||||
@@ -393,12 +393,10 @@ enum DownloadStatus: Equatable, Codable {
|
||||
}
|
||||
|
||||
var canRetry: Bool {
|
||||
switch self {
|
||||
case .failed(let info):
|
||||
if case .failed(let info) = self {
|
||||
return info.recoverable
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var canPause: Bool {
|
||||
@@ -411,92 +409,26 @@ enum DownloadStatus: Equatable, Codable {
|
||||
}
|
||||
|
||||
var canResume: Bool {
|
||||
switch self {
|
||||
case .paused(let info):
|
||||
if case .paused(let info) = self {
|
||||
return info.resumable
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus {
|
||||
static func == (lhs: DownloadStatus, rhs: DownloadStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.waiting, .waiting):
|
||||
return true
|
||||
case (.preparing(let lInfo), .preparing(let rInfo)):
|
||||
return lInfo.message == rInfo.message &&
|
||||
lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.stage == rInfo.stage
|
||||
case (.downloading(let lInfo), .downloading(let rInfo)):
|
||||
return lInfo.fileName == rInfo.fileName &&
|
||||
lInfo.currentPackageIndex == rInfo.currentPackageIndex &&
|
||||
lInfo.totalPackages == rInfo.totalPackages
|
||||
case (.paused(let lInfo), .paused(let rInfo)):
|
||||
return lInfo.reason == rInfo.reason &&
|
||||
lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.resumable == rInfo.resumable
|
||||
case (.completed(let lInfo), .completed(let rInfo)):
|
||||
return lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.totalTime == rInfo.totalTime &&
|
||||
lInfo.totalSize == rInfo.totalSize
|
||||
case (.failed(let lInfo), .failed(let rInfo)):
|
||||
return lInfo.message == rInfo.message &&
|
||||
lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.recoverable == rInfo.recoverable
|
||||
case (.retrying(let lInfo), .retrying(let rInfo)):
|
||||
return lInfo.attempt == rInfo.attempt &&
|
||||
lInfo.maxAttempts == rInfo.maxAttempts &&
|
||||
lInfo.reason == rInfo.reason &&
|
||||
lInfo.nextRetryDate == rInfo.nextRetryDate
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
extension DownloadStatus.PrepareInfo: Equatable {}
|
||||
extension DownloadStatus.PrepareInfo.PrepareStage: Equatable {}
|
||||
extension DownloadStatus.PauseInfo.PauseReason: Equatable {}
|
||||
extension DownloadStatus.DownloadInfo: Equatable {}
|
||||
extension DownloadStatus.PauseInfo: Equatable {}
|
||||
extension DownloadStatus.CompletionInfo: Equatable {}
|
||||
extension DownloadStatus.RetryInfo: Equatable {}
|
||||
|
||||
extension DownloadStatus.PrepareInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.PrepareInfo, rhs: DownloadStatus.PrepareInfo) -> Bool {
|
||||
extension DownloadStatus.FailureInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.FailureInfo, rhs: DownloadStatus.FailureInfo) -> Bool {
|
||||
return lhs.message == rhs.message &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.stage == rhs.stage
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PrepareInfo.PrepareStage: Equatable {
|
||||
static func == (lhs: DownloadStatus.PrepareInfo.PrepareStage, rhs: DownloadStatus.PrepareInfo.PrepareStage) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.initializing, .initializing):
|
||||
return true
|
||||
case (.creatingInstaller, .creatingInstaller):
|
||||
return true
|
||||
case (.signingApp, .signingApp):
|
||||
return true
|
||||
case (.fetchingInfo, .fetchingInfo):
|
||||
return true
|
||||
case (.validatingSetup, .validatingSetup):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PauseInfo.PauseReason: Equatable {
|
||||
static func == (lhs: DownloadStatus.PauseInfo.PauseReason, rhs: DownloadStatus.PauseInfo.PauseReason) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.userRequested, .userRequested):
|
||||
return true
|
||||
case (.networkIssue, .networkIssue):
|
||||
return true
|
||||
case (.systemSleep, .systemSleep):
|
||||
return true
|
||||
case (.other(let lhsReason), .other(let rhsReason)):
|
||||
return lhsReason == rhsReason
|
||||
default:
|
||||
return false
|
||||
}
|
||||
lhs.recoverable == rhs.recoverable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,13 +440,9 @@ enum LoadingState: Equatable {
|
||||
|
||||
static func == (lhs: LoadingState, rhs: LoadingState) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.idle, .idle):
|
||||
case (.idle, .idle), (.loading, .loading), (.success, .success):
|
||||
return true
|
||||
case (.loading, .loading):
|
||||
return true
|
||||
case (.success, .success):
|
||||
return true
|
||||
case (.failed(let lError), .failed(let rError)):
|
||||
case let (.failed(lError), .failed(rError)):
|
||||
return lError.localizedDescription == rError.localizedDescription
|
||||
default:
|
||||
return false
|
||||
@@ -522,60 +450,19 @@ enum LoadingState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDuration(_ seconds: TimeInterval) -> String {
|
||||
if seconds < 60 {
|
||||
return String(format: "%.1f秒", seconds)
|
||||
} else if seconds < 3600 {
|
||||
let minutes = Int(seconds / 60)
|
||||
let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60))
|
||||
private extension TimeInterval {
|
||||
func formatDuration() -> String {
|
||||
if self < 60 {
|
||||
return String(format: "%.1f秒", self)
|
||||
} else if self < 3600 {
|
||||
let minutes = Int(self / 60)
|
||||
let remainingSeconds = Int(self.truncatingRemainder(dividingBy: 60))
|
||||
return "\(minutes)分\(remainingSeconds)秒"
|
||||
} else {
|
||||
let hours = Int(seconds / 3600)
|
||||
let minutes = Int((seconds.truncatingRemainder(dividingBy: 3600)) / 60)
|
||||
let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60))
|
||||
let hours = Int(self / 3600)
|
||||
let minutes = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
|
||||
let remainingSeconds = Int(self.truncatingRemainder(dividingBy: 60))
|
||||
return "\(hours)小时\(minutes)分\(remainingSeconds)秒"
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.DownloadInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.DownloadInfo, rhs: DownloadStatus.DownloadInfo) -> Bool {
|
||||
return lhs.fileName == rhs.fileName &&
|
||||
lhs.currentPackageIndex == rhs.currentPackageIndex &&
|
||||
lhs.totalPackages == rhs.totalPackages &&
|
||||
lhs.startTime == rhs.startTime &&
|
||||
lhs.estimatedTimeRemaining == rhs.estimatedTimeRemaining
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PauseInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.PauseInfo, rhs: DownloadStatus.PauseInfo) -> Bool {
|
||||
return lhs.reason == rhs.reason &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.resumable == rhs.resumable
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.CompletionInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.CompletionInfo, rhs: DownloadStatus.CompletionInfo) -> Bool {
|
||||
return lhs.timestamp == rhs.timestamp &&
|
||||
lhs.totalTime == rhs.totalTime &&
|
||||
lhs.totalSize == rhs.totalSize
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.FailureInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.FailureInfo, rhs: DownloadStatus.FailureInfo) -> Bool {
|
||||
return lhs.message == rhs.message &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.recoverable == rhs.recoverable
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.RetryInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.RetryInfo, rhs: DownloadStatus.RetryInfo) -> Bool {
|
||||
return lhs.attempt == rhs.attempt &&
|
||||
lhs.maxAttempts == rhs.maxAttempts &&
|
||||
lhs.reason == rhs.reason &&
|
||||
lhs.nextRetryDate == rhs.nextRetryDate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,13 @@ import AppKit
|
||||
extension NewDownloadTask {
|
||||
var startTime: Date {
|
||||
switch totalStatus {
|
||||
case .downloading(let info):
|
||||
return info.startTime
|
||||
case .completed(let info):
|
||||
return info.timestamp.addingTimeInterval(-info.totalTime)
|
||||
case .preparing(let info):
|
||||
return info.timestamp
|
||||
case .paused(let info):
|
||||
return info.timestamp
|
||||
case .failed(let info):
|
||||
return info.timestamp
|
||||
case .retrying(let info):
|
||||
return info.nextRetryDate.addingTimeInterval(-60)
|
||||
case .waiting, .none:
|
||||
return createAt
|
||||
case .downloading(let info): return info.startTime
|
||||
case .completed(let info): return info.timestamp - info.totalTime
|
||||
case .preparing(let info): return info.timestamp
|
||||
case .paused(let info): return info.timestamp
|
||||
case .failed(let info): return info.timestamp
|
||||
case .retrying(let info): return info.nextRetryDate - 60
|
||||
case .waiting, .none: return createAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,36 +23,40 @@ extension NewDownloadTask {
|
||||
extension NetworkManager {
|
||||
func configureNetworkMonitor() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
let wasConnected = self.isConnected
|
||||
self.isConnected = path.status == .satisfied
|
||||
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 {
|
||||
for task in self.downloadTasks {
|
||||
private func resumePausedTasks() async {
|
||||
for task in downloadTasks {
|
||||
if case .paused(let info) = task.status,
|
||||
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 {
|
||||
await self.downloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: .networkIssue
|
||||
)
|
||||
await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
monitor.start(queue: DispatchQueue.global(qos: .utility))
|
||||
}
|
||||
|
||||
func generateCookie() -> String {
|
||||
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
let randomString = String((0..<26).map { _ in letters.randomElement()! })
|
||||
return "fg=\(randomString)======"
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
let randomString = (0..<26).map { _ in chars.randomElement()! }
|
||||
return "fg=\(String(randomString))======"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,15 @@ struct ContentView: View {
|
||||
@State private var errorMessage: String?
|
||||
@State private var showDownloadManager = false
|
||||
@State private var searchText = ""
|
||||
@AppStorage("apiVersion") private var apiVersion: String = "6"
|
||||
@State private var currentApiVersion = StorageData.shared.apiVersion
|
||||
|
||||
private var apiVersion: String {
|
||||
get { StorageData.shared.apiVersion }
|
||||
set {
|
||||
StorageData.shared.apiVersion = newValue
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
private var filteredProducts: [Sap] {
|
||||
let products = networkManager.saps.values
|
||||
@@ -37,17 +45,36 @@ struct ContentView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Toggle(isOn: Binding(
|
||||
get: { StorageData.shared.downloadAppleSilicon },
|
||||
set: { newValue in
|
||||
StorageData.shared.downloadAppleSilicon = newValue
|
||||
networkManager.updateAllowedPlatform(useAppleSilicon: newValue)
|
||||
Task {
|
||||
await networkManager.fetchProducts()
|
||||
}
|
||||
}
|
||||
)) {
|
||||
Text("Apple Silicon")
|
||||
}
|
||||
.toggleStyle(.switch)
|
||||
.tint(.green)
|
||||
.disabled(isRefreshing)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
HStack {
|
||||
Text("API:")
|
||||
.foregroundColor(.secondary)
|
||||
Picker("", selection: $apiVersion) {
|
||||
Picker("", selection: $currentApiVersion) {
|
||||
Text("v4").tag("4")
|
||||
Text("v5").tag("5")
|
||||
Text("v6").tag("6")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 150)
|
||||
.onChange(of: apiVersion) { newValue in
|
||||
.onChange(of: currentApiVersion) { newValue in
|
||||
StorageData.shared.apiVersion = newValue
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,24 +31,54 @@ class PrivilegedHelperManager: NSObject {
|
||||
private var useLegacyInstall = false
|
||||
private var connection: NSXPCConnection?
|
||||
|
||||
@Published private(set) var connectionState: ConnectionState = .disconnected
|
||||
@Published private(set) var connectionState: ConnectionState = .disconnected {
|
||||
didSet {
|
||||
if oldValue != connectionState {
|
||||
print("Helper 连接状态: \(connectionState.description)")
|
||||
if connectionState == .disconnected {
|
||||
connection?.invalidate()
|
||||
connection = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnectionState {
|
||||
case connected
|
||||
case disconnected
|
||||
case connecting
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .connected:
|
||||
return String(localized: "已连接")
|
||||
case .disconnected:
|
||||
return String(localized: "未连接")
|
||||
case .connecting:
|
||||
return String(localized: "正在连接")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isInitializing = false
|
||||
|
||||
private let connectionQueue = DispatchQueue(label: "com.x1a0he.helper.connection")
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
initAuthorizationRef()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
_ = self?.connectToHelper()
|
||||
}
|
||||
setupAutoReconnect()
|
||||
}
|
||||
|
||||
func checkInstall() {
|
||||
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
|
||||
let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") {
|
||||
if currentBuild != installedBuild {
|
||||
notifyInstall()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
getHelperStatus { [weak self] status in
|
||||
guard let self = self else { return }
|
||||
switch status {
|
||||
@@ -58,8 +88,8 @@ class PrivilegedHelperManager: NSObject {
|
||||
let status = SMAppService.statusForLegacyPlist(at: url)
|
||||
if status == .requiresApproval {
|
||||
let alert = NSAlert()
|
||||
let notice = "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件,请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App"
|
||||
let addition = "如果在设置里没找到当前App,可以尝试重置守护程序"
|
||||
let notice = String(localized: "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件,请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App")
|
||||
let addition = String(localized: "如果在设置里没找到当前App,可以尝试重置守护程序")
|
||||
alert.messageText = notice + "\n" + addition
|
||||
alert.addButton(withTitle: "打开系统登录项设置")
|
||||
alert.addButton(withTitle: "重置守护程序")
|
||||
@@ -124,8 +154,12 @@ class PrivilegedHelperManager: NSObject {
|
||||
NSAlert.alert(with: "SMJobBless failed with error: \(blessError)\nError domain: \(nsError.domain)\nError code: \(nsError.code)\nError description: \(nsError.localizedDescription)\nError user info: \(nsError.userInfo)")
|
||||
return .blessError(nsError.code)
|
||||
}
|
||||
return .blessError(-1)
|
||||
}
|
||||
|
||||
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild")
|
||||
}
|
||||
return .success
|
||||
}
|
||||
|
||||
@@ -140,19 +174,25 @@ class PrivilegedHelperManager: NSObject {
|
||||
|
||||
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName)
|
||||
guard
|
||||
let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any],
|
||||
let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String else {
|
||||
CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else {
|
||||
reply(.noFound)
|
||||
return
|
||||
}
|
||||
|
||||
let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)")
|
||||
if !helperFileExists {
|
||||
reply(.noFound)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -163,7 +203,6 @@ class PrivilegedHelperManager: NSObject {
|
||||
status = helperStatus == .installed
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
semaphore.wait()
|
||||
return status
|
||||
}
|
||||
@@ -178,30 +217,30 @@ class PrivilegedHelperManager: NSObject {
|
||||
guard let self = self else { return }
|
||||
|
||||
guard let connection = connectToHelper() else {
|
||||
completion(false, "无法连接到Helper")
|
||||
completion(false, String(localized: "无法连接到Helper"))
|
||||
return
|
||||
}
|
||||
|
||||
guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else {
|
||||
completion(false, "无法获取Helper代理")
|
||||
completion(false, String(localized: "无法获取Helper代理"))
|
||||
return
|
||||
}
|
||||
|
||||
helper.executeCommand("whoami") { result in
|
||||
if result.contains("root") {
|
||||
completion(true, "Helper 重新安装成功")
|
||||
completion(true, String(localized: "Helper 重新安装成功"))
|
||||
} else {
|
||||
completion(false, "Helper未能获取root权限")
|
||||
completion(false, String(localized: "Helper 安装失败"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .authorizationFail:
|
||||
completion(false, "获取授权失败")
|
||||
completion(false, String(localized: "获取授权失败"))
|
||||
case .getAdminFail:
|
||||
completion(false, "获取管理员权限失败")
|
||||
completion(false, String(localized: "获取管理员权限失败"))
|
||||
case .blessError(_):
|
||||
completion(false, "安装失败: \(result.alertContent)")
|
||||
completion(false, String(localized: "安装失败: \(result.alertContent)"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,18 +249,10 @@ class PrivilegedHelperManager: NSObject {
|
||||
try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist")
|
||||
}
|
||||
|
||||
private func connectToHelper() -> NSXPCConnection? {
|
||||
func connectToHelper() -> NSXPCConnection? {
|
||||
connectionState = .connecting
|
||||
|
||||
objc_sync_enter(self)
|
||||
defer { objc_sync_exit(self) }
|
||||
|
||||
if let existingConnection = connection,
|
||||
existingConnection.remoteObjectProxy != nil {
|
||||
connectionState = .connected
|
||||
return existingConnection
|
||||
}
|
||||
|
||||
return connectionQueue.sync {
|
||||
connection?.invalidate()
|
||||
connection = nil
|
||||
|
||||
@@ -249,79 +280,88 @@ class PrivilegedHelperManager: NSObject {
|
||||
newConnection.resume()
|
||||
connection = newConnection
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var isConnected = false
|
||||
|
||||
if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol {
|
||||
helper.executeCommand("whoami") { [weak self] result in
|
||||
if result == "root" {
|
||||
isConnected = true
|
||||
DispatchQueue.main.async {
|
||||
self?.connectionState = .connected
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self?.connectionState = .disconnected
|
||||
self?.connection?.invalidate()
|
||||
self?.connection = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
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) {
|
||||
guard let connection = connectToHelper() else {
|
||||
connectionState = .disconnected
|
||||
completion("Error: Could not connect to helper")
|
||||
return
|
||||
}
|
||||
|
||||
guard let helper = connection.remoteObjectProxyWithErrorHandler({ error in
|
||||
self.connectionState = .disconnected
|
||||
}) as? HelperToolProtocol else {
|
||||
connectionState = .disconnected
|
||||
completion("Error: Could not get helper proxy")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let helper = try getHelperProxy()
|
||||
helper.executeCommand(command) { [weak self] result in
|
||||
DispatchQueue.main.async {
|
||||
if self?.connection == nil {
|
||||
self?.connectionState = .disconnected
|
||||
completion("Error: Connection lost")
|
||||
return
|
||||
}
|
||||
|
||||
if result.starts(with: "Error:") {
|
||||
self?.connectionState = .disconnected
|
||||
} else {
|
||||
self?.connectionState = .connected
|
||||
}
|
||||
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
connectionState = .disconnected
|
||||
completion("Error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func reconnectHelper(completion: @escaping (Bool, String) -> Void) {
|
||||
connectionState = .disconnected
|
||||
connection?.invalidate()
|
||||
connection = nil
|
||||
|
||||
guard let newConnection = connectToHelper() else {
|
||||
print("重新连接失败")
|
||||
completion(false, "无法连接到 Helper")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard let newConnection = self.connectToHelper() else {
|
||||
completion(false, String(localized: "无法连接到 Helper"))
|
||||
return
|
||||
}
|
||||
|
||||
guard let helper = newConnection.remoteObjectProxyWithErrorHandler({ error in
|
||||
completion(false, "连接出现错误: \(error.localizedDescription)")
|
||||
completion(false, String(localized: "连接出现错误: \(error.localizedDescription)"))
|
||||
}) as? HelperToolProtocol else {
|
||||
completion(false, "无法获取 Helper 代理")
|
||||
completion(false, String(localized: "无法获取 Helper 代理"))
|
||||
return
|
||||
}
|
||||
|
||||
helper.executeCommand("whoami") { result in
|
||||
if result == "root" {
|
||||
completion(true, "Helper 重新连接成功")
|
||||
completion(true, String(localized: "Helper 重新连接成功"))
|
||||
} else {
|
||||
completion(false, "Helper 响应异常")
|
||||
completion(false, String(localized: "Helper 响应异常"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,15 +403,27 @@ class PrivilegedHelperManager: NSObject {
|
||||
try await Task.sleep(nanoseconds: 100_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
func forceReinstallHelper() {
|
||||
guard !isInitializing else { return }
|
||||
isInitializing = true
|
||||
|
||||
removeInstallHelper()
|
||||
notifyInstall()
|
||||
|
||||
isInitializing = false
|
||||
}
|
||||
|
||||
func disconnectHelper() {
|
||||
connection?.invalidate()
|
||||
connection = nil
|
||||
connectionState = .disconnected
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivilegedHelperManager {
|
||||
private func notifyInstall() {
|
||||
if useLegacyInstall {
|
||||
useLegacyInstall = false
|
||||
checkInstall()
|
||||
return
|
||||
}
|
||||
guard !isInitializing else { return }
|
||||
|
||||
let result = installHelperDaemon()
|
||||
if case .success = result {
|
||||
@@ -385,7 +437,7 @@ extension PrivilegedHelperManager {
|
||||
if !isCancle, useLegacyInstall {
|
||||
checkInstall()
|
||||
} else if isCancle, !useLegacyInstall {
|
||||
NSAlert.alert(with: "获取管理员授权失败,用户主动取消授权!")
|
||||
NSAlert.alert(with: String(localized: "获取管理员授权失败,用户主动取消授权!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,3 +511,54 @@ extension NSAlert {
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivilegedHelperManager {
|
||||
private func setupAutoReconnect() {
|
||||
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if self.connectionState == .disconnected {
|
||||
print("尝试重新连接 Helper...")
|
||||
_ = self.connectToHelper()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum HelperError: LocalizedError {
|
||||
case connectionFailed
|
||||
case proxyError
|
||||
case authorizationFailed
|
||||
case installationFailed(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .connectionFailed:
|
||||
return "无法连接到 Helper"
|
||||
case .proxyError:
|
||||
return "无法获取 Helper 代理"
|
||||
case .authorizationFailed:
|
||||
return "获取授权失败"
|
||||
case .installationFailed(let reason):
|
||||
return "安装失败: \(reason)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivilegedHelperManager {
|
||||
private func getHelperProxy() throws -> HelperToolProtocol {
|
||||
if connectionState != .connected {
|
||||
guard let newConnection = connectToHelper() else {
|
||||
throw HelperError.connectionFailed
|
||||
}
|
||||
connection = newConnection
|
||||
}
|
||||
|
||||
guard let helper = connection?.remoteObjectProxyWithErrorHandler({ [weak self] error in
|
||||
self?.connectionState = .disconnected
|
||||
}) as? HelperToolProtocol else {
|
||||
throw HelperError.proxyError
|
||||
}
|
||||
|
||||
return helper
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<key>SMPrivilegedExecutables</key>
|
||||
<dict>
|
||||
<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>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string>
|
||||
|
||||
@@ -23,9 +23,21 @@ class NetworkManager: ObservableObject {
|
||||
internal var monitor = NWPathMonitor()
|
||||
internal var isFetchingProducts = false
|
||||
private let installManager = InstallManager()
|
||||
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
|
||||
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true
|
||||
@AppStorage("apiVersion") private var apiVersion: String = "6"
|
||||
|
||||
private var defaultDirectory: String {
|
||||
get { StorageData.shared.defaultDirectory }
|
||||
set { StorageData.shared.defaultDirectory = newValue }
|
||||
}
|
||||
|
||||
private var useDefaultDirectory: Bool {
|
||||
get { StorageData.shared.useDefaultDirectory }
|
||||
set { StorageData.shared.useDefaultDirectory = newValue }
|
||||
}
|
||||
|
||||
private var apiVersion: String {
|
||||
get { StorageData.shared.apiVersion }
|
||||
set { StorageData.shared.apiVersion = newValue }
|
||||
}
|
||||
|
||||
enum InstallationState {
|
||||
case idle
|
||||
@@ -38,8 +50,9 @@ class NetworkManager: ObservableObject {
|
||||
|
||||
init(networkService: NetworkService = NetworkService(),
|
||||
downloadUtils: DownloadUtils? = nil) {
|
||||
let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon")
|
||||
self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
||||
self.allowedPlatform = StorageData.shared.downloadAppleSilicon ?
|
||||
["macuniversal", "macarm64"] :
|
||||
["macuniversal", "osx10-64", "osx10"]
|
||||
|
||||
self.networkService = networkService
|
||||
self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
||||
@@ -51,7 +64,10 @@ class NetworkManager: ObservableObject {
|
||||
func fetchProducts() async {
|
||||
loadingState = .loading
|
||||
do {
|
||||
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(version: apiVersion)
|
||||
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(
|
||||
version: apiVersion,
|
||||
platform: allowedPlatform.joined(separator: ",")
|
||||
)
|
||||
await MainActor.run {
|
||||
self.saps = saps
|
||||
self.cdn = cdn
|
||||
@@ -68,7 +84,7 @@ class NetworkManager: ObservableObject {
|
||||
guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else {
|
||||
throw NetworkError.invalidData("无法获取产品信息")
|
||||
}
|
||||
|
||||
print(productInfo.apPlatform)
|
||||
let task = NewDownloadTask(
|
||||
sapCode: sap.sapCode,
|
||||
version: selectedVersion,
|
||||
@@ -157,7 +173,7 @@ class NetworkManager: ObservableObject {
|
||||
|
||||
while retryCount < maxRetries {
|
||||
do {
|
||||
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(version: apiVersion)
|
||||
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData(version: apiVersion, platform: allowedPlatform.joined(separator: ","))
|
||||
|
||||
await MainActor.run {
|
||||
self.saps = saps
|
||||
@@ -345,11 +361,14 @@ class NetworkManager: ObservableObject {
|
||||
}
|
||||
|
||||
func updateAllowedPlatform(useAppleSilicon: Bool) {
|
||||
allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
||||
allowedPlatform = useAppleSilicon ?
|
||||
["macuniversal", "macarm64"] :
|
||||
["macuniversal", "osx10-64", "osx10"]
|
||||
}
|
||||
|
||||
func saveTask(_ task: NewDownloadTask) {
|
||||
TaskPersistenceManager.shared.saveTask(task)
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
func loadSavedTasks() {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
class NetworkService {
|
||||
func fetchProductsData(version: String) async throws -> ([String: Sap], String, [SapCodes]) {
|
||||
typealias ProductsData = (products: [String: Sap], cdn: String, sapCodes: [SapCodes])
|
||||
|
||||
private func makeProductsURL(version: String) throws -> URL {
|
||||
var components = URLComponents(string: NetworkConstants.productsXmlURL)
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "_type", value: "xml"),
|
||||
@@ -15,10 +17,19 @@ class NetworkService {
|
||||
guard let url = components?.url else {
|
||||
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
|
||||
}
|
||||
print(url)
|
||||
return url
|
||||
}
|
||||
|
||||
private func configureRequest(_ request: inout URLRequest, headers: [String: String]) {
|
||||
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
}
|
||||
|
||||
func fetchProductsData(version: String, platform: String) async throws -> ProductsData {
|
||||
let url = try makeProductsURL(version: version)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders)
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
@@ -34,7 +45,7 @@ class NetworkService {
|
||||
throw NetworkError.invalidData("无法解码XML数据")
|
||||
}
|
||||
|
||||
let result: ([String: Sap], String, [SapCodes]) = try await Task.detached(priority: .userInitiated) {
|
||||
let result: ProductsData = try await Task.detached(priority: .userInitiated) {
|
||||
let parseResult = try XHXMLParser.parse(xmlString: xmlString)
|
||||
let products = parseResult.products, cdn = parseResult.cdn
|
||||
var sapCodes: [SapCodes] = []
|
||||
|
||||
125
Adobe Downloader/Storages/StorageData.swift
Normal file
125
Adobe Downloader/Storages/StorageData.swift
Normal 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")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ class DownloadUtils {
|
||||
var fileName: String
|
||||
private var hasCompleted = false
|
||||
private let completionLock = NSLock()
|
||||
private var lastUpdateTime = Date()
|
||||
private var lastBytes: Int64 = 0
|
||||
|
||||
init(destinationDirectory: URL,
|
||||
fileName: String,
|
||||
@@ -93,13 +95,29 @@ class DownloadUtils {
|
||||
guard totalBytesExpectedToWrite > 0 else { return }
|
||||
guard bytesWritten > 0 else { return }
|
||||
|
||||
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
handleProgressUpdate(
|
||||
bytesWritten: bytesWritten,
|
||||
totalBytesWritten: totalBytesWritten,
|
||||
totalBytesExpectedToWrite: totalBytesExpectedToWrite
|
||||
)
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
completionHandler = { _, _, _ in }
|
||||
progressHandler = nil
|
||||
}
|
||||
|
||||
private func handleProgressUpdate(bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
let now = Date()
|
||||
let timeDiff = now.timeIntervalSince(lastUpdateTime)
|
||||
|
||||
guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
|
||||
|
||||
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
|
||||
lastUpdateTime = now
|
||||
lastBytes = totalBytesWritten
|
||||
}
|
||||
}
|
||||
|
||||
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
|
||||
@@ -194,32 +212,15 @@ class DownloadUtils {
|
||||
"""
|
||||
}
|
||||
|
||||
func clearExtendedAttributes(at url: URL) async throws {
|
||||
let escapedPath = url.path.replacingOccurrences(of: "'", with: "'\\''")
|
||||
let script = """
|
||||
do shell script "sudo xattr -cr '\(escapedPath)'" with administrator privileges
|
||||
"""
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||
process.arguments = ["-e", script]
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
let data = try pipe.fileHandleForReading.readToEnd() ?? Data()
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
print("xattr command output:", output)
|
||||
private func executePrivilegedCommand(_ command: String) async throws -> String {
|
||||
return await withCheckedContinuation { continuation in
|
||||
PrivilegedHelperManager.shared.executeCommand(command) { result in
|
||||
if result.starts(with: "Error:") {
|
||||
continuation.resume(returning: result)
|
||||
} else {
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error executing xattr command:", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,11 +506,6 @@ class DownloadUtils {
|
||||
|
||||
let (manifestData, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
#if DEBUG
|
||||
if let manifestString = String(data: manifestData, encoding: .utf8) {
|
||||
print("Manifest内容: \(manifestString)")
|
||||
}
|
||||
#endif
|
||||
let manifestDoc = try XMLDocument(data: manifestData)
|
||||
|
||||
guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
|
||||
@@ -662,28 +658,23 @@ class DownloadUtils {
|
||||
|
||||
for dependency in productInfo.dependencies {
|
||||
if let dependencyVersions = saps[dependency.sapCode]?.versions {
|
||||
let sortedVersions = dependencyVersions.sorted { first, second in
|
||||
first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending
|
||||
|
||||
let matchingVersions = dependencyVersions.filter {
|
||||
$0.value.baseVersion == dependency.version
|
||||
}
|
||||
|
||||
var firstGuid = "", buildGuid = ""
|
||||
|
||||
for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version {
|
||||
if firstGuid.isEmpty { firstGuid = versionInfo.buildGuid }
|
||||
|
||||
if allowedPlatform.contains(versionInfo.apPlatform) {
|
||||
buildGuid = versionInfo.buildGuid
|
||||
break
|
||||
}
|
||||
var selectedVersion: (key: String, value: Sap.Versions)? = matchingVersions.first {
|
||||
allowedPlatform.contains($0.value.apPlatform)
|
||||
}
|
||||
|
||||
if buildGuid.isEmpty { buildGuid = firstGuid }
|
||||
selectedVersion = selectedVersion ?? matchingVersions.first
|
||||
|
||||
if !buildGuid.isEmpty {
|
||||
if let version = selectedVersion {
|
||||
productsToDownload.append(ProductsToDownload(
|
||||
sapCode: dependency.sapCode,
|
||||
version: dependency.version,
|
||||
buildGuid: buildGuid
|
||||
buildGuid: version.value.buildGuid
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1132,4 +1123,67 @@ class DownloadUtils {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func handleDownloadError(_ error: Error, task: URLSessionTask) -> Error {
|
||||
let nsError = error as NSError
|
||||
switch nsError.code {
|
||||
case NSURLErrorCancelled:
|
||||
return NetworkError.cancelled
|
||||
case NSURLErrorTimedOut:
|
||||
return NetworkError.timeout
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NetworkError.noConnection
|
||||
case NSURLErrorCannotWriteToFile:
|
||||
if let expectedSize = task.response?.expectedContentLength {
|
||||
let fileManager = FileManager.default
|
||||
if let availableSpace = try? fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())[.systemFreeSize] as? Int64 {
|
||||
return NetworkError.insufficientStorage(expectedSize, availableSpace)
|
||||
}
|
||||
}
|
||||
return NetworkError.downloadError("存储空间不足", error)
|
||||
default:
|
||||
return NetworkError.downloadError("下载失败: \(error.localizedDescription)", error)
|
||||
}
|
||||
}
|
||||
|
||||
private func moveDownloadedFile(from location: URL, to destination: URL) throws {
|
||||
let fileManager = FileManager.default
|
||||
let destinationDirectory = destination.deletingLastPathComponent()
|
||||
|
||||
do {
|
||||
if !fileManager.fileExists(atPath: destinationDirectory.path) {
|
||||
try fileManager.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
try fileManager.removeItem(at: destination)
|
||||
}
|
||||
|
||||
try fileManager.moveItem(at: location, to: destination)
|
||||
} catch {
|
||||
switch (error as NSError).code {
|
||||
case NSFileWriteNoPermissionError:
|
||||
throw NetworkError.filePermissionDenied(destination.path)
|
||||
case NSFileWriteOutOfSpaceError:
|
||||
throw NetworkError.insufficientStorage(
|
||||
try fileManager.attributesOfItem(atPath: location.path)[.size] as? Int64 ?? 0,
|
||||
try fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())[.systemFreeSize] as? Int64 ?? 0
|
||||
)
|
||||
default:
|
||||
throw NetworkError.fileSystemError("移动文件失败", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createDownloadTask(url: URL?, resumeData: Data?, session: URLSession) throws -> URLSessionDownloadTask {
|
||||
if let resumeData = resumeData {
|
||||
return session.downloadTask(withResumeData: resumeData)
|
||||
} else if let url = url {
|
||||
var request = URLRequest(url: url)
|
||||
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
return session.downloadTask(with: request)
|
||||
} else {
|
||||
throw NetworkError.invalidData("Neither URL nor resume data provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
//
|
||||
/*
|
||||
Adobe Exit Code
|
||||
107: 架构或者版本不一致
|
||||
107: 架构不一致或安装文件被损坏
|
||||
103: 权限问题
|
||||
182: 可能是文件不全或者出错了
|
||||
133: 磁盘空间不足
|
||||
*/
|
||||
import Foundation
|
||||
|
||||
@@ -32,6 +33,44 @@ actor InstallManager {
|
||||
private var progressHandler: ((Double, String) -> Void)?
|
||||
private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||
|
||||
actor InstallationState {
|
||||
var isCompleted = false
|
||||
var error: Error?
|
||||
var hasExitCode0 = false
|
||||
var lastOutputTime = Date()
|
||||
|
||||
func markCompleted() {
|
||||
isCompleted = true
|
||||
}
|
||||
|
||||
func setError(_ error: Error) {
|
||||
if !isCompleted {
|
||||
self.error = error
|
||||
isCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
func setExitCode0() {
|
||||
hasExitCode0 = true
|
||||
}
|
||||
|
||||
func updateLastOutputTime() {
|
||||
lastOutputTime = Date()
|
||||
}
|
||||
|
||||
func getTimeSinceLastOutput() -> TimeInterval {
|
||||
return Date().timeIntervalSince(lastOutputTime)
|
||||
}
|
||||
|
||||
var shouldContinue: Bool {
|
||||
!isCompleted
|
||||
}
|
||||
|
||||
var hasReceivedExitCode0: Bool {
|
||||
hasExitCode0
|
||||
}
|
||||
}
|
||||
|
||||
private func executeInstallation(
|
||||
at appPath: URL,
|
||||
progressHandler: @escaping (Double, String) -> Void
|
||||
@@ -41,7 +80,7 @@ actor InstallManager {
|
||||
}
|
||||
|
||||
let driverPath = appPath.appendingPathComponent("driver.xml").path
|
||||
let installCommand = "\"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||
let installCommand = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||
|
||||
await MainActor.run {
|
||||
progressHandler(0.0, String(localized: "正在准备安装..."))
|
||||
@@ -123,6 +162,9 @@ actor InstallManager {
|
||||
at appPath: URL,
|
||||
progressHandler: @escaping (Double, String) -> Void
|
||||
) async throws {
|
||||
cancel()
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
|
||||
try await executeInstallation(
|
||||
at: appPath,
|
||||
progressHandler: progressHandler
|
||||
|
||||
@@ -11,29 +11,61 @@ struct ParseResult {
|
||||
}
|
||||
|
||||
class XHXMLParser {
|
||||
private static let xpathCache = [
|
||||
"cdn": "//channels/channel/cdn/secure",
|
||||
"products": "//channels/channel/products/product",
|
||||
"icons": "productIcons/icon",
|
||||
"platforms": "platforms/platform",
|
||||
"languageSet": "languageSet",
|
||||
"dependencies": "dependencies/dependency",
|
||||
"sapCode": "sapCode",
|
||||
"baseVersion": "baseVersion",
|
||||
"builds": "//builds/build",
|
||||
"appVersion": "nglLicensingInfo/appVersion",
|
||||
"manifestURL": "urls/manifestURL"
|
||||
]
|
||||
|
||||
private static func createParentMap(_ root: XMLNode?) -> [XMLNode: XMLNode] {
|
||||
var parentMap = [XMLNode: XMLNode](minimumCapacity: 500)
|
||||
|
||||
func traverse(_ node: XMLNode) {
|
||||
guard let children = node.children else { return }
|
||||
for child in children {
|
||||
parentMap[child] = node
|
||||
traverse(child)
|
||||
}
|
||||
}
|
||||
|
||||
if let root = root {
|
||||
traverse(root)
|
||||
}
|
||||
return parentMap
|
||||
}
|
||||
|
||||
static func parseProductsXML(xmlData: Data) throws -> ParseResult {
|
||||
let xml = try XMLDocument(data: xmlData)
|
||||
|
||||
guard let cdn = try xml.nodes(forXPath: "//channels/channel/cdn/secure").first?.stringValue else {
|
||||
guard let cdn = try xml.nodes(forXPath: xpathCache["cdn"]!).first?.stringValue else {
|
||||
throw ParserError.missingCDN
|
||||
}
|
||||
|
||||
var products: [String: Sap] = [:]
|
||||
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())
|
||||
|
||||
for productNode in productNodes {
|
||||
guard let element = productNode as? XMLElement else { continue }
|
||||
|
||||
let sap = element.attribute(forName: "id")?.stringValue ?? ""
|
||||
let parentElement = parentMap[parentMap[element] ?? element]
|
||||
let hidden = (parentElement as? XMLElement)?.attribute(forName: "name")?.stringValue != "ccm"
|
||||
|
||||
let displayName = try element.nodes(forXPath: "displayName").first?.stringValue ?? ""
|
||||
var productVersion = element.attribute(forName: "version")?.stringValue ?? ""
|
||||
|
||||
if products[sap] == nil {
|
||||
let icons = try element.nodes(forXPath: "productIcons/icon").compactMap { node -> Sap.ProductIcon? in
|
||||
let icons = try element.nodes(forXPath: xpathCache["icons"]!).compactMap { node -> Sap.ProductIcon? in
|
||||
guard let element = node as? XMLElement,
|
||||
let size = element.attribute(forName: "size")?.stringValue,
|
||||
let url = element.stringValue else {
|
||||
@@ -51,45 +83,40 @@ class XHXMLParser {
|
||||
)
|
||||
}
|
||||
|
||||
let platforms = try element.nodes(forXPath: "platforms/platform")
|
||||
let platforms = try element.nodes(forXPath: xpathCache["platforms"]!)
|
||||
for platformNode in platforms {
|
||||
guard let platform = platformNode as? XMLElement,
|
||||
let languageSet = try platform.nodes(forXPath: "languageSet").first as? XMLElement else { continue }
|
||||
let languageSet = try platform.nodes(forXPath: xpathCache["languageSet"]!).first as? XMLElement else { continue }
|
||||
|
||||
var baseVersion = languageSet.attribute(forName: "baseVersion")?.stringValue ?? ""
|
||||
var buildGuid = languageSet.attribute(forName: "buildGuid")?.stringValue ?? ""
|
||||
let appPlatform = platform.attribute(forName: "id")?.stringValue ?? ""
|
||||
|
||||
if let existingVersion = products[sap]?.versions[productVersion] {
|
||||
if existingVersion.apPlatform == "macuniversal" {
|
||||
if let existingVersion = products[sap]?.versions[productVersion],
|
||||
existingVersion.apPlatform == "macuniversal" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let dependencies = try languageSet.nodes(forXPath: "dependencies/dependency").compactMap { node -> Sap.Versions.Dependencies? in
|
||||
guard let element = node as? XMLElement,
|
||||
let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue,
|
||||
let version = try element.nodes(forXPath: "baseVersion").first?.stringValue else {
|
||||
return nil
|
||||
}
|
||||
let dependencies = try languageSet.nodes(forXPath: xpathCache["dependencies"]!).compactMap { node -> Sap.Versions.Dependencies? in
|
||||
guard let element = node as? XMLElement else { return nil }
|
||||
let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue ?? ""
|
||||
let version = try element.nodes(forXPath: "baseVersion").first?.stringValue ?? ""
|
||||
guard !sapCode.isEmpty, !version.isEmpty else { return nil }
|
||||
return Sap.Versions.Dependencies(sapCode: sapCode, version: version)
|
||||
}
|
||||
|
||||
if sap == "APRO" {
|
||||
baseVersion = productVersion
|
||||
let buildNodes = try xml.nodes(forXPath: "//builds/build")
|
||||
for buildNode in buildNodes {
|
||||
guard let buildElement = buildNode as? XMLElement,
|
||||
buildElement.attribute(forName: "id")?.stringValue == sap,
|
||||
buildElement.attribute(forName: "version")?.stringValue == baseVersion else {
|
||||
continue
|
||||
}
|
||||
if let appVersion = try buildElement.nodes(forXPath: "nglLicensingInfo/appVersion").first?.stringValue {
|
||||
if let buildNode = try xml.nodes(forXPath: xpathCache["builds"]!).first(where: { node in
|
||||
guard let element = node as? XMLElement else { return false }
|
||||
return element.attribute(forName: "id")?.stringValue == sap &&
|
||||
element.attribute(forName: "version")?.stringValue == baseVersion
|
||||
}) as? XMLElement {
|
||||
if let appVersion = try buildNode.nodes(forXPath: xpathCache["appVersion"]!).first?.stringValue {
|
||||
productVersion = appVersion
|
||||
break
|
||||
}
|
||||
}
|
||||
buildGuid = try languageSet.nodes(forXPath: "urls/manifestURL").first?.stringValue ?? ""
|
||||
buildGuid = try languageSet.nodes(forXPath: xpathCache["manifestURL"]!).first?.stringValue ?? ""
|
||||
}
|
||||
|
||||
if !buildGuid.isEmpty {
|
||||
@@ -108,23 +135,6 @@ class XHXMLParser {
|
||||
|
||||
return ParseResult(products: products, cdn: cdn)
|
||||
}
|
||||
|
||||
private static func createParentMap(_ root: XMLNode?) -> [XMLNode: XMLNode] {
|
||||
var parentMap: [XMLNode: XMLNode] = [:]
|
||||
|
||||
func traverse(_ node: XMLNode) {
|
||||
for child in node.children ?? [] {
|
||||
parentMap[child] = node
|
||||
traverse(child)
|
||||
}
|
||||
}
|
||||
|
||||
if let root = root {
|
||||
traverse(root)
|
||||
}
|
||||
|
||||
return parentMap
|
||||
}
|
||||
}
|
||||
|
||||
enum ParserError: Error {
|
||||
|
||||
@@ -8,23 +8,33 @@ import SwiftUI
|
||||
import Sparkle
|
||||
import Combine
|
||||
|
||||
struct PulsingCircle: View {
|
||||
let color: Color
|
||||
@State private var scale: CGFloat = 1.0
|
||||
|
||||
private enum AboutViewConstants {
|
||||
static let appIconSize: CGFloat = 96
|
||||
static let titleFontSize: CGFloat = 18
|
||||
static let subtitleFontSize: CGFloat = 14
|
||||
static let linkFontSize: CGFloat = 14
|
||||
static let licenseFontSize: CGFloat = 12
|
||||
|
||||
static let verticalSpacing: CGFloat = 12
|
||||
static let formPadding: CGFloat = 8
|
||||
|
||||
static let links: [(title: String, url: String)] = [
|
||||
("@X1a0He", "https://t.me/X1a0He"),
|
||||
("Github: Adobe Downloader", "https://github.com/X1a0He/Adobe-Downloader"),
|
||||
("Drovosek01: adobe-packager", "https://github.com/Drovosek01/adobe-packager"),
|
||||
("QiuChenly: InjectLib", "https://github.com/QiuChenly/InjectLib")
|
||||
]
|
||||
}
|
||||
|
||||
struct ExternalLinkView: View {
|
||||
let title: String
|
||||
let url: String
|
||||
|
||||
var body: some View {
|
||||
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
|
||||
}
|
||||
Link(title, destination: URL(string: url)!)
|
||||
.font(.system(size: AboutViewConstants.linkFontSize))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@Published var setupVersion: String = ""
|
||||
@Published var isDownloadingSetup = false
|
||||
@@ -67,18 +144,40 @@ final class GeneralSettingsViewModel: ObservableObject {
|
||||
@Published var showDownloadConfirmAlert = false
|
||||
@Published var showReprocessConfirmAlert = false
|
||||
@Published var isProcessing = false
|
||||
@Published var helperConnectionStatus: HelperConnectionStatus = .connecting
|
||||
@Published var helperConnectionStatus: HelperConnectionStatus = .disconnected
|
||||
@Published var downloadAppleSilicon: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(downloadAppleSilicon, forKey: "downloadAppleSilicon")
|
||||
StorageData.shared.downloadAppleSilicon = downloadAppleSilicon
|
||||
}
|
||||
}
|
||||
|
||||
@AppStorage("defaultLanguage") var defaultLanguage: String = "ALL"
|
||||
@AppStorage("defaultDirectory") var defaultDirectory: String = ""
|
||||
@AppStorage("useDefaultLanguage") var useDefaultLanguage: Bool = true
|
||||
@AppStorage("useDefaultDirectory") var useDefaultDirectory: Bool = true
|
||||
@AppStorage("confirmRedownload") var confirmRedownload: Bool = true
|
||||
var defaultLanguage: String {
|
||||
get { StorageData.shared.defaultLanguage }
|
||||
set { StorageData.shared.defaultLanguage = newValue }
|
||||
}
|
||||
|
||||
var defaultDirectory: String {
|
||||
get { StorageData.shared.defaultDirectory }
|
||||
set { StorageData.shared.defaultDirectory = newValue }
|
||||
}
|
||||
|
||||
var useDefaultLanguage: Bool {
|
||||
get { StorageData.shared.useDefaultLanguage }
|
||||
set { StorageData.shared.useDefaultLanguage = newValue }
|
||||
}
|
||||
|
||||
var useDefaultDirectory: Bool {
|
||||
get { StorageData.shared.useDefaultDirectory }
|
||||
set { StorageData.shared.useDefaultDirectory = newValue }
|
||||
}
|
||||
|
||||
var confirmRedownload: Bool {
|
||||
get { StorageData.shared.confirmRedownload }
|
||||
set {
|
||||
StorageData.shared.confirmRedownload = newValue
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@Published var automaticallyChecksForUpdates: Bool
|
||||
@Published var automaticallyDownloadsUpdates: Bool
|
||||
@@ -99,7 +198,7 @@ final class GeneralSettingsViewModel: ObservableObject {
|
||||
self.updater = updater
|
||||
self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
|
||||
self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates
|
||||
self.downloadAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon")
|
||||
self.downloadAppleSilicon = StorageData.shared.downloadAppleSilicon
|
||||
|
||||
self.helperConnectionStatus = .connecting
|
||||
|
||||
@@ -117,9 +216,14 @@ final class GeneralSettingsViewModel: ObservableObject {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
PrivilegedHelperManager.shared.executeCommand("whoami") { _ in }
|
||||
|
||||
NotificationCenter.default.publisher(for: .storageDidChange)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -159,7 +263,6 @@ struct GeneralSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
DownloadSettingsView(viewModel: viewModel)
|
||||
|
||||
HelperSettingsView(viewModel: viewModel,
|
||||
showHelperAlert: $showHelperAlert,
|
||||
helperAlertMessage: $helperAlertMessage,
|
||||
@@ -259,6 +362,9 @@ struct GeneralSettingsView: View {
|
||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
||||
networkManager.updateAllowedPlatform(useAppleSilicon: viewModel.downloadAppleSilicon)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .storageDidChange)) { _ in
|
||||
viewModel.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,56 +433,6 @@ struct UpdateSettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct AboutAppView: View {
|
||||
private var appVersion: String {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
||||
// let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
|
||||
// return "Version \(version) (\(build))"
|
||||
return "\(version)"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 96, height: 96)
|
||||
|
||||
Text("Adobe Downloader \(appVersion)")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
|
||||
Text("By X1a0He. ❤️ Love from China. ❤️")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Link("联系 @X1a0He",
|
||||
destination: URL(string: "https://t.me/X1a0He")!)
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
Link("Github: Adobe Downloader",
|
||||
destination: URL(string: "https://github.com/X1a0He/Adobe-Downloader")!)
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Link("感谢 Drovosek01: adobe-packager",
|
||||
destination: URL(string: "https://github.com/Drovosek01/adobe-packager")!)
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Link("感谢 QiuChenly: InjectLib",
|
||||
destination: URL(string: "https://github.com/QiuChenly/InjectLib")!)
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Text("GNU通用公共许可证GPL v3.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("About Tab") {
|
||||
AboutAppView()
|
||||
}
|
||||
@@ -419,7 +475,10 @@ struct LanguageSettingRow: View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Toggle("使用默认语言", isOn: $viewModel.useDefaultLanguage)
|
||||
Toggle("使用默认语言", isOn: Binding(
|
||||
get: { viewModel.useDefaultLanguage },
|
||||
set: { viewModel.useDefaultLanguage = $0 }
|
||||
))
|
||||
.padding(.leading, 5)
|
||||
Spacer()
|
||||
Text(getLanguageName(code: viewModel.defaultLanguage))
|
||||
@@ -500,6 +559,7 @@ struct ArchitectureSettingRow: View {
|
||||
HStack {
|
||||
Toggle("下载 Apple Silicon 架构", isOn: $viewModel.downloadAppleSilicon)
|
||||
.padding(.leading, 5)
|
||||
.disabled(networkManager.loadingState == .loading)
|
||||
Spacer()
|
||||
Text("当前架构: \(AppStatics.cpuArchitecture)")
|
||||
.foregroundColor(.secondary)
|
||||
@@ -508,6 +568,9 @@ struct ArchitectureSettingRow: View {
|
||||
}
|
||||
.onChange(of: viewModel.downloadAppleSilicon) { newValue in
|
||||
networkManager.updateAllowedPlatform(useAppleSilicon: newValue)
|
||||
Task {
|
||||
await networkManager.fetchProducts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,6 +581,7 @@ struct HelperStatusRow: View {
|
||||
@Binding var helperAlertMessage: String
|
||||
@Binding var helperAlertSuccess: Bool
|
||||
@State private var isReinstallingHelper = false
|
||||
@State private var installationTask: Task<Void, Error>?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
@@ -526,7 +590,7 @@ struct HelperStatusRow: View {
|
||||
if PrivilegedHelperManager.getHelperStatus {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("已安装")
|
||||
Text("已安装 (build \(UserDefaults.standard.string(forKey: "InstalledHelperBuild") ?? "0"))")
|
||||
} else {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
@@ -594,10 +658,10 @@ struct HelperStatusRow: View {
|
||||
|
||||
private var helperStatusText: String {
|
||||
switch viewModel.helperConnectionStatus {
|
||||
case .connected: return "运行正常"
|
||||
case .connecting: return "正在连接"
|
||||
case .checking: return "检查中"
|
||||
case .disconnected: return "连接断开"
|
||||
case .connected: return String(localized: "运行正常")
|
||||
case .connecting: return String(localized: "正在连接")
|
||||
case .checking: return String(localized: "检查中")
|
||||
case .disconnected: return String(localized: "连接断开")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -618,17 +682,6 @@ struct SetupComponentRow: View {
|
||||
.foregroundColor(.red)
|
||||
Text("(可能导致处理 Setup 组件失败)")
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
if !ModifySetup.isSetupExists() {
|
||||
viewModel.showDownloadAlert = true
|
||||
} else {
|
||||
viewModel.showReprocessConfirmAlert = true
|
||||
}
|
||||
}) {
|
||||
Text("重新备份")
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HStack {
|
||||
@@ -656,7 +709,7 @@ struct SetupComponentRow: View {
|
||||
}
|
||||
Divider()
|
||||
HStack {
|
||||
Text("X1a0He CC 版本信息: \(viewModel.setupVersion)")
|
||||
Text("X1a0He CC 版本信息: \(viewModel.setupVersion) [\(AppStatics.cpuArchitecture)]")
|
||||
Spacer()
|
||||
|
||||
if viewModel.isDownloadingSetup {
|
||||
@@ -706,3 +759,4 @@ struct AutoDownloadRow: View {
|
||||
.disabled(viewModel.isAutomaticallyDownloadsUpdatesDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,23 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class IconCache {
|
||||
private enum AppCardConstants {
|
||||
static let cardWidth: CGFloat = 250
|
||||
static let cardHeight: CGFloat = 200
|
||||
static let iconSize: CGFloat = 64
|
||||
static let cornerRadius: CGFloat = 10
|
||||
static let buttonHeight: CGFloat = 32
|
||||
static let titleFontSize: CGFloat = 16
|
||||
static let buttonFontSize: CGFloat = 14
|
||||
|
||||
static let shadowOpacity: Double = 0.05
|
||||
static let shadowRadius: CGFloat = 2
|
||||
static let strokeOpacity: Double = 0.1
|
||||
static let strokeWidth: CGFloat = 2
|
||||
static let backgroundOpacity: Double = 0.05
|
||||
}
|
||||
|
||||
final class IconCache {
|
||||
static let shared = IconCache()
|
||||
private var cache = NSCache<NSString, NSImage>()
|
||||
|
||||
@@ -20,7 +36,8 @@ class IconCache {
|
||||
}
|
||||
}
|
||||
|
||||
class AppCardViewModel: ObservableObject {
|
||||
@MainActor
|
||||
final class AppCardViewModel: ObservableObject {
|
||||
@Published var iconImage: NSImage?
|
||||
@Published var showError = false
|
||||
@Published var errorMessage = ""
|
||||
@@ -40,12 +57,12 @@ class AppCardViewModel: ObservableObject {
|
||||
@Published var isDownloading = false
|
||||
private let userDefaults = UserDefaults.standard
|
||||
|
||||
var useDefaultDirectory: Bool {
|
||||
get { userDefaults.bool(forKey: "useDefaultDirectory") }
|
||||
private var useDefaultDirectory: Bool {
|
||||
StorageData.shared.useDefaultDirectory
|
||||
}
|
||||
|
||||
var defaultDirectory: String {
|
||||
get { userDefaults.string(forKey: "defaultDirectory") ?? "" }
|
||||
private var defaultDirectory: String {
|
||||
StorageData.shared.defaultDirectory
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
@@ -54,10 +71,28 @@ class AppCardViewModel: ObservableObject {
|
||||
self.sap = sap
|
||||
self.networkManager = networkManager
|
||||
|
||||
Task { @MainActor in
|
||||
setupObservers()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func setupObservers() {
|
||||
networkManager?.$downloadTasks
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] tasks in
|
||||
guard let self = self else { return }
|
||||
let hasActiveTask = tasks.contains {
|
||||
$0.sapCode == self.sap.sapCode && self.isTaskActive($0.status)
|
||||
}
|
||||
|
||||
if hasActiveTask != self.isDownloading {
|
||||
self.isDownloading = hasActiveTask
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
networkManager?.objectWillChange
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
@@ -66,28 +101,31 @@ class AppCardViewModel: ObservableObject {
|
||||
.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() {
|
||||
guard let networkManager = networkManager else {
|
||||
Task { @MainActor in
|
||||
self.isDownloading = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
let isActive = networkManager.downloadTasks.contains { task in
|
||||
task.sapCode == sap.sapCode && isTaskActive(task.status)
|
||||
}
|
||||
self.isDownloading = isActive
|
||||
}
|
||||
let hasActiveTask = networkManager.downloadTasks.contains {
|
||||
$0.sapCode == sap.sapCode && isTaskActive($0.status)
|
||||
}
|
||||
|
||||
private func isTaskActive(_ status: DownloadStatus) -> Bool {
|
||||
switch status {
|
||||
case .downloading, .preparing, .paused, .waiting, .retrying(_):
|
||||
return true
|
||||
case .completed, .failed:
|
||||
return false
|
||||
if hasActiveTask != self.isDownloading {
|
||||
self.isDownloading = hasActiveTask
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +223,7 @@ class AppCardViewModel: ObservableObject {
|
||||
|
||||
func checkAndStartDownload(version: String, language: String) async {
|
||||
if let networkManager = networkManager {
|
||||
if let existingPath = await networkManager.isVersionDownloaded(sap: sap, version: version, language: language) {
|
||||
if let existingPath = networkManager.isVersionDownloaded(sap: sap, version: version, language: language) {
|
||||
await MainActor.run {
|
||||
existingFilePath = existingPath
|
||||
pendingVersion = version
|
||||
@@ -212,7 +250,7 @@ class AppCardViewModel: ObservableObject {
|
||||
guard let networkManager = networkManager,
|
||||
let productInfo = sap.versions[pendingVersion] else { return }
|
||||
|
||||
let existingTask = await networkManager.downloadTasks.first { task in
|
||||
let existingTask = networkManager.downloadTasks.first { task in
|
||||
return task.sapCode == sap.sapCode &&
|
||||
task.version == pendingVersion &&
|
||||
task.language == pendingLanguage &&
|
||||
@@ -232,14 +270,14 @@ class AppCardViewModel: ObservableObject {
|
||||
productsToDownload.append(mainProduct)
|
||||
|
||||
for dependency in productInfo.dependencies {
|
||||
if let dependencyVersions = await networkManager.saps[dependency.sapCode]?.versions {
|
||||
if let dependencyVersions = networkManager.saps[dependency.sapCode]?.versions {
|
||||
let sortedVersions = dependencyVersions.sorted { first, second in
|
||||
first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending
|
||||
}
|
||||
|
||||
var buildGuid = ""
|
||||
for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version {
|
||||
if await networkManager.allowedPlatform.contains(versionInfo.apPlatform) {
|
||||
if networkManager.allowedPlatform.contains(versionInfo.apPlatform) {
|
||||
buildGuid = versionInfo.buildGuid
|
||||
break
|
||||
}
|
||||
@@ -289,95 +327,81 @@ class AppCardViewModel: ObservableObject {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var hasValidIcon: Bool {
|
||||
iconImage != nil
|
||||
}
|
||||
|
||||
var canDownload: Bool {
|
||||
!isDownloading
|
||||
}
|
||||
|
||||
var downloadButtonTitle: String {
|
||||
isDownloading ? String(localized: "下载中") : String(localized: "下载")
|
||||
}
|
||||
|
||||
var downloadButtonIcon: String {
|
||||
isDownloading ? "hourglass.circle.fill" : "arrow.down.circle"
|
||||
}
|
||||
}
|
||||
|
||||
struct AppCardView: View {
|
||||
@StateObject private var viewModel: AppCardViewModel
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@AppStorage("useDefaultLanguage") private var useDefaultLanguage = true
|
||||
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN"
|
||||
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||
|
||||
init(sap: Sap) {
|
||||
_viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
CardContent(
|
||||
sap: viewModel.sap,
|
||||
iconImage: viewModel.iconImage,
|
||||
loadIcon: viewModel.loadIcon,
|
||||
dependenciesCount: viewModel.dependenciesCount,
|
||||
isDownloading: viewModel.isDownloading,
|
||||
showVersionPicker: $viewModel.showVersionPicker
|
||||
)
|
||||
.padding()
|
||||
.frame(width: 250, height: 200)
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.black.opacity(0.05)))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.1), lineWidth: 2)
|
||||
)
|
||||
CardContainer {
|
||||
VStack {
|
||||
IconView(viewModel: viewModel)
|
||||
ProductInfoView(viewModel: viewModel)
|
||||
Spacer()
|
||||
DownloadButtonView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
.modifier(CardModifier())
|
||||
.modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager))
|
||||
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
|
||||
.sheet(isPresented: $viewModel.showVersionPicker) {
|
||||
VersionPickerView(sap: viewModel.sap) { version in
|
||||
Task {
|
||||
await viewModel.handleDownloadRequest(version, useDefaultLanguage: useDefaultLanguage, defaultLanguage: defaultLanguage)
|
||||
.onAppear(perform: setupViewModel)
|
||||
.onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus)
|
||||
}
|
||||
}
|
||||
.environmentObject(networkManager)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showLanguagePicker) {
|
||||
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(version: viewModel.selectedVersion, language: language)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
private func setupViewModel() {
|
||||
viewModel.networkManager = networkManager
|
||||
viewModel.updateDownloadingStatus()
|
||||
}
|
||||
.onChange(of: networkManager.downloadTasks) { _ in
|
||||
|
||||
private func updateDownloadStatus(_ _: [NewDownloadTask]) {
|
||||
viewModel.updateDownloadingStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardContent: View {
|
||||
let sap: Sap
|
||||
let iconImage: NSImage?
|
||||
let loadIcon: () -> Void
|
||||
let dependenciesCount: Int
|
||||
let isDownloading: Bool
|
||||
@Binding var showVersionPicker: Bool
|
||||
private struct CardContainer<Content: View>: View {
|
||||
let content: Content
|
||||
|
||||
init(@ViewBuilder content: () -> Content) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
IconView(iconImage: iconImage, loadIcon: loadIcon)
|
||||
ProductInfoView(sap: sap, dependenciesCount: dependenciesCount)
|
||||
Spacer()
|
||||
DownloadButton(
|
||||
isDownloading: isDownloading,
|
||||
showVersionPicker: $showVersionPicker
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func applyModifiers(viewModel: AppCardViewModel) -> some View {
|
||||
self.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
|
||||
content
|
||||
.padding()
|
||||
.frame(width: AppCardConstants.cardWidth, height: AppCardConstants.cardHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private struct IconView: View {
|
||||
let iconImage: NSImage?
|
||||
let loadIcon: () -> Void
|
||||
@ObservedObject var viewModel: AppCardViewModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let iconImage = iconImage {
|
||||
Image(nsImage: iconImage)
|
||||
if viewModel.hasValidIcon {
|
||||
Image(nsImage: viewModel.iconImage!)
|
||||
.resizable()
|
||||
.interpolation(.high)
|
||||
.scaledToFit()
|
||||
@@ -388,27 +412,26 @@ private struct IconView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 64, height: 64)
|
||||
.onAppear(perform: loadIcon)
|
||||
.frame(width: AppCardConstants.iconSize, height: AppCardConstants.iconSize)
|
||||
.onAppear(perform: viewModel.loadIcon)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProductInfoView: View {
|
||||
let sap: Sap
|
||||
let dependenciesCount: Int
|
||||
@ObservedObject var viewModel: AppCardViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(sap.displayName)
|
||||
.font(.system(size: 16))
|
||||
Text(viewModel.sap.displayName)
|
||||
.font(.system(size: AppCardConstants.titleFontSize))
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text("可用版本: \(sap.versions.count)")
|
||||
Text("可用版本: \(viewModel.sap.versions.count)")
|
||||
Text("|")
|
||||
Text("依赖包: \(dependenciesCount)")
|
||||
Text("依赖包: \(viewModel.dependenciesCount)")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -417,21 +440,74 @@ private struct ProductInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadButton: View {
|
||||
let isDownloading: Bool
|
||||
@Binding var showVersionPicker: Bool
|
||||
private struct DownloadButtonView: View {
|
||||
@ObservedObject var viewModel: AppCardViewModel
|
||||
|
||||
var body: some View {
|
||||
Button(action: { showVersionPicker = true }) {
|
||||
Label(isDownloading ? "下载中" : "下载",
|
||||
systemImage: isDownloading ? "hourglass.circle.fill" : "arrow.down.circle")
|
||||
.font(.system(size: 14))
|
||||
Button(action: { viewModel.showVersionPicker = true }) {
|
||||
Label(viewModel.downloadButtonTitle,
|
||||
systemImage: viewModel.downloadButtonIcon)
|
||||
.font(.system(size: AppCardConstants.buttonFontSize))
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(height: 32)
|
||||
.frame(height: AppCardConstants.buttonHeight)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(isDownloading ? .gray : .blue)
|
||||
.disabled(isDownloading)
|
||||
.tint(viewModel.isDownloading ? .gray : .blue)
|
||||
.disabled(!viewModel.canDownload)
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: AppCardConstants.cornerRadius)
|
||||
.fill(Color.black.opacity(AppCardConstants.backgroundOpacity))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppCardConstants.cornerRadius)
|
||||
.stroke(Color.gray.opacity(AppCardConstants.strokeOpacity),
|
||||
lineWidth: AppCardConstants.strokeWidth)
|
||||
)
|
||||
.shadow(
|
||||
color: Color.primary.opacity(AppCardConstants.shadowOpacity),
|
||||
radius: AppCardConstants.shadowRadius,
|
||||
x: 0,
|
||||
y: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SheetModifier: ViewModifier {
|
||||
@ObservedObject var viewModel: AppCardViewModel
|
||||
let networkManager: NetworkManager
|
||||
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.sheet(isPresented: $viewModel.showVersionPicker) {
|
||||
VersionPickerView(sap: viewModel.sap) { version in
|
||||
Task {
|
||||
await viewModel.handleDownloadRequest(
|
||||
version,
|
||||
useDefaultLanguage: useDefaultLanguage,
|
||||
defaultLanguage: defaultLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
.environmentObject(networkManager)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showLanguagePicker) {
|
||||
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(
|
||||
version: viewModel.selectedVersion,
|
||||
language: language
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,9 +557,16 @@ struct AlertModifier: ViewModifier {
|
||||
viewModel.showExistingFileAlert = false
|
||||
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onRedownload: {
|
||||
viewModel.showExistingFileAlert = false
|
||||
@@ -492,10 +575,7 @@ struct AlertModifier: ViewModifier {
|
||||
viewModel.showRedownloadConfirm = true
|
||||
} else {
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(
|
||||
version: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage
|
||||
)
|
||||
await startRedownload()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,20 +585,13 @@ struct AlertModifier: ViewModifier {
|
||||
},
|
||||
iconImage: viewModel.iconImage
|
||||
)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
.alert("确认重新下载", isPresented: $viewModel.showRedownloadConfirm) {
|
||||
Button("取消", role: .cancel) { }
|
||||
Button("确认") {
|
||||
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(
|
||||
version: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage
|
||||
)
|
||||
}
|
||||
await startRedownload()
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
@@ -540,4 +613,34 @@ struct AlertModifier: ViewModifier {
|
||||
Text(viewModel.errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private func startRedownload() async {
|
||||
guard let networkManager = viewModel.networkManager else { return }
|
||||
|
||||
do {
|
||||
networkManager.downloadTasks.removeAll { task in
|
||||
task.sapCode == viewModel.sap.sapCode &&
|
||||
task.version == viewModel.pendingVersion &&
|
||||
task.language == viewModel.pendingLanguage
|
||||
}
|
||||
|
||||
if let existingPath = viewModel.existingFilePath {
|
||||
try? FileManager.default.removeItem(at: existingPath)
|
||||
}
|
||||
|
||||
let destinationURL = try await viewModel.getDestinationURL(
|
||||
version: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage
|
||||
)
|
||||
|
||||
try await networkManager.startDownload(
|
||||
sap: viewModel.sap,
|
||||
selectedVersion: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage,
|
||||
destinationURL: destinationURL
|
||||
)
|
||||
} catch {
|
||||
viewModel.handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,11 +120,18 @@ struct DownloadProgressView: View {
|
||||
if !ModifySetup.isSetupBackup() {
|
||||
showSetupBackupAlert = true
|
||||
} else {
|
||||
print("正在连接 Helper...")
|
||||
if PrivilegedHelperManager.shared.connectToHelper() != nil {
|
||||
print("Helper 连接成功,开始安装...")
|
||||
showInstallPrompt = false
|
||||
isInstalling = true
|
||||
Task {
|
||||
await networkManager.installProduct(at: task.directory)
|
||||
}
|
||||
} else {
|
||||
print("Helper 连接失败")
|
||||
showSetupBackupAlert = true
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Label("安装", systemImage: "square.and.arrow.down.on.square")
|
||||
@@ -134,8 +141,13 @@ struct DownloadProgressView: View {
|
||||
.alert("Setup 组件未处理", isPresented: $showSetupBackupAlert) {
|
||||
Button("确定") { }
|
||||
} message: {
|
||||
if !ModifySetup.isSetupBackup() {
|
||||
Text("未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理")
|
||||
.font(.system(size: 18))
|
||||
} else {
|
||||
Text("Helper 未安装或未连接,请先在设置中安装并连接 Helper")
|
||||
.font(.system(size: 18))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//
|
||||
// ShouldExistsSetUpView.swift
|
||||
// Adobe Downloader
|
||||
//
|
||||
// Created by X1a0He.
|
||||
@@ -7,6 +6,18 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
private enum AlertConstants {
|
||||
static let iconSize: CGFloat = 64
|
||||
static let warningIconSize: CGFloat = 24
|
||||
static let warningIconOffset: CGFloat = 10
|
||||
static let verticalSpacing: CGFloat = 20
|
||||
static let buttonHeight: CGFloat = 32
|
||||
static let buttonWidth: CGFloat = 260
|
||||
static let buttonFontSize: CGFloat = 14
|
||||
static let cornerRadius: CGFloat = 12
|
||||
static let shadowRadius: CGFloat = 10
|
||||
}
|
||||
|
||||
struct ExistingFileAlertView: View {
|
||||
let path: URL
|
||||
let onUseExisting: () -> Void
|
||||
@@ -15,8 +26,40 @@ struct ExistingFileAlertView: View {
|
||||
let iconImage: NSImage?
|
||||
|
||||
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) {
|
||||
AppIcon(iconImage: iconImage)
|
||||
WarningIcon()
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppIcon: View {
|
||||
let iconImage: NSImage?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let iconImage = iconImage {
|
||||
Image(nsImage: iconImage)
|
||||
@@ -30,66 +73,118 @@ struct ExistingFileAlertView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 64, height: 64)
|
||||
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.orange)
|
||||
.offset(x: 10, y: 4)
|
||||
.frame(width: AlertConstants.iconSize, height: AlertConstants.iconSize)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Text("安装程序已存在")
|
||||
.font(.headline)
|
||||
private struct WarningIcon: View {
|
||||
var body: some View {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: AlertConstants.warningIconSize))
|
||||
.foregroundColor(.orange)
|
||||
.offset(x: AlertConstants.warningIconOffset, y: 4)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PathSection: View {
|
||||
let path: URL
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text(path.path)
|
||||
.foregroundColor(.blue)
|
||||
.onTapGesture {
|
||||
openInFinder(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openInFinder(_ path: URL) {
|
||||
NSWorkspace.shared.activateFileViewerSelecting([path])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ButtonSection: View {
|
||||
let onUseExisting: () -> Void
|
||||
let onRedownload: () -> Void
|
||||
let onCancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(action: onUseExisting) {
|
||||
Label("使用现有程序", systemImage: "checkmark.circle")
|
||||
.frame(minWidth: 0,maxWidth: 260)
|
||||
.frame(height: 32)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
ActionButton(
|
||||
title: "使用现有程序",
|
||||
icon: "checkmark.circle",
|
||||
color: .blue,
|
||||
action: onUseExisting
|
||||
)
|
||||
|
||||
Button(action: onRedownload) {
|
||||
Label("重新下载", systemImage: "arrow.down.circle")
|
||||
.frame(minWidth: 0,maxWidth: 260)
|
||||
.frame(height: 32)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.green)
|
||||
ActionButton(
|
||||
title: "重新下载",
|
||||
icon: "arrow.down.circle",
|
||||
color: .green,
|
||||
action: onRedownload
|
||||
)
|
||||
|
||||
Button(action: onCancel) {
|
||||
Label("取消", systemImage: "xmark.circle")
|
||||
.frame(minWidth: 0, maxWidth: 260)
|
||||
.frame(height: 32)
|
||||
.font(.system(size: 14))
|
||||
ActionButton(
|
||||
title: "取消",
|
||||
icon: "xmark.circle",
|
||||
color: .red,
|
||||
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(
|
||||
path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"),
|
||||
onUseExisting: {},
|
||||
@@ -98,9 +193,8 @@ struct ExistingFileAlertView: View {
|
||||
iconImage: NSImage(named: "PHSP")
|
||||
)
|
||||
.background(Color.black.opacity(0.3))
|
||||
}
|
||||
.previewDisplayName("Light Mode")
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
ExistingFileAlertView(
|
||||
path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"),
|
||||
onUseExisting: {},
|
||||
@@ -110,4 +204,7 @@ struct ExistingFileAlertView: View {
|
||||
)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.preferredColorScheme(.dark)
|
||||
.previewDisplayName("Dark Mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ struct InstallProgressView: View {
|
||||
}
|
||||
|
||||
private var isFailed: Bool {
|
||||
status.contains(String(localized: "失败"))
|
||||
status.contains(String(localized: "安装失败"))
|
||||
}
|
||||
|
||||
private var progressText: String {
|
||||
@@ -77,7 +77,8 @@ struct InstallProgressView: View {
|
||||
|
||||
if isFailed {
|
||||
ErrorSection(
|
||||
status: status, isFailed: isFailed
|
||||
status: status,
|
||||
isFailed: true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,7 +120,6 @@ private struct ErrorSection: View {
|
||||
let isFailed: Bool
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("错误详情:")
|
||||
.font(.caption)
|
||||
|
||||
@@ -118,7 +118,7 @@ private struct ButtonsView: View {
|
||||
if isDownloading {
|
||||
downloadProgressView
|
||||
} else {
|
||||
Label("下载 X1a0He CC 组件", systemImage: "arrow.down")
|
||||
Label("下载 X1a0He CC", systemImage: "arrow.down")
|
||||
.frame(minWidth: 0, maxWidth: 360)
|
||||
.frame(height: 32)
|
||||
.font(.system(size: 14))
|
||||
|
||||
@@ -5,11 +5,26 @@
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
|
||||
private enum VersionPickerConstants {
|
||||
static let headerPadding: CGFloat = 5
|
||||
static let viewWidth: CGFloat = 400
|
||||
static let viewHeight: CGFloat = 500
|
||||
static let iconSize: CGFloat = 32
|
||||
static let verticalSpacing: CGFloat = 8
|
||||
static let horizontalSpacing: CGFloat = 12
|
||||
static let cornerRadius: CGFloat = 8
|
||||
static let buttonPadding: CGFloat = 8
|
||||
|
||||
static let titleFontSize: CGFloat = 14
|
||||
static let captionFontSize: CGFloat = 12
|
||||
}
|
||||
|
||||
struct VersionPickerView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN"
|
||||
@AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true
|
||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
||||
@State private var expandedVersions: Set<String> = []
|
||||
|
||||
private let sap: Sap
|
||||
@@ -22,6 +37,25 @@ struct VersionPickerView: View {
|
||||
|
||||
var body: some View {
|
||||
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 {
|
||||
HStack {
|
||||
Text("\(sap.displayName)")
|
||||
@@ -33,25 +67,60 @@ struct VersionPickerView: View {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
Text("🔔 即将下载 \(downloadAppleSilicon ? "Apple Silicon" : "Intel") (\(networkManager.allowedPlatform.joined(separator: ", "))) 版本 🔔")
|
||||
.padding(.bottom, VersionPickerConstants.headerPadding)
|
||||
|
||||
Text("🔔 即将下载 \(downloadAppleSilicon ? "Apple Silicon" : "Intel") (\(platformText)) 版本 🔔")
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
|
||||
private var platformText: String {
|
||||
networkManager.allowedPlatform.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
private struct VersionListView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
let sap: Sap
|
||||
@Binding var expandedVersions: Set<String>
|
||||
let onSelect: (String) -> Void
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(Array(sap.versions.sorted { $0.key > $1.key }), id: \.key) { version, info in
|
||||
if networkManager.allowedPlatform.contains(info.apPlatform) {
|
||||
VStack(spacing: 0) {
|
||||
Button(action: {
|
||||
if info.dependencies.isEmpty {
|
||||
LazyVStack(spacing: VersionPickerConstants.verticalSpacing) {
|
||||
ForEach(filteredVersions, id: \.key) { version, info in
|
||||
VersionRow(
|
||||
sap: sap,
|
||||
version: version,
|
||||
info: info,
|
||||
isExpanded: expandedVersions.contains(version),
|
||||
onSelect: handleVersionSelect,
|
||||
onToggle: handleVersionToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
|
||||
private var filteredVersions: [(key: String, value: Sap.Versions)] {
|
||||
sap.versions
|
||||
.filter { networkManager.allowedPlatform.contains($0.value.apPlatform) }
|
||||
.sorted { $0.key > $1.key }
|
||||
}
|
||||
|
||||
private func handleVersionSelect(_ version: String) {
|
||||
onSelect(version)
|
||||
dismiss()
|
||||
} else {
|
||||
}
|
||||
|
||||
private func handleVersionToggle(_ version: String) {
|
||||
withAnimation {
|
||||
if expandedVersions.contains(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,
|
||||
version: version,
|
||||
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("已存在")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
@@ -92,31 +237,53 @@ struct VersionPickerView: View {
|
||||
.background(Color.blue)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
if !info.dependencies.isEmpty {
|
||||
Image(systemName: expandedVersions.contains(version) ? "chevron.down" : "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
private struct ExpandButton: View {
|
||||
let isExpanded: Bool
|
||||
let hasDependencies: Bool
|
||||
|
||||
if expandedVersions.contains(version) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
var body: some View {
|
||||
Image(systemName: iconName)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private var iconName: String {
|
||||
if !hasDependencies {
|
||||
return "chevron.right"
|
||||
}
|
||||
return isExpanded ? "chevron.down" : "chevron.right"
|
||||
}
|
||||
}
|
||||
|
||||
private struct VersionDetails: View {
|
||||
let info: Sap.Versions
|
||||
let version: String
|
||||
let onSelect: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: VersionPickerConstants.verticalSpacing) {
|
||||
Text("依赖包:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 16)
|
||||
|
||||
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) {
|
||||
Image(systemName: "cube.box")
|
||||
.foregroundColor(.blue)
|
||||
@@ -127,38 +294,30 @@ struct VersionPickerView: View {
|
||||
}
|
||||
.padding(.leading, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadButton: View {
|
||||
let version: String
|
||||
let onSelect: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
Button("下载此版本") {
|
||||
onSelect(version)
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
.frame(width: 400, height: 500)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
struct VersionPickerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let networkManager = NetworkManager()
|
||||
networkManager.allowedPlatform = ["macuniversal", "macarm64"]
|
||||
networkManager.cdn = "https://example.cdn.adobe.com"
|
||||
|
||||
return VersionPickerView(
|
||||
sap: Sap(
|
||||
let previewSap = Sap(
|
||||
hidden: false,
|
||||
displayName: "Photoshop",
|
||||
sapCode: "PHSP",
|
||||
@@ -195,14 +354,11 @@ struct VersionPickerView: View {
|
||||
buildGuid: "q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6"
|
||||
)
|
||||
],
|
||||
icons: [
|
||||
Sap.ProductIcon(
|
||||
size: "192x192",
|
||||
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/26.0.0/192x192.png"
|
||||
)
|
||||
]
|
||||
),
|
||||
onSelect: { version in }
|
||||
icons: []
|
||||
)
|
||||
|
||||
return VersionPickerView(sap: previewSap) { _ in }
|
||||
.environmentObject(networkManager)
|
||||
.previewDisplayName("Version Picker")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,11 @@
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<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>
|
||||
<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>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,30 @@
|
||||
# Change Log
|
||||
|
||||
## 2024-11-13 00:00 更新日志
|
||||
## 2024-11-14 15:30 更新日志
|
||||
|
||||
```markdown
|
||||
1. 新增可选API版本 (v4, v5, v6)
|
||||
1. 新增可选API版本 (v4, v5, v6)【更老的API意味着更长的等待时间】
|
||||
2. 引入 Privilege Helper 来处理所有需要权限的操作
|
||||
3. 修改从 Github 下载 Setup 组件功能,改为从官方下载简化版CC,称为 X1a0He CC
|
||||
4. 调整 CC 组件备份与处理状态检测,分离二者的检测机制
|
||||
5. 移除了安装日志显示
|
||||
6. 调整 Setup 组件版本号的获取方式
|
||||
7. 修复了当任务下载完成后,AppCardView 仍显示下载中的问题
|
||||
8. 修复了 Intel 架构下,安装时因架构文件错误出现错误代码 107 的问题
|
||||
|
||||
PS: CC 组件的来源均为 Adobe Creative Cloud 官方提取,可随时下载到最新版,但处理可能会失败
|
||||
====================
|
||||
|
||||
1. Added optional API versions (v4, v5, v6) (Older API means longer waiting time)
|
||||
2. Introduced Privilege Helper to handle all operations that require permissions
|
||||
3. Modified the function of downloading the Setup component from Github to downloading a simplified version of CC from
|
||||
the official website, called X1a0He CC
|
||||
4. Adjusted the detection of CC component backup and processing status, and separated the detection mechanism of the two
|
||||
5. Removed the installation log display
|
||||
6. Adjusted the way to obtain the version number of the Setup component
|
||||
|
||||
PS: CC components are all from Adobe Creative Cloud official extraction, you can download the latest version at any
|
||||
time, but the processing may fail
|
||||
```
|
||||
|
||||
## 2024-11-11 21:00 更新日志
|
||||
|
||||
Reference in New Issue
Block a user