mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
refactor: complete initial reconstruction
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
.idea/
|
||||
.fleet
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<key>Adobe-Downloader.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>AdobeDownloaderHelperTool.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
||||
@@ -105,7 +105,6 @@ struct Adobe_DownloaderApp: App {
|
||||
|
||||
Settings {
|
||||
AboutView(updater: updaterController.updater)
|
||||
.environmentObject(globalNetworkManager)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ enum NetworkError: Error, LocalizedError {
|
||||
case unsupportedPlatform(String)
|
||||
case incompatibleVersion(String, String)
|
||||
case installError(String)
|
||||
case productNotFound
|
||||
|
||||
private var errorGroup: Int {
|
||||
switch self {
|
||||
@@ -64,7 +65,7 @@ enum NetworkError: Error, LocalizedError {
|
||||
case .httpError, .serverError, .clientError: return 4000
|
||||
case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000
|
||||
case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000
|
||||
case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError: return 7000
|
||||
case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError, .productNotFound: return 7000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +95,7 @@ enum NetworkError: Error, LocalizedError {
|
||||
case .unsupportedPlatform: return 2
|
||||
case .incompatibleVersion: return 3
|
||||
case .installError: return 4
|
||||
case .productNotFound: return 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +162,8 @@ enum NetworkError: Error, LocalizedError {
|
||||
return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
|
||||
case .installError(let message):
|
||||
return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message)
|
||||
case .productNotFound:
|
||||
return NSLocalizedString("产品未找到", value: "产品未找到", comment: "Product not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
// 下面是所有全局变量的私有存储
|
||||
private var _globalStiResult: NewParseResult?
|
||||
private var _globalCcmResult: NewParseResult?
|
||||
private var _globalProducts: [Product]?
|
||||
private var _globalUniqueProducts: [UniqueProduct]?
|
||||
private var _globalCdn: String = ""
|
||||
private var _globalNetworkService: NewNetworkService?
|
||||
private var _globalNetworkManager: NetworkManager?
|
||||
@@ -96,6 +98,30 @@ var globalCancelTracker: CancelTracker {
|
||||
}
|
||||
}
|
||||
|
||||
var globalProducts: [Product] {
|
||||
get {
|
||||
if _globalProducts == nil {
|
||||
_globalProducts = []
|
||||
}
|
||||
return _globalProducts!
|
||||
}
|
||||
set {
|
||||
_globalProducts = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var globalUniqueProducts: [UniqueProduct] {
|
||||
get {
|
||||
if _globalUniqueProducts == nil {
|
||||
_globalUniqueProducts = []
|
||||
}
|
||||
return _globalUniqueProducts!
|
||||
}
|
||||
set {
|
||||
_globalUniqueProducts = newValue
|
||||
}
|
||||
}
|
||||
|
||||
func getAllProducts() -> [Product] {
|
||||
var allProducts = [Product]()
|
||||
let stiProducts = globalStiResult.products
|
||||
@@ -108,3 +134,93 @@ func getAllProducts() -> [Product] {
|
||||
}
|
||||
return allProducts
|
||||
}
|
||||
|
||||
/// 根据产品ID和版本号快速查找产品
|
||||
/// - Parameters:
|
||||
/// - id: 产品ID
|
||||
/// - version: 版本号(可选)
|
||||
/// - Returns: 如果提供版本号,返回指定版本的产品;否则返回最新版本的产品
|
||||
func findProduct(id: String, version: String? = nil) -> Product? {
|
||||
// 首先在全局产品列表中查找匹配ID的产品
|
||||
guard let product = globalProducts.first(where: { $0.id == id }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果没有指定版本,直接返回找到的产品
|
||||
guard let version = version else {
|
||||
return product
|
||||
}
|
||||
|
||||
// 检查产品的平台和语言集是否包含指定版本
|
||||
for platform in product.platforms {
|
||||
for languageSet in platform.languageSet {
|
||||
if languageSet.productVersion == version {
|
||||
return product
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 获取产品的所有可用版本
|
||||
/// - Parameter id: 产品ID
|
||||
/// - Returns: 版本号数组,已去重并按版本号排序
|
||||
func getProductVersions(id: String) -> [String] {
|
||||
guard let product = globalProducts.first(where: { $0.id == id }) else {
|
||||
return []
|
||||
}
|
||||
|
||||
// 收集所有平台和语言集中的版本号
|
||||
var versions = Set<String>()
|
||||
for platform in product.platforms {
|
||||
for languageSet in platform.languageSet {
|
||||
versions.insert(languageSet.productVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为数组并按版本号排序(降序)
|
||||
return Array(versions).sorted { version1, version2 in
|
||||
version1.compare(version2, options: .numeric) == .orderedDescending
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取产品在指定版本下的所有可用语言
|
||||
/// - Parameters:
|
||||
/// - id: 产品ID
|
||||
/// - version: 版本号
|
||||
/// - Returns: 语言代码数组
|
||||
func getProductLanguages(id: String, version: String) -> [String] {
|
||||
guard let product = globalProducts.first(where: { $0.id == id }) else {
|
||||
return []
|
||||
}
|
||||
|
||||
var languages = Set<String>()
|
||||
for platform in product.platforms {
|
||||
for languageSet in platform.languageSet {
|
||||
if languageSet.productVersion == version {
|
||||
languages.insert(languageSet.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array(languages).sorted()
|
||||
}
|
||||
|
||||
/// 查找所有匹配指定ID的产品
|
||||
/// - Parameter id: 产品ID
|
||||
/// - Returns: 匹配的产品数组
|
||||
func findProducts(id: String) -> [Product] {
|
||||
var matchedProducts = [Product]()
|
||||
|
||||
// 从 globalProducts 中查找
|
||||
matchedProducts.append(contentsOf: globalProducts.filter { $0.id == id })
|
||||
|
||||
// 从 globalCcmResult 中查找
|
||||
matchedProducts.append(contentsOf: globalCcmResult.products.filter { $0.id == id })
|
||||
|
||||
// 从 globalStiResult 中查找
|
||||
matchedProducts.append(contentsOf: globalStiResult.products.filter { $0.id == id })
|
||||
|
||||
return matchedProducts
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ struct ContentView: View {
|
||||
@State private var showDownloadManager = false
|
||||
@State private var searchText = ""
|
||||
@State private var currentApiVersion = StorageData.shared.apiVersion
|
||||
@State private var cachedProducts: [UniqueProduct] = []
|
||||
|
||||
private var apiVersion: String {
|
||||
get { StorageData.shared.apiVersion }
|
||||
@@ -17,13 +16,10 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private var filteredProducts: [UniqueProduct] {
|
||||
if searchText.isEmpty {
|
||||
return cachedProducts
|
||||
}
|
||||
if searchText.isEmpty { return globalUniqueProducts }
|
||||
|
||||
return cachedProducts.filter {
|
||||
$0.displayName.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.id.localizedCaseInsensitiveContains(searchText)
|
||||
return globalUniqueProducts.filter {
|
||||
$0.displayName.localizedCaseInsensitiveContains(searchText) || $0.id.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,22 +27,14 @@ struct ContentView: View {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
}
|
||||
|
||||
private func updateProductsCache() {
|
||||
// 先获取有效的产品
|
||||
let validProducts = globalCcmResult.products
|
||||
.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
|
||||
private func refreshData() {
|
||||
isRefreshing = true
|
||||
errorMessage = nil
|
||||
|
||||
// 使用字典合并相同ID的产品
|
||||
var uniqueProductsDict = [String: UniqueProduct]()
|
||||
for product in validProducts {
|
||||
uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName)
|
||||
Task {
|
||||
await globalNetworkManager.fetchProducts()
|
||||
await MainActor.run { isRefreshing = false }
|
||||
}
|
||||
|
||||
// 转换为数组并按显示名称排序
|
||||
let uniqueProducts = Array(uniqueProductsDict.values)
|
||||
.sorted { $0.displayName < $1.displayName }
|
||||
|
||||
cachedProducts = uniqueProducts
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -60,9 +48,7 @@ struct ContentView: View {
|
||||
await globalNetworkManager.fetchProducts()
|
||||
}
|
||||
}
|
||||
)) {
|
||||
Text("Apple Silicon")
|
||||
}
|
||||
)) { Text("Apple Silicon") }
|
||||
.toggleStyle(.switch)
|
||||
.tint(.green)
|
||||
.disabled(isRefreshing)
|
||||
@@ -137,7 +123,7 @@ struct ContentView: View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader")
|
||||
Text("Adobe Downloader 完全免费: https://github.com/X1a0He/Adobe-Downloader")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.horizontal)
|
||||
@@ -204,8 +190,8 @@ struct ContentView: View {
|
||||
],
|
||||
spacing: 20
|
||||
) {
|
||||
ForEach(filteredProducts, id: \.sapCode) { sap in
|
||||
AppCardView(sap: sap)
|
||||
ForEach(filteredProducts, id: \.id) { uniqueProduct in
|
||||
AppCardView(uniqueProduct: uniqueProduct)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -224,63 +210,30 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showDownloadManager) {
|
||||
DownloadManagerView()
|
||||
.environmentObject(globalNetworkManager)
|
||||
}
|
||||
.onAppear {
|
||||
if globalCcmResult.products.isEmpty {
|
||||
refreshData()
|
||||
} else {
|
||||
updateProductsCache()
|
||||
}
|
||||
}
|
||||
.onChange(of: globalNetworkManager.saps) { _ in
|
||||
updateProductsCache()
|
||||
}
|
||||
.sheet(isPresented: $showDownloadManager) { DownloadManagerView() }
|
||||
.onAppear { if globalCcmResult.products.isEmpty { refreshData() } }
|
||||
}
|
||||
|
||||
private func refreshData() {
|
||||
isRefreshing = true
|
||||
errorMessage = nil
|
||||
struct SearchField: View {
|
||||
@Binding var text: String
|
||||
|
||||
Task {
|
||||
await globalNetworkManager.fetchProducts()
|
||||
await MainActor.run {
|
||||
updateProductsCache()
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchField: View {
|
||||
@Binding var text: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
TextField("搜索应用", text: $text)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
if !text.isEmpty {
|
||||
Button(action: { text = "" }) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
TextField("搜索应用", text: $text)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
if !text.isEmpty {
|
||||
Button(action: { text = "" }) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let networkManager = NetworkManager()
|
||||
|
||||
return ContentView()
|
||||
.environmentObject(networkManager)
|
||||
.frame(width: 792, height: 600)
|
||||
}
|
||||
|
||||
@@ -55,13 +55,9 @@ class NewNetworkService {
|
||||
|
||||
let products = globalCcmResult.products
|
||||
|
||||
if products.isEmpty {
|
||||
return ([], [])
|
||||
}
|
||||
if products.isEmpty { return ([], []) }
|
||||
|
||||
let validProducts = products.filter {
|
||||
$0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform)
|
||||
}
|
||||
let validProducts = products.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
|
||||
|
||||
var uniqueProductsDict = [String: UniqueProduct]()
|
||||
for product in validProducts {
|
||||
|
||||
@@ -49,16 +49,10 @@ class NetworkManager: ObservableObject {
|
||||
func fetchProducts() async {
|
||||
loadingState = .loading
|
||||
do {
|
||||
let (saps, sapCodes) = try await globalNetworkService.fetchProductsData()
|
||||
|
||||
let (newProducts, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
||||
print("新产品数量: \(newProducts.count), 唯一产品数量: \(uniqueProducts.count), CDN: \(globalCdn)")
|
||||
for uniqueProduct in uniqueProducts {
|
||||
print("新唯一产品: \(uniqueProduct)")
|
||||
}
|
||||
|
||||
|
||||
let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
||||
await MainActor.run {
|
||||
globalProducts = products
|
||||
globalUniqueProducts = uniqueProducts.sorted { $0.displayName < $1.displayName }
|
||||
self.loadingState = .success
|
||||
}
|
||||
} catch {
|
||||
@@ -153,8 +147,10 @@ class NetworkManager: ObservableObject {
|
||||
|
||||
while retryCount < maxRetries {
|
||||
do {
|
||||
let (saps, sapCodes) = try await globalNetworkService.fetchProductsData()
|
||||
let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
||||
await MainActor.run {
|
||||
globalProducts = products
|
||||
globalUniqueProducts = uniqueProducts
|
||||
self.loadingState = .success
|
||||
self.isFetchingProducts = false
|
||||
}
|
||||
@@ -290,18 +286,18 @@ class NetworkManager: ObservableObject {
|
||||
return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid)
|
||||
}
|
||||
|
||||
func isVersionDownloaded(product: Product, version: String, language: String) -> URL? {
|
||||
func isVersionDownloaded(productId: String, version: String, language: String) -> URL? {
|
||||
if let task = downloadTasks.first(where: {
|
||||
$0.sapCode == sap.sapCode &&
|
||||
$0.version == version &&
|
||||
$0.productId == productId &&
|
||||
$0.productVersion == version &&
|
||||
$0.language == language &&
|
||||
!$0.status.isCompleted
|
||||
}) { return task.directory }
|
||||
|
||||
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
||||
let fileName = sap.sapCode == "APRO"
|
||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
||||
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
||||
let platform = globalProducts.first(where: { $0.id == productId })?.platforms.first?.id ?? "unknown"
|
||||
let fileName = productId == "APRO"
|
||||
? "Adobe Downloader \(productId)_\(version)_\(platform).dmg"
|
||||
: "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)"
|
||||
|
||||
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
||||
let defaultPath = URL(fileURLWithPath: defaultDirectory)
|
||||
@@ -344,7 +340,7 @@ class NetworkManager: ObservableObject {
|
||||
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
||||
await MainActor.run {
|
||||
for task in savedTasks {
|
||||
for product in task.productsToDownload {
|
||||
for product in task.dependenciesToDownload {
|
||||
product.updateCompletedPackages()
|
||||
}
|
||||
}
|
||||
@@ -383,7 +379,7 @@ class NetworkManager: ObservableObject {
|
||||
for task in downloadTasks {
|
||||
if case .paused(let info) = task.status,
|
||||
info.reason == .networkIssue {
|
||||
await downloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,7 +387,7 @@ class NetworkManager: ObservableObject {
|
||||
private func pauseActiveTasks() async {
|
||||
for task in downloadTasks {
|
||||
if case .downloading = task.status {
|
||||
await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
|
||||
await globalNewDownloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,9 @@ class NewDownloadUtils {
|
||||
if condition.contains("[OSArchitecture]==\(AppStatics.architectureSymbol)") {
|
||||
shouldDownload = true
|
||||
}
|
||||
if condition.contains("[OSArchitecture]==x64") {
|
||||
shouldDownload = true
|
||||
}
|
||||
if condition.contains(installLanguage) || task.language == "ALL" {
|
||||
shouldDownload = true
|
||||
}
|
||||
@@ -311,7 +314,6 @@ class NewDownloadUtils {
|
||||
if !FileManager.default.fileExists(atPath: driverPath.path) {
|
||||
if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) {
|
||||
let driverXml = generateDriverXML(
|
||||
sapCode: task.productId,
|
||||
version: task.productVersion,
|
||||
language: task.language,
|
||||
productInfo: productInfo,
|
||||
@@ -412,19 +414,19 @@ class NewDownloadUtils {
|
||||
|
||||
func handleError(_ taskId: UUID, _ error: Error) async {
|
||||
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
|
||||
guard task != nil else { return }
|
||||
guard let task = task else { return }
|
||||
|
||||
let (errorMessage, isRecoverable) = classifyError(error)
|
||||
|
||||
if isRecoverable,
|
||||
let downloadTask = await globalCancelTracker?.downloadTasks[taskId] {
|
||||
let downloadTask = await globalCancelTracker.downloadTasks[taskId] {
|
||||
let resumeData = await withCheckedContinuation { continuation in
|
||||
downloadTask.cancel(byProducingResumeData: { data in
|
||||
continuation.resume(returning: data)
|
||||
})
|
||||
}
|
||||
if let resumeData = resumeData {
|
||||
await globalCancelTracker?.storeResumeData(taskId, data: resumeData)
|
||||
await globalCancelTracker.storeResumeData(taskId, data: resumeData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +443,7 @@ class NewDownloadUtils {
|
||||
Task {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
|
||||
if await !(globalCancelTracker?.isCancelled(taskId) ?? false) {
|
||||
if await globalCancelTracker.isCancelled(taskId) == false {
|
||||
await resumeDownloadTask(taskId: taskId)
|
||||
}
|
||||
} catch {
|
||||
@@ -458,7 +460,7 @@ class NewDownloadUtils {
|
||||
|
||||
if let currentPackage = task.currentPackage {
|
||||
let destinationDir = task.directory
|
||||
.appendingPathComponent("\(task.productId)")
|
||||
.appendingPathComponent("\(task.productId ?? "")")
|
||||
let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
@@ -473,16 +475,18 @@ class NewDownloadUtils {
|
||||
|
||||
func resumeDownloadTask(taskId: UUID) async {
|
||||
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
|
||||
guard task != nil else { return }
|
||||
guard let task = task else { return }
|
||||
|
||||
await MainActor.run {
|
||||
let totalPackages = task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }
|
||||
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
|
||||
fileName: task.currentPackage?.fullPackageName ?? "",
|
||||
currentPackageIndex: 0,
|
||||
totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count },
|
||||
totalPackages: totalPackages,
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
)))
|
||||
task.objectWillChange.send()
|
||||
}
|
||||
|
||||
await globalNetworkManager.saveTask(task)
|
||||
@@ -491,7 +495,7 @@ class NewDownloadUtils {
|
||||
}
|
||||
|
||||
if task.productId == "APRO" {
|
||||
if let resumeData = await globalCancelTracker?.getResumeData(taskId),
|
||||
if let resumeData = await globalCancelTracker.getResumeData(taskId),
|
||||
let currentPackage = task.currentPackage,
|
||||
let product = task.dependenciesToDownload.first {
|
||||
try? await downloadPackage(
|
||||
@@ -598,9 +602,9 @@ class NewDownloadUtils {
|
||||
|
||||
product.updateCompletedPackages()
|
||||
}
|
||||
await globalNetworkManager.saveTask(task)
|
||||
await globalNetworkManager?.saveTask(task)
|
||||
await MainActor.run {
|
||||
globalNetworkManager.objectWillChange.send()
|
||||
globalNetworkManager?.objectWillChange.send()
|
||||
}
|
||||
continuation.resume()
|
||||
}
|
||||
@@ -643,7 +647,7 @@ class NewDownloadUtils {
|
||||
lastUpdateTime = now
|
||||
lastBytes = totalBytesWritten
|
||||
|
||||
globalNetworkManager.objectWillChange.send()
|
||||
globalNetworkManager?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,27 +668,27 @@ class NewDownloadUtils {
|
||||
return
|
||||
}
|
||||
|
||||
await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session)
|
||||
await globalCancelTracker?.clearResumeData(task.id)
|
||||
await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||
await globalCancelTracker.clearResumeData(task.id)
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Product, displayName: String) -> String {
|
||||
func generateDriverXML(version: String, language: String, productInfo: Product, displayName: String) -> String {
|
||||
// 获取匹配的 platform 和 languageSet
|
||||
guard let platform = productInfo.platforms.first(where: { $0.id == "mac" }),
|
||||
let languageSet = platform.languageSet.first else {
|
||||
guard let platform = globalProducts.first(where: { $0.id == productInfo.id })?.platforms.first?.id,
|
||||
let languageSet = globalProducts.first(where: { $0.id == productInfo.id })?.platforms.first?.languageSet else {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 构建依赖列表
|
||||
let dependencies = languageSet.dependencies.map { dependency in
|
||||
let dependencies = languageSet.first?.dependencies.map { dependency in
|
||||
"""
|
||||
<Dependency>
|
||||
<SAPCode>\(dependency.sapCode)</SAPCode>
|
||||
<BaseVersion>\(dependency.baseVersion)</BaseVersion>
|
||||
<EsdDirectory>\(dependency.sapCode)</EsdDirectory>
|
||||
<SAPCode>\(productInfo.id)</SAPCode>
|
||||
<BaseVersion>\(languageSet.first?.baseVersion)</BaseVersion>
|
||||
<EsdDirectory>\(productInfo.id)</EsdDirectory>
|
||||
</Dependency>
|
||||
"""
|
||||
}.joined(separator: "\n")
|
||||
@@ -693,10 +697,10 @@ class NewDownloadUtils {
|
||||
<DriverInfo>
|
||||
<ProductInfo>
|
||||
<n>Adobe \(displayName)</n>
|
||||
<SAPCode>\(sapCode)</SAPCode>
|
||||
<SAPCode>\(productInfo.id)</SAPCode>
|
||||
<CodexVersion>\(version)</CodexVersion>
|
||||
<Platform>mac</Platform>
|
||||
<EsdDirectory>\(sapCode)</EsdDirectory>
|
||||
<EsdDirectory>\(productInfo.id)</EsdDirectory>
|
||||
<Dependencies>
|
||||
\(dependencies)
|
||||
</Dependencies>
|
||||
@@ -811,11 +815,11 @@ class NewDownloadUtils {
|
||||
task.objectWillChange.send()
|
||||
}
|
||||
|
||||
await globalNetworkManager.saveTask(task)
|
||||
await globalNetworkManager?.saveTask(task)
|
||||
|
||||
await MainActor.run {
|
||||
globalNetworkManager.updateDockBadge()
|
||||
globalNetworkManager.objectWillChange.send()
|
||||
globalNetworkManager?.updateDockBadge()
|
||||
globalNetworkManager?.objectWillChange.send()
|
||||
}
|
||||
continuation.resume()
|
||||
}
|
||||
@@ -842,10 +846,10 @@ class NewDownloadUtils {
|
||||
lastBytes = totalBytesWritten
|
||||
|
||||
task.objectWillChange.send()
|
||||
globalNetworkManager.objectWillChange.send()
|
||||
globalNetworkManager?.objectWillChange.send()
|
||||
|
||||
Task {
|
||||
await globalNetworkManager.saveTask(task)
|
||||
await globalNetworkManager?.saveTask(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -860,20 +864,19 @@ class NewDownloadUtils {
|
||||
let downloadTask = session.downloadTask(with: downloadRequest)
|
||||
|
||||
Task {
|
||||
await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session)
|
||||
await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||
|
||||
if await (globalCancelTracker?.isCancelled(task.id) ?? false) {
|
||||
if await globalCancelTracker.isCancelled(task.id) {
|
||||
continuation.resume(throwing: NetworkError.cancelled)
|
||||
return
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
|
||||
let task = await globalCancelTracker?.downloadTasks[taskId]
|
||||
let task = await globalCancelTracker.downloadTasks[taskId]
|
||||
if let downloadTask = task {
|
||||
let data = await withCheckedContinuation { continuation in
|
||||
downloadTask.cancel(byProducingResumeData: { data in
|
||||
@@ -881,7 +884,7 @@ class NewDownloadUtils {
|
||||
})
|
||||
}
|
||||
if let data = data {
|
||||
await globalCancelTracker?.storeResumeData(taskId, data: data)
|
||||
await globalCancelTracker.storeResumeData(taskId, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,4 +947,223 @@ class NewDownloadUtils {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func executePrivilegedCommand(_ command: String) async -> String {
|
||||
return await withCheckedContinuation { continuation in
|
||||
PrivilegedHelperManager.shared.executeCommand(command) { result in
|
||||
if result.starts(with: "Error:") {
|
||||
print("命令执行失败: \(command)")
|
||||
print("错误信息: \(result)")
|
||||
}
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadX1a0HeCCPackages(
|
||||
progressHandler: @escaping (Double, String) -> Void,
|
||||
cancellationHandler: @escaping () -> Bool,
|
||||
shouldProcess: Bool = true
|
||||
) async throws {
|
||||
let baseUrl = "https://cdn-ffc.oobesaas.adobe.com/core/v1/applications?name=CreativeCloud&platform=\(AppStatics.isAppleSilicon ? "macarm64" : "osx10")"
|
||||
|
||||
let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = 30
|
||||
configuration.timeoutIntervalForResource = 300
|
||||
configuration.httpAdditionalHeaders = NetworkConstants.downloadHeaders
|
||||
let session = URLSession(configuration: configuration)
|
||||
|
||||
do {
|
||||
var request = URLRequest(url: URL(string: baseUrl)!)
|
||||
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
let xmlDoc = try XMLDocument(data: data)
|
||||
|
||||
let packageSets = try xmlDoc.nodes(forXPath: "//packageSet[name='ADC']")
|
||||
guard let adcPackageSet = packageSets.first else {
|
||||
throw NetworkError.invalidData("找不到ADC包集")
|
||||
}
|
||||
|
||||
let targetPackages = ["HDBox", "IPCBox"]
|
||||
var packagesToDownload: [(name: String, url: URL, size: Int64)] = []
|
||||
|
||||
for packageName in targetPackages {
|
||||
let packageNodes = try adcPackageSet.nodes(forXPath: ".//package[name='\(packageName)']")
|
||||
guard let package = packageNodes.first else {
|
||||
print("未找到包: \(packageName)")
|
||||
continue
|
||||
}
|
||||
|
||||
guard let manifestUrl = try package.nodes(forXPath: ".//manifestUrl").first?.stringValue,
|
||||
let cdnBase = try xmlDoc.nodes(forXPath: "//cdn/secure").first?.stringValue else {
|
||||
print("无法获取manifest URL或CDN基础URL")
|
||||
continue
|
||||
}
|
||||
|
||||
let manifestFullUrl = cdnBase + manifestUrl
|
||||
|
||||
var manifestRequest = URLRequest(url: URL(string: manifestFullUrl)!)
|
||||
NetworkConstants.downloadHeaders.forEach { manifestRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
let (manifestData, manifestResponse) = try await session.data(for: manifestRequest)
|
||||
|
||||
guard let manifestHttpResponse = manifestResponse as? HTTPURLResponse,
|
||||
(200...299).contains(manifestHttpResponse.statusCode) else {
|
||||
print("获取manifest失败: HTTP \(String(describing: (manifestResponse as? HTTPURLResponse)?.statusCode))")
|
||||
continue
|
||||
}
|
||||
|
||||
let manifestDoc = try XMLDocument(data: manifestData)
|
||||
let assetPathNodes = try manifestDoc.nodes(forXPath: "//asset_path")
|
||||
let sizeNodes = try manifestDoc.nodes(forXPath: "//asset_size")
|
||||
guard let assetPath = assetPathNodes.first?.stringValue,
|
||||
let sizeStr = sizeNodes.first?.stringValue,
|
||||
let size = Int64(sizeStr),
|
||||
let downloadUrl = URL(string: assetPath) else {
|
||||
continue
|
||||
}
|
||||
packagesToDownload.append((packageName, downloadUrl, size))
|
||||
}
|
||||
|
||||
guard !packagesToDownload.isEmpty else {
|
||||
throw NetworkError.invalidData("没有找到可下载的包")
|
||||
}
|
||||
|
||||
let totalCount = packagesToDownload.count
|
||||
for (index, package) in packagesToDownload.enumerated() {
|
||||
if cancellationHandler() {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.cancelled
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
progressHandler(Double(index) / Double(totalCount), "正在下载 \(package.name)...")
|
||||
}
|
||||
|
||||
let destinationURL = tempDirectory.appendingPathComponent("\(package.name).zip")
|
||||
var downloadRequest = URLRequest(url: package.url)
|
||||
print(downloadRequest)
|
||||
NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
let (downloadURL, downloadResponse) = try await session.download(for: downloadRequest)
|
||||
|
||||
guard let httpResponse = downloadResponse as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
print("下载失败: HTTP \(String(describing: (downloadResponse as? HTTPURLResponse)?.statusCode))")
|
||||
continue
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: downloadURL, to: destinationURL)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
progressHandler(0.9, shouldProcess ? "正在安装组件..." : "正在完成下载...")
|
||||
}
|
||||
|
||||
let targetDirectory = "/Library/Application\\ Support/Adobe/Adobe\\ Desktop\\ Common"
|
||||
let rawTargetDirectory = "/Library/Application Support/Adobe/Adobe Desktop Common"
|
||||
|
||||
if !FileManager.default.fileExists(atPath: rawTargetDirectory) {
|
||||
let createDirResult = await executePrivilegedCommand("/bin/mkdir -p \(targetDirectory)")
|
||||
if createDirResult.starts(with: "Error:") {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.installError("创建目录失败: \(createDirResult)")
|
||||
}
|
||||
|
||||
let chmodResult = await executePrivilegedCommand("/bin/chmod 755 \(targetDirectory)")
|
||||
if chmodResult.starts(with: "Error:") {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.installError("设置权限失败: \(chmodResult)")
|
||||
}
|
||||
}
|
||||
|
||||
for package in packagesToDownload {
|
||||
let packageDir = "\(targetDirectory)/\(package.name)"
|
||||
|
||||
let removeResult = await executePrivilegedCommand("/bin/rm -rf \(packageDir)")
|
||||
if removeResult.starts(with: "Error:") {
|
||||
print("移除旧目录失败: \(removeResult)")
|
||||
}
|
||||
|
||||
let mkdirResult = await executePrivilegedCommand("/bin/mkdir -p \(packageDir)")
|
||||
if mkdirResult.starts(with: "Error:") {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.installError("创建 \(package.name) 目录失败")
|
||||
}
|
||||
|
||||
let unzipResult = await executePrivilegedCommand("cd \(packageDir) && /usr/bin/unzip -o '\(tempDirectory.path)/\(package.name).zip'")
|
||||
if unzipResult.starts(with: "Error:") {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.installError("解压 \(package.name) 失败: \(unzipResult)")
|
||||
}
|
||||
|
||||
let chmodResult = await executePrivilegedCommand("/bin/chmod -R 755 \(packageDir)")
|
||||
if chmodResult.starts(with: "Error:") {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.installError("设置 \(package.name) 权限失败: \(chmodResult)")
|
||||
}
|
||||
|
||||
let chownResult = await executePrivilegedCommand("/usr/sbin/chown -R root:wheel \(packageDir)")
|
||||
if chownResult.starts(with: "Error:") {
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
throw NetworkError.installError("设置 \(package.name) 所有者失败: \(chownResult)")
|
||||
}
|
||||
}
|
||||
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
|
||||
if shouldProcess {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
ModifySetup.backupAndModifySetupFile { success, message in
|
||||
if success {
|
||||
continuation.resume()
|
||||
} else {
|
||||
continuation.resume(throwing: NetworkError.installError(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
ModifySetup.clearVersionCache()
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(at: tempDirectory)
|
||||
|
||||
await MainActor.run {
|
||||
progressHandler(1.0, shouldProcess ? "安装完成" : "下载完成")
|
||||
}
|
||||
} catch {
|
||||
print("发生错误: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
|
||||
await globalCancelTracker.cancel(taskId)
|
||||
|
||||
if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) {
|
||||
if removeFiles {
|
||||
try? FileManager.default.removeItem(at: task.directory)
|
||||
}
|
||||
|
||||
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
||||
message: String(localized: "下载已取消"),
|
||||
error: NetworkError.downloadCancelled,
|
||||
timestamp: Date(),
|
||||
recoverable: false
|
||||
)))
|
||||
|
||||
await globalNetworkManager.saveTask(task)
|
||||
await MainActor.run {
|
||||
globalNetworkManager.updateDockBadge()
|
||||
globalNetworkManager.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,15 +18,15 @@ class TaskPersistenceManager {
|
||||
self.cancelTracker = tracker
|
||||
}
|
||||
|
||||
private func getTaskFileName(sapCode: String, version: String, language: String, platform: String) -> String {
|
||||
return sapCode == "APRO"
|
||||
? "Adobe Downloader \(sapCode)_\(version)_\(platform)-task.json"
|
||||
: "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json"
|
||||
private func getTaskFileName(productId: String, version: String, language: String, platform: String) -> String {
|
||||
return productId == "APRO"
|
||||
? "Adobe Downloader \(productId)_\(version)_\(platform)-task.json"
|
||||
: "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)-task.json"
|
||||
}
|
||||
|
||||
func saveTask(_ task: NewDownloadTask) async {
|
||||
let fileName = getTaskFileName(
|
||||
sapCode: task.productId,
|
||||
productId: task.productId,
|
||||
version: task.productVersion,
|
||||
language: task.language,
|
||||
platform: task.platform
|
||||
@@ -209,7 +209,7 @@ class TaskPersistenceManager {
|
||||
|
||||
func removeTask(_ task: NewDownloadTask) {
|
||||
let fileName = getTaskFileName(
|
||||
sapCode: task.productId,
|
||||
productId: task.productId,
|
||||
version: task.productVersion,
|
||||
language: task.language,
|
||||
platform: task.platform
|
||||
@@ -220,16 +220,16 @@ class TaskPersistenceManager {
|
||||
try? fileManager.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
func createExistingProgramTask(sapCode: String, version: String, language: String, displayName: String, platform: String, directory: URL) async {
|
||||
func createExistingProgramTask(productId: String, version: String, language: String, displayName: String, platform: String, directory: URL) async {
|
||||
let fileName = getTaskFileName(
|
||||
sapCode: sapCode,
|
||||
productId: productId,
|
||||
version: version,
|
||||
language: language,
|
||||
platform: platform
|
||||
)
|
||||
|
||||
let product = DependenciesToDownload(
|
||||
sapCode: sapCode,
|
||||
sapCode: productId,
|
||||
version: version,
|
||||
buildGuid: "",
|
||||
applicationJson: ""
|
||||
@@ -249,7 +249,7 @@ class TaskPersistenceManager {
|
||||
product.packages = [package]
|
||||
|
||||
let task = NewDownloadTask(
|
||||
productId: sapCode,
|
||||
productId: productId,
|
||||
productVersion: version,
|
||||
language: language,
|
||||
displayName: displayName,
|
||||
|
||||
@@ -277,17 +277,19 @@ struct GeneralSettingsView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
DownloadSettingsView(viewModel: viewModel)
|
||||
HelperSettingsView(viewModel: viewModel,
|
||||
showHelperAlert: $showHelperAlert,
|
||||
helperAlertMessage: $helperAlertMessage,
|
||||
helperAlertSuccess: $helperAlertSuccess)
|
||||
CCSettingsView(viewModel: viewModel)
|
||||
UpdateSettingsView(viewModel: viewModel)
|
||||
CleanConfigView()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
DownloadSettingsView(viewModel: viewModel)
|
||||
HelperSettingsView(viewModel: viewModel,
|
||||
showHelperAlert: $showHelperAlert,
|
||||
helperAlertMessage: $helperAlertMessage,
|
||||
helperAlertSuccess: $helperAlertSuccess)
|
||||
CCSettingsView(viewModel: viewModel)
|
||||
UpdateSettingsView(viewModel: viewModel)
|
||||
CleanConfigView()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(helperAlertSuccess ? "操作成功" : "操作失败", isPresented: $showHelperAlert) {
|
||||
Button("确定") { }
|
||||
@@ -298,28 +300,7 @@ struct GeneralSettingsView: View {
|
||||
Button("取消", role: .cancel) { }
|
||||
Button("下载") {
|
||||
Task {
|
||||
viewModel.isDownloadingSetup = true
|
||||
viewModel.isCancelled = false
|
||||
do {
|
||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
||||
progressHandler: { progress, status in
|
||||
viewModel.setupDownloadProgress = progress
|
||||
viewModel.setupDownloadStatus = status
|
||||
},
|
||||
cancellationHandler: { viewModel.isCancelled }
|
||||
)
|
||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
||||
viewModel.isSuccess = true
|
||||
viewModel.alertMessage = String(localized: "Setup 组件安装成功")
|
||||
} catch NetworkError.cancelled {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = String(localized: "下载已取消")
|
||||
} catch {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = error.localizedDescription
|
||||
}
|
||||
viewModel.showAlert = true
|
||||
viewModel.isDownloadingSetup = false
|
||||
await downloadSetup(shouldProcess: false)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
@@ -329,29 +310,7 @@ struct GeneralSettingsView: View {
|
||||
Button("取消", role: .cancel) { }
|
||||
Button("确定") {
|
||||
Task {
|
||||
viewModel.isDownloadingSetup = true
|
||||
viewModel.isCancelled = false
|
||||
do {
|
||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
||||
progressHandler: { progress, status in
|
||||
viewModel.setupDownloadProgress = progress
|
||||
viewModel.setupDownloadStatus = status
|
||||
},
|
||||
cancellationHandler: { viewModel.isCancelled },
|
||||
shouldProcess: true
|
||||
)
|
||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
||||
viewModel.isSuccess = true
|
||||
viewModel.alertMessage = String(localized: "X1a0He CC 下载并处理成功")
|
||||
} catch NetworkError.cancelled {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = String(localized: "下载已取消")
|
||||
} catch {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = error.localizedDescription
|
||||
}
|
||||
viewModel.showAlert = true
|
||||
viewModel.isDownloadingSetup = false
|
||||
await downloadSetup(shouldProcess: true)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
@@ -361,29 +320,7 @@ struct GeneralSettingsView: View {
|
||||
Button("取消", role: .cancel) { }
|
||||
Button("确定") {
|
||||
Task {
|
||||
viewModel.isDownloadingSetup = true
|
||||
viewModel.isCancelled = false
|
||||
do {
|
||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
||||
progressHandler: { progress, status in
|
||||
viewModel.setupDownloadProgress = progress
|
||||
viewModel.setupDownloadStatus = status
|
||||
},
|
||||
cancellationHandler: { viewModel.isCancelled },
|
||||
shouldProcess: false
|
||||
)
|
||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
||||
viewModel.isSuccess = true
|
||||
viewModel.alertMessage = String(localized: "X1a0He CC 下载成功")
|
||||
} catch NetworkError.cancelled {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = String(localized: "下载已取消")
|
||||
} catch {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = error.localizedDescription
|
||||
}
|
||||
viewModel.showAlert = true
|
||||
viewModel.isDownloadingSetup = false
|
||||
await downloadSetup(shouldProcess: false)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
@@ -401,6 +338,34 @@ struct GeneralSettingsView: View {
|
||||
viewModel.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadSetup(shouldProcess: Bool) async {
|
||||
viewModel.isDownloadingSetup = true
|
||||
viewModel.isCancelled = false
|
||||
do {
|
||||
try await globalNewDownloadUtils.downloadX1a0HeCCPackages(
|
||||
progressHandler: { progress, status in
|
||||
viewModel.setupDownloadProgress = progress
|
||||
viewModel.setupDownloadStatus = status
|
||||
},
|
||||
cancellationHandler: { viewModel.isCancelled },
|
||||
shouldProcess: shouldProcess
|
||||
)
|
||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
||||
viewModel.isSuccess = true
|
||||
viewModel.alertMessage = shouldProcess ?
|
||||
String(localized: "X1a0He CC 下载并处理成功") :
|
||||
String(localized: "X1a0He CC 下载成功")
|
||||
} catch NetworkError.cancelled {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = String(localized: "下载已取消")
|
||||
} catch {
|
||||
viewModel.isSuccess = false
|
||||
viewModel.alertMessage = error.localizedDescription
|
||||
}
|
||||
viewModel.showAlert = true
|
||||
viewModel.isDownloadingSetup = false
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadSettingsView: View {
|
||||
|
||||
@@ -51,8 +51,7 @@ final class AppCardViewModel: ObservableObject {
|
||||
@Published var pendingLanguage = ""
|
||||
@Published var showRedownloadConfirm = false
|
||||
|
||||
let sap: Sap
|
||||
weak var networkManager: NetworkManager?
|
||||
let uniqueProduct: UniqueProduct
|
||||
|
||||
@Published var isDownloading = false
|
||||
private let userDefaults = UserDefaults.standard
|
||||
@@ -67,9 +66,8 @@ final class AppCardViewModel: ObservableObject {
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(sap: Sap, networkManager: NetworkManager?) {
|
||||
self.sap = sap
|
||||
self.networkManager = networkManager
|
||||
init(uniqueProduct: UniqueProduct) {
|
||||
self.uniqueProduct = uniqueProduct
|
||||
|
||||
Task { @MainActor in
|
||||
setupObservers()
|
||||
@@ -78,12 +76,12 @@ final class AppCardViewModel: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
private func setupObservers() {
|
||||
networkManager?.$downloadTasks
|
||||
globalNetworkManager.$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)
|
||||
$0.productId == self.uniqueProduct.id && self.isTaskActive($0.status)
|
||||
}
|
||||
|
||||
if hasActiveTask != self.isDownloading {
|
||||
@@ -93,7 +91,7 @@ final class AppCardViewModel: ObservableObject {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
networkManager?.objectWillChange
|
||||
globalNetworkManager.objectWillChange
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateDownloadingStatus()
|
||||
@@ -114,13 +112,8 @@ final class AppCardViewModel: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
func updateDownloadingStatus() {
|
||||
guard let networkManager = networkManager else {
|
||||
self.isDownloading = false
|
||||
return
|
||||
}
|
||||
|
||||
let hasActiveTask = networkManager.downloadTasks.contains {
|
||||
$0.sapCode == sap.sapCode && isTaskActive($0.status)
|
||||
let hasActiveTask = globalNetworkManager.downloadTasks.contains {
|
||||
$0.productId == uniqueProduct.id && isTaskActive($0.status)
|
||||
}
|
||||
|
||||
if hasActiveTask != self.isDownloading {
|
||||
@@ -130,10 +123,10 @@ final class AppCardViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func getDestinationURL(version: String, language: String) async throws -> URL {
|
||||
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
||||
let installerName = sap.sapCode == "APRO"
|
||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
||||
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
||||
let platform = globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.id
|
||||
let installerName = uniqueProduct.id == "APRO"
|
||||
? "Adobe Downloader \(uniqueProduct.id)_\(version)_\(platform).dmg"
|
||||
: "Adobe Downloader \(uniqueProduct.id)_\(version)-\(language)-\(platform)"
|
||||
|
||||
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
||||
return URL(fileURLWithPath: defaultDirectory)
|
||||
@@ -167,10 +160,9 @@ final class AppCardViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func loadIcon() {
|
||||
if let bestIcon = sap.getBestIcon(),
|
||||
let iconURL = URL(string: bestIcon.url) {
|
||||
|
||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
|
||||
if let bestIcon = globalProducts.first(where: { $0.id == uniqueProduct.id })?.getBestIcon(),
|
||||
let iconURL = URL(string: bestIcon.value) {
|
||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) {
|
||||
self.iconImage = cachedImage
|
||||
return
|
||||
}
|
||||
@@ -189,20 +181,20 @@ final class AppCardViewModel: ObservableObject {
|
||||
|
||||
await MainActor.run {
|
||||
if let image = NSImage(data: data) {
|
||||
IconCache.shared.setIcon(image, for: bestIcon.url)
|
||||
IconCache.shared.setIcon(image, for: bestIcon.value)
|
||||
self.iconImage = image
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
if let localImage = NSImage(named: sap.sapCode) {
|
||||
if let localImage = NSImage(named: uniqueProduct.id) {
|
||||
self.iconImage = localImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let localImage = NSImage(named: sap.sapCode) {
|
||||
if let localImage = NSImage(named: uniqueProduct.id) {
|
||||
self.iconImage = localImage
|
||||
}
|
||||
}
|
||||
@@ -222,37 +214,32 @@ final class AppCardViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func checkAndStartDownload(version: String, language: String) async {
|
||||
if let networkManager = networkManager {
|
||||
if let existingPath = networkManager.isVersionDownloaded(sap: sap, version: version, language: language) {
|
||||
await MainActor.run {
|
||||
existingFilePath = existingPath
|
||||
pendingVersion = version
|
||||
pendingLanguage = language
|
||||
showExistingFileAlert = true
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
let destinationURL = try await getDestinationURL(version: version, language: language)
|
||||
try await networkManager.startDownload(
|
||||
sap: sap,
|
||||
selectedVersion: version,
|
||||
language: language,
|
||||
destinationURL: destinationURL
|
||||
)
|
||||
} catch {
|
||||
handleError(error)
|
||||
}
|
||||
if let existingPath = globalNetworkManager.isVersionDownloaded(productId: uniqueProduct.id, version: version, language: language) {
|
||||
await MainActor.run {
|
||||
existingFilePath = existingPath
|
||||
pendingVersion = version
|
||||
pendingLanguage = language
|
||||
showExistingFileAlert = true
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
let destinationURL = try await getDestinationURL(version: version, language: language)
|
||||
try await globalNetworkManager.startDownload(
|
||||
productId: uniqueProduct.id,
|
||||
selectedVersion: version,
|
||||
language: language,
|
||||
destinationURL: destinationURL
|
||||
)
|
||||
} catch {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createCompletedTask(_ path: URL) async {
|
||||
guard let networkManager = networkManager,
|
||||
let productInfo = sap.versions[pendingVersion] else { return }
|
||||
|
||||
let existingTask = networkManager.downloadTasks.first { task in
|
||||
return task.sapCode == sap.sapCode &&
|
||||
task.version == pendingVersion &&
|
||||
let existingTask = globalNetworkManager.downloadTasks.first { task in
|
||||
return task.productId == uniqueProduct.id &&
|
||||
task.productVersion == pendingVersion &&
|
||||
task.language == pendingLanguage &&
|
||||
task.directory == path
|
||||
}
|
||||
@@ -262,27 +249,24 @@ final class AppCardViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
await TaskPersistenceManager.shared.createExistingProgramTask(
|
||||
sapCode: sap.sapCode,
|
||||
productId: uniqueProduct.id,
|
||||
version: pendingVersion,
|
||||
language: pendingLanguage,
|
||||
displayName: sap.displayName,
|
||||
platform: productInfo.apPlatform,
|
||||
displayName: uniqueProduct.displayName,
|
||||
platform: globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.id ?? "unknown",
|
||||
directory: path
|
||||
)
|
||||
|
||||
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
||||
await MainActor.run {
|
||||
networkManager.downloadTasks = savedTasks
|
||||
networkManager.updateDockBadge()
|
||||
networkManager.objectWillChange.send()
|
||||
globalNetworkManager.downloadTasks = savedTasks
|
||||
globalNetworkManager.updateDockBadge()
|
||||
globalNetworkManager.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
var dependenciesCount: Int {
|
||||
if let firstVersion = sap.versions.first?.value {
|
||||
return firstVersion.dependencies.count
|
||||
}
|
||||
return 0
|
||||
return globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.languageSet.first?.dependencies.count ?? 0
|
||||
}
|
||||
|
||||
var hasValidIcon: Bool {
|
||||
@@ -304,12 +288,11 @@ final class AppCardViewModel: ObservableObject {
|
||||
|
||||
struct AppCardView: View {
|
||||
@StateObject private var viewModel: AppCardViewModel
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||
|
||||
init(sap: Sap) {
|
||||
_viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil))
|
||||
init(uniqueProduct: UniqueProduct) {
|
||||
_viewModel = StateObject(wrappedValue: AppCardViewModel(uniqueProduct: uniqueProduct))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -322,18 +305,19 @@ struct AppCardView: View {
|
||||
}
|
||||
}
|
||||
.modifier(CardModifier())
|
||||
.modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager))
|
||||
.modifier(SheetModifier(viewModel: viewModel))
|
||||
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
|
||||
.onAppear(perform: setupViewModel)
|
||||
.onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus)
|
||||
.onChange(of: globalNetworkManager.downloadTasks.count) { _ in
|
||||
updateDownloadStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupViewModel() {
|
||||
viewModel.networkManager = networkManager
|
||||
viewModel.updateDownloadingStatus()
|
||||
}
|
||||
|
||||
private func updateDownloadStatus(_ _: [NewDownloadTask]) {
|
||||
private func updateDownloadStatus() {
|
||||
viewModel.updateDownloadingStatus()
|
||||
}
|
||||
}
|
||||
@@ -379,16 +363,19 @@ private struct ProductInfoView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(viewModel.sap.displayName)
|
||||
Text(viewModel.uniqueProduct.displayName)
|
||||
.font(.system(size: AppCardConstants.titleFontSize))
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text("可用版本: \(viewModel.sap.versions.count)")
|
||||
let product = findProduct(id: viewModel.uniqueProduct.id)
|
||||
let versions = Set(product?.platforms.first?.languageSet.map { $0.productVersion } ?? [])
|
||||
let dependenciesCount = product?.platforms.first?.languageSet.first?.dependencies.count ?? 0
|
||||
Text("可用版本: \(versions.count)")
|
||||
Text("|")
|
||||
Text("依赖包: \(viewModel.dependenciesCount)")
|
||||
Text("依赖包: \(dependenciesCount)")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -437,23 +424,23 @@ private struct CardModifier: ViewModifier {
|
||||
|
||||
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
|
||||
)
|
||||
if let product = findProduct(id: viewModel.uniqueProduct.id) {
|
||||
VersionPickerView(product: product) { version in
|
||||
Task {
|
||||
await viewModel.handleDownloadRequest(
|
||||
version,
|
||||
useDefaultLanguage: useDefaultLanguage,
|
||||
defaultLanguage: defaultLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.environmentObject(networkManager)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showLanguagePicker) {
|
||||
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
||||
@@ -482,10 +469,9 @@ struct AlertModifier: ViewModifier {
|
||||
viewModel.showExistingFileAlert = false
|
||||
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
||||
Task {
|
||||
if let networkManager = viewModel.networkManager,
|
||||
!networkManager.downloadTasks.contains(where: { task in
|
||||
task.sapCode == viewModel.sap.sapCode &&
|
||||
task.version == viewModel.pendingVersion &&
|
||||
if !globalNetworkManager.downloadTasks.contains(where: { task in
|
||||
task.productId == viewModel.uniqueProduct.id &&
|
||||
task.productVersion == viewModel.pendingVersion &&
|
||||
task.language == viewModel.pendingLanguage
|
||||
}) {
|
||||
await viewModel.createCompletedTask(path)
|
||||
@@ -540,12 +526,10 @@ struct AlertModifier: ViewModifier {
|
||||
}
|
||||
|
||||
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 &&
|
||||
globalNetworkManager.downloadTasks.removeAll { task in
|
||||
task.productId == viewModel.uniqueProduct.id &&
|
||||
task.productVersion == viewModel.pendingVersion &&
|
||||
task.language == viewModel.pendingLanguage
|
||||
}
|
||||
|
||||
@@ -558,8 +542,8 @@ struct AlertModifier: ViewModifier {
|
||||
language: viewModel.pendingLanguage
|
||||
)
|
||||
|
||||
try await networkManager.startDownload(
|
||||
sap: viewModel.sap,
|
||||
try await globalNetworkManager.startDownload(
|
||||
productId: viewModel.uniqueProduct.id,
|
||||
selectedVersion: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage,
|
||||
destinationURL: destinationURL
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadManagerView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var sortOrder: SortOrder = .addTime
|
||||
@@ -27,7 +26,7 @@ struct DownloadManagerView: View {
|
||||
}
|
||||
|
||||
private func removeTask(_ task: NewDownloadTask) {
|
||||
networkManager.removeTask(taskId: task.id)
|
||||
globalNetworkManager.removeTask(taskId: task.id)
|
||||
}
|
||||
|
||||
private func sortTasks(_ tasks: [NewDownloadTask]) -> [NewDownloadTask] {
|
||||
@@ -47,123 +46,163 @@ struct DownloadManagerView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("下载管理")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
HStack(){
|
||||
Menu {
|
||||
ForEach([SortOrder.addTime, .name, .status], id: \.self) { order in
|
||||
Button(action: {
|
||||
sortOrder = order
|
||||
}) {
|
||||
HStack {
|
||||
Text(order.description)
|
||||
if sortOrder == order {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
Text(sortOrder.description)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 120)
|
||||
.fixedSize()
|
||||
|
||||
Button("全部暂停") {
|
||||
Task {
|
||||
for task in networkManager.downloadTasks {
|
||||
if case .downloading = task.status {
|
||||
await networkManager.downloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: .userRequested
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button("全部继续") {
|
||||
Task {
|
||||
for task in networkManager.downloadTasks {
|
||||
if case .paused = task.status {
|
||||
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button("清理已完成") {
|
||||
networkManager.downloadTasks.removeAll { task in
|
||||
if case .completed = task.status {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
networkManager.updateDockBadge()
|
||||
}
|
||||
|
||||
Button("关闭") {
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(sortTasks(networkManager.downloadTasks)) { task in
|
||||
DownloadProgressView(
|
||||
task: task,
|
||||
onCancel: {
|
||||
Task {
|
||||
await networkManager.downloadUtils.cancelDownloadTask(taskId: task.id)
|
||||
}
|
||||
},
|
||||
onPause: {
|
||||
Task {
|
||||
await networkManager.downloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: .userRequested
|
||||
)
|
||||
}
|
||||
},
|
||||
onResume: {
|
||||
Task {
|
||||
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
},
|
||||
onRetry: {
|
||||
Task {
|
||||
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
},
|
||||
onRemove: {
|
||||
removeTask(task)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
DownloadManagerToolbar(
|
||||
sortOrder: $sortOrder,
|
||||
dismiss: dismiss
|
||||
)
|
||||
DownloadTaskList(
|
||||
tasks: sortTasks(globalNetworkManager.downloadTasks),
|
||||
removeTask: removeTask
|
||||
)
|
||||
}
|
||||
.frame(width:800, height: 600)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadManagerToolbar: View {
|
||||
@Binding var sortOrder: DownloadManagerView.SortOrder
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("下载管理")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
SortMenuView(sortOrder: $sortOrder)
|
||||
.frame(minWidth: 120)
|
||||
.fixedSize()
|
||||
|
||||
ToolbarButtons(dismiss: dismiss)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ToolbarButtons: View {
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Button("全部暂停") {
|
||||
Task {
|
||||
for task in globalNetworkManager.downloadTasks {
|
||||
if case .downloading = task.status {
|
||||
await globalNewDownloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: .userRequested
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button("全部继续") {
|
||||
Task {
|
||||
for task in globalNetworkManager.downloadTasks {
|
||||
if case .paused = task.status {
|
||||
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button("清理已完成") {
|
||||
globalNetworkManager.downloadTasks.removeAll { task in
|
||||
if case .completed = task.status {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
globalNetworkManager.updateDockBadge()
|
||||
}
|
||||
|
||||
Button("关闭") {
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadTaskList: View {
|
||||
let tasks: [NewDownloadTask]
|
||||
let removeTask: (NewDownloadTask) -> Void
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(tasks) { task in
|
||||
DownloadProgressView(
|
||||
task: task,
|
||||
onCancel: { TaskOperations.cancelTask(task) },
|
||||
onPause: { TaskOperations.pauseTask(task) },
|
||||
onResume: { TaskOperations.resumeTask(task) },
|
||||
onRetry: { TaskOperations.resumeTask(task) },
|
||||
onRemove: { removeTask(task) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
private enum TaskOperations {
|
||||
static func cancelTask(_ task: NewDownloadTask) {
|
||||
Task {
|
||||
await globalNewDownloadUtils.cancelDownloadTask(taskId: task.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func pauseTask(_ task: NewDownloadTask) {
|
||||
Task {
|
||||
await globalNewDownloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: .userRequested
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func resumeTask(_ task: NewDownloadTask) {
|
||||
Task {
|
||||
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadManagerView.SortOrder: Hashable {}
|
||||
|
||||
struct SortMenuView: View {
|
||||
@Binding var sortOrder: DownloadManagerView.SortOrder
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
ForEach([DownloadManagerView.SortOrder.addTime, .name, .status], id: \.self) { order in
|
||||
Button(action: {
|
||||
sortOrder = order
|
||||
}) {
|
||||
HStack {
|
||||
Text(order.description)
|
||||
if sortOrder == order {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
Text(sortOrder.description)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DownloadManagerView()
|
||||
.environmentObject(NetworkManager())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadProgressView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@ObservedObject var task: NewDownloadTask
|
||||
let onCancel: () -> Void
|
||||
let onPause: () -> Void
|
||||
@@ -26,7 +25,7 @@ struct DownloadProgressView: View {
|
||||
private var statusLabel: some View {
|
||||
Text(task.status.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(statusColor)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 6)
|
||||
.background(statusBackgroundColor)
|
||||
@@ -131,7 +130,7 @@ struct DownloadProgressView: View {
|
||||
showInstallPrompt = false
|
||||
isInstalling = true
|
||||
Task {
|
||||
await networkManager.installProduct(at: task.directory)
|
||||
await globalNetworkManager.installProduct(at: task.directory)
|
||||
}
|
||||
} catch {
|
||||
showSetupProcessAlert = true
|
||||
@@ -145,7 +144,7 @@ struct DownloadProgressView: View {
|
||||
showInstallPrompt = false
|
||||
isInstalling = true
|
||||
Task {
|
||||
await networkManager.installProduct(at: task.directory)
|
||||
await globalNetworkManager.installProduct(at: task.directory)
|
||||
}
|
||||
} catch {
|
||||
showSetupProcessAlert = true
|
||||
@@ -205,7 +204,7 @@ struct DownloadProgressView: View {
|
||||
showInstallPrompt = false
|
||||
isInstalling = true
|
||||
Task {
|
||||
await networkManager.installProduct(at: task.directory)
|
||||
await globalNetworkManager.installProduct(at: task.directory)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
@@ -217,18 +216,18 @@ struct DownloadProgressView: View {
|
||||
}
|
||||
.sheet(isPresented: $isInstalling) {
|
||||
Group {
|
||||
if case .installing(let progress, let status) = networkManager.installationState {
|
||||
if case .installing(let progress, let status) = globalNetworkManager.installationState {
|
||||
InstallProgressView(
|
||||
productName: task.displayName,
|
||||
progress: progress,
|
||||
status: status,
|
||||
onCancel: {
|
||||
networkManager.cancelInstallation()
|
||||
globalNetworkManager.cancelInstallation()
|
||||
isInstalling = false
|
||||
},
|
||||
onRetry: nil
|
||||
)
|
||||
} else if case .completed = networkManager.installationState {
|
||||
} else if case .completed = globalNetworkManager.installationState {
|
||||
InstallProgressView(
|
||||
productName: task.displayName,
|
||||
progress: 1.0,
|
||||
@@ -238,7 +237,7 @@ struct DownloadProgressView: View {
|
||||
},
|
||||
onRetry: nil
|
||||
)
|
||||
} else if case .failed(let error) = networkManager.installationState {
|
||||
} else if case .failed(let error) = globalNetworkManager.installationState {
|
||||
InstallProgressView(
|
||||
productName: task.displayName,
|
||||
progress: 0,
|
||||
@@ -248,7 +247,7 @@ struct DownloadProgressView: View {
|
||||
},
|
||||
onRetry: {
|
||||
Task {
|
||||
await networkManager.retryInstallation(at: task.directory)
|
||||
await globalNetworkManager.retryInstallation(at: task.directory)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -258,7 +257,7 @@ struct DownloadProgressView: View {
|
||||
progress: 0,
|
||||
status: String(localized: "准备安装..."),
|
||||
onCancel: {
|
||||
networkManager.cancelInstallation()
|
||||
globalNetworkManager.cancelInstallation()
|
||||
isInstalling = false
|
||||
},
|
||||
onRetry: nil
|
||||
@@ -301,12 +300,12 @@ struct DownloadProgressView: View {
|
||||
}
|
||||
|
||||
private func loadIcon() {
|
||||
let product = globalCcmResult.products.first { $0.id == task.productId }
|
||||
let product = findProduct(id: task.productId)
|
||||
if product != nil {
|
||||
if let bestIcon = product.getBestIcon(),
|
||||
let iconURL = URL(string: bestIcon.url) {
|
||||
if let bestIcon = product?.getBestIcon(),
|
||||
let iconURL = URL(string: bestIcon.value) {
|
||||
|
||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
|
||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) {
|
||||
self.iconImage = cachedImage
|
||||
return
|
||||
}
|
||||
@@ -324,13 +323,13 @@ struct DownloadProgressView: View {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
IconCache.shared.setIcon(image, for: bestIcon.url)
|
||||
IconCache.shared.setIcon(image, for: bestIcon.value)
|
||||
|
||||
await MainActor.run {
|
||||
self.iconImage = image
|
||||
}
|
||||
} catch {
|
||||
if let localImage = NSImage(named: task.sapCode) {
|
||||
if let localImage = NSImage(named: task.productId) {
|
||||
await MainActor.run {
|
||||
self.iconImage = localImage
|
||||
}
|
||||
@@ -357,184 +356,20 @@ struct DownloadProgressView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 12) {
|
||||
Group {
|
||||
if let iconImage = iconImage {
|
||||
Image(nsImage: iconImage)
|
||||
.resizable()
|
||||
.interpolation(.high)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 32, height: 32)
|
||||
.onAppear(perform: loadIcon)
|
||||
TaskHeaderView(iconImage: iconImage, task: task, loadIcon: loadIcon, formatPath: formatPath, openInFinder: openInFinder)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 4) {
|
||||
Text(task.displayName)
|
||||
.font(.headline)
|
||||
Text(task.productVersion)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
statusLabel
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text(formatPath(task.directory.path))
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.onTapGesture {
|
||||
openInFinder(task.directory.path)
|
||||
}
|
||||
.help(task.directory.path)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Text(task.formattedDownloadedSize)
|
||||
Text("/")
|
||||
Text(task.formattedTotalSize)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if task.totalSpeed > 0 {
|
||||
Text(formatRemainingTime(
|
||||
totalSize: task.totalSize,
|
||||
downloadedSize: task.totalDownloadedSize,
|
||||
speed: task.totalSpeed
|
||||
))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("\(Int(task.totalProgress * 100))%")
|
||||
|
||||
if task.totalSpeed > 0 {
|
||||
Text(formatSpeed(task.totalSpeed))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
|
||||
ProgressView(value: task.totalProgress)
|
||||
.progressViewStyle(.linear)
|
||||
}
|
||||
TaskProgressView(task: task, formatRemainingTime: formatRemainingTime, formatSpeed: formatSpeed)
|
||||
|
||||
if !task.dependenciesToDownload.isEmpty {
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isPackageListExpanded.toggle()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
Text("产品和包列表")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
#if DEBUG
|
||||
Button(action: {
|
||||
let containerURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
let tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
|
||||
let fileName = "\(task.sapCode == "APRO" ? "Adobe Downloader \(task.sapCode)_\(task.version)_\(task.platform)" : "Adobe Downloader \(task.sapCode)_\(task.version)-\(task.language)-\(task.platform)")-task.json"
|
||||
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
||||
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: tasksDirectory.path)
|
||||
}) {
|
||||
Label("查看持久化文件", systemImage: "doc.text.magnifyingglass")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
.controlSize(.regular)
|
||||
#endif
|
||||
|
||||
if case .completed = task.status, task.sapCode != "APRO" {
|
||||
Button(action: {
|
||||
showCommandLineInstall.toggle()
|
||||
}) {
|
||||
Label("命令行安装", systemImage: "terminal")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.purple)
|
||||
.controlSize(.regular)
|
||||
.popover(isPresented: $showCommandLineInstall, arrowEdge: .bottom) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button("复制命令") {
|
||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||
let driverPath = "\(task.directory.path)/driver.xml"
|
||||
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(command, forType: .string)
|
||||
showCopiedAlert = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
showCopiedAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
if showCopiedAlert {
|
||||
Text("已复制")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||
let driverPath = "\(task.directory.path)/driver.xml"
|
||||
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||
Text(command)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundColor(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.padding(8)
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 400)
|
||||
}
|
||||
}
|
||||
|
||||
actionButtons
|
||||
}
|
||||
|
||||
if isPackageListExpanded {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(task.productsToDownload, id: \.sapCode) { product in
|
||||
ProductRow(
|
||||
product: product,
|
||||
isCurrentProduct: task.currentPackage?.id == product.packages.first?.id,
|
||||
expandedProducts: $expandedProducts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
}
|
||||
}
|
||||
PackageListView(
|
||||
task: task,
|
||||
isPackageListExpanded: $isPackageListExpanded,
|
||||
showCommandLineInstall: $showCommandLineInstall,
|
||||
showCopiedAlert: $showCopiedAlert,
|
||||
expandedProducts: $expandedProducts,
|
||||
actionButtons: AnyView(actionButtons)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -548,8 +383,256 @@ struct DownloadProgressView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct TaskHeaderView: View {
|
||||
let iconImage: NSImage?
|
||||
let task: NewDownloadTask
|
||||
let loadIcon: () -> Void
|
||||
let formatPath: (String) -> String
|
||||
let openInFinder: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Group {
|
||||
if let iconImage = iconImage {
|
||||
Image(nsImage: iconImage)
|
||||
.resizable()
|
||||
.interpolation(.high)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 32, height: 32)
|
||||
.onAppear(perform: loadIcon)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 4) {
|
||||
Text(task.displayName)
|
||||
.font(.headline)
|
||||
Text(task.productVersion)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
statusLabel
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text(formatPath(task.directory.path))
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.onTapGesture {
|
||||
openInFinder(task.directory.path)
|
||||
}
|
||||
.help(task.directory.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusLabel: some View {
|
||||
Text(task.status.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 6)
|
||||
.background(statusBackgroundColor)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
|
||||
private var statusBackgroundColor: Color {
|
||||
switch task.status {
|
||||
case .downloading:
|
||||
return Color.blue
|
||||
case .preparing:
|
||||
return Color.purple.opacity(0.8)
|
||||
case .completed:
|
||||
return Color.green.opacity(0.8)
|
||||
case .failed:
|
||||
return Color.red.opacity(0.8)
|
||||
case .paused:
|
||||
return Color.orange.opacity(0.8)
|
||||
case .waiting:
|
||||
return Color.gray.opacity(0.8)
|
||||
case .retrying:
|
||||
return Color.yellow.opacity(0.8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TaskProgressView: View {
|
||||
let task: NewDownloadTask
|
||||
let formatRemainingTime: (Int64, Int64, Double) -> String
|
||||
let formatSpeed: (Double) -> String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Text(task.formattedDownloadedSize)
|
||||
Text("/")
|
||||
Text(task.formattedTotalSize)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if task.totalSpeed > 0 {
|
||||
Text(formatRemainingTime(
|
||||
task.totalSize,
|
||||
task.totalDownloadedSize,
|
||||
task.totalSpeed
|
||||
))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("\(Int(task.totalProgress * 100))%")
|
||||
|
||||
if task.totalSpeed > 0 {
|
||||
Text(formatSpeed(task.totalSpeed))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
|
||||
ProgressView(value: task.totalProgress)
|
||||
.progressViewStyle(.linear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PackageListView: View {
|
||||
let task: NewDownloadTask
|
||||
@Binding var isPackageListExpanded: Bool
|
||||
@Binding var showCommandLineInstall: Bool
|
||||
@Binding var showCopiedAlert: Bool
|
||||
@Binding var expandedProducts: Set<String>
|
||||
let actionButtons: AnyView
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isPackageListExpanded.toggle()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
Text("产品和包列表")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
#if DEBUG
|
||||
Button(action: {
|
||||
let containerURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
let tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
|
||||
let fileName = "\(task.productId == "APRO" ? "Adobe Downloader \(task.productId)_\(task.productVersion)_\(task.platform)" : "Adobe Downloader \(task.productId)_\(task.productVersion)-\(task.language)-\(task.platform)")-task.json"
|
||||
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
||||
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: tasksDirectory.path)
|
||||
}) {
|
||||
Label("查看持久化文件", systemImage: "doc.text.magnifyingglass")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
.controlSize(.regular)
|
||||
#endif
|
||||
|
||||
if case .completed = task.status, task.productId != "APRO" {
|
||||
CommandLineInstallButton(
|
||||
task: task,
|
||||
showCommandLineInstall: $showCommandLineInstall,
|
||||
showCopiedAlert: $showCopiedAlert
|
||||
)
|
||||
}
|
||||
|
||||
actionButtons
|
||||
}
|
||||
|
||||
if isPackageListExpanded {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(task.dependenciesToDownload, id: \.sapCode) { product in
|
||||
ProductRow(
|
||||
product: product,
|
||||
isCurrentProduct: task.currentPackage?.id == product.packages.first?.id,
|
||||
expandedProducts: $expandedProducts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CommandLineInstallButton: View {
|
||||
let task: NewDownloadTask
|
||||
@Binding var showCommandLineInstall: Bool
|
||||
@Binding var showCopiedAlert: Bool
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
showCommandLineInstall.toggle()
|
||||
}) {
|
||||
Label("命令行安装", systemImage: "terminal")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.purple)
|
||||
.controlSize(.regular)
|
||||
.popover(isPresented: $showCommandLineInstall, arrowEdge: .bottom) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button("复制命令") {
|
||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||
let driverPath = "\(task.directory.path)/driver.xml"
|
||||
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(command, forType: .string)
|
||||
showCopiedAlert = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
showCopiedAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
if showCopiedAlert {
|
||||
Text("已复制")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||
let driverPath = "\(task.directory.path)/driver.xml"
|
||||
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||
Text(command)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundColor(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.padding(8)
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProductRow: View {
|
||||
@ObservedObject var dependencies: DependenciesToDownload
|
||||
@ObservedObject var product: DependenciesToDownload
|
||||
let isCurrentProduct: Bool
|
||||
@Binding var expandedProducts: Set<String>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ struct ShouldExistsSetUpView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
HeaderView()
|
||||
SetupAlertHeaderView()
|
||||
MessageView()
|
||||
ButtonsView(
|
||||
isDownloading: $isDownloading,
|
||||
@@ -47,17 +47,19 @@ struct ShouldExistsSetUpView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct HeaderView: View {
|
||||
private struct SetupAlertHeaderView: View {
|
||||
var body: some View {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(.orange)
|
||||
.padding(.bottom, 5)
|
||||
.frame(alignment: .bottomTrailing)
|
||||
VStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(.orange)
|
||||
.padding(.bottom, 5)
|
||||
.frame(alignment: .bottomTrailing)
|
||||
|
||||
Text("未检测到 Adobe CC 组件")
|
||||
.font(.system(size: 24))
|
||||
.bold()
|
||||
Text("未检测到 Adobe CC 组件")
|
||||
.font(.system(size: 24))
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ private struct ButtonsView: View {
|
||||
isCancelled = false
|
||||
Task {
|
||||
do {
|
||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
||||
try await globalNewDownloadUtils.downloadX1a0HeCCPackages(
|
||||
progressHandler: { progress, status in
|
||||
Task { @MainActor in
|
||||
downloadProgress = progress
|
||||
|
||||
@@ -21,7 +21,6 @@ private enum VersionPickerConstants {
|
||||
}
|
||||
|
||||
struct VersionPickerView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
||||
@@ -37,7 +36,7 @@ struct VersionPickerView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon)
|
||||
VersionPickerHeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon)
|
||||
VersionListView(
|
||||
product: product,
|
||||
expandedVersions: $expandedVersions,
|
||||
@@ -49,7 +48,7 @@ struct VersionPickerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct HeaderView: View {
|
||||
private struct VersionPickerHeaderView: View {
|
||||
let product: Product
|
||||
let downloadAppleSilicon: Bool
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@@ -160,7 +159,7 @@ private struct VersionRow: View {
|
||||
|
||||
private var existingPath: URL? {
|
||||
globalNetworkManager.isVersionDownloaded(
|
||||
product: product,
|
||||
productId: product.id,
|
||||
version: version,
|
||||
language: defaultLanguage
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
},
|
||||
"(可能导致处理 Setup 组件失败)" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -22,7 +21,6 @@
|
||||
}
|
||||
},
|
||||
"(将导致无法使用安装功能)" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -157,8 +155,12 @@
|
||||
},
|
||||
"Adobe Downloader %@" : {
|
||||
|
||||
},
|
||||
"Adobe Downloader 完全免费: https://github.com/X1a0He/Adobe-Downloader" : {
|
||||
|
||||
},
|
||||
"Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -288,6 +290,7 @@
|
||||
|
||||
},
|
||||
"Debug 模式" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -422,6 +425,7 @@
|
||||
|
||||
},
|
||||
"Setup 组件安装成功" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -769,6 +773,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"产品未找到" : {
|
||||
"comment" : "Product not found"
|
||||
},
|
||||
"仅下载" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -1385,6 +1392,7 @@
|
||||
}
|
||||
},
|
||||
"将执行的命令:" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1425,6 +1433,7 @@
|
||||
}
|
||||
},
|
||||
"展开全部" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1445,7 +1454,6 @@
|
||||
}
|
||||
},
|
||||
"已处理" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1456,7 +1464,6 @@
|
||||
}
|
||||
},
|
||||
"已备份" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1662,6 +1669,7 @@
|
||||
}
|
||||
},
|
||||
"折叠全部" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -2099,9 +2107,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"查看持久化文件" : {
|
||||
|
||||
},
|
||||
"检查中" : {
|
||||
"localizations" : {
|
||||
|
||||
Reference in New Issue
Block a user