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