diff --git a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
index 3a6a7bc..e0e91cb 100644
--- a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+++ b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -14,10 +14,10 @@
filePath = "Adobe Downloader/Commons/Structs.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
- startingLineNumber = "152"
- endingLineNumber = "152"
- landmarkName = "ProductsToDownload"
- landmarkType = "3">
+ startingLineNumber = "27"
+ endingLineNumber = "27"
+ landmarkName = "unknown"
+ landmarkType = "0">
-
-
-
-
diff --git a/Adobe Downloader/Adobe DownloaderApp.swift b/Adobe Downloader/Adobe DownloaderApp.swift
index 3453a90..e1fc249 100644
--- a/Adobe Downloader/Adobe DownloaderApp.swift
+++ b/Adobe Downloader/Adobe DownloaderApp.swift
@@ -4,7 +4,6 @@ import Sparkle
@main
struct Adobe_DownloaderApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
- @StateObject private var networkManager = NetworkManager()
@State private var showBackupAlert = false
@State private var showTipsSheet = false
@State private var showLanguagePicker = false
@@ -17,6 +16,9 @@ struct Adobe_DownloaderApp: App {
private let updaterController: SPUStandardUpdaterController
init() {
+ globalNetworkService = NewNetworkService()
+ globalNetworkManager = NetworkManager()
+ globalNewDownloadUtils = NewDownloadUtils()
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
@@ -56,7 +58,7 @@ struct Adobe_DownloaderApp: App {
var body: some Scene {
WindowGroup {
ContentView()
- .environmentObject(networkManager)
+ .environmentObject(globalNetworkManager)
.frame(minWidth: 792, minHeight: 600)
.tint(.blue)
.task {
@@ -64,7 +66,7 @@ struct Adobe_DownloaderApp: App {
}
.sheet(isPresented: $showCreativeCloudAlert) {
ShouldExistsSetUpView()
- .environmentObject(networkManager)
+ .environmentObject(globalNetworkManager)
}
.alert("Setup未备份提示", isPresented: $showBackupAlert) {
Button("确定") {
@@ -84,7 +86,7 @@ struct Adobe_DownloaderApp: App {
showTipsSheet: $showTipsSheet,
showLanguagePicker: $showLanguagePicker
)
- .environmentObject(networkManager)
+ .environmentObject(globalNetworkManager)
.sheet(isPresented: $showLanguagePicker) {
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
storage.defaultLanguage = language
@@ -103,7 +105,7 @@ struct Adobe_DownloaderApp: App {
Settings {
AboutView(updater: updaterController.updater)
- .environmentObject(networkManager)
+ .environmentObject(globalNetworkManager)
}
}
@@ -111,8 +113,7 @@ struct Adobe_DownloaderApp: App {
PrivilegedHelperManager.shared.checkInstall()
await MainActor.run {
- appDelegate.networkManager = networkManager
- networkManager.loadSavedTasks()
+ globalNetworkManager.loadSavedTasks()
}
let needsBackup = !ModifySetup.isSetupBackup()
diff --git a/Adobe Downloader/AppDelegate.swift b/Adobe Downloader/AppDelegate.swift
index aba61a4..6dc80c0 100644
--- a/Adobe Downloader/AppDelegate.swift
+++ b/Adobe Downloader/AppDelegate.swift
@@ -3,7 +3,6 @@ import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
private var eventMonitor: Any?
- var networkManager: NetworkManager?
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.mainMenu = nil
@@ -20,22 +19,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
- guard let manager = networkManager else { return .terminateNow }
-
- let hasActiveDownloads = manager.downloadTasks.contains { task in
+ let hasActiveDownloads = globalNetworkManager.downloadTasks.contains { task in
if case .downloading = task.totalStatus { return true }
return false
}
if hasActiveDownloads {
Task {
- for task in manager.downloadTasks {
+ for task in globalNetworkManager.downloadTasks {
if case .downloading = task.totalStatus {
- await manager.downloadUtils.pauseDownloadTask(
+ await globalNewDownloadUtils.pauseDownloadTask(
taskId: task.id,
reason: .other(String(localized: "程序即将退出"))
)
- await manager.saveTask(task)
+ await globalNetworkManager.saveTask(task)
}
}
@@ -50,9 +47,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let response = alert.runModal()
if response == .alertSecondButtonReturn {
Task {
- for task in manager.downloadTasks {
+ for task in globalNetworkManager.downloadTasks {
if case .paused = task.totalStatus {
- await manager.downloadUtils.resumeDownloadTask(taskId: task.id)
+ await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
}
}
}
@@ -71,6 +68,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if let monitor = eventMonitor {
NSEvent.removeMonitor(monitor)
}
- networkManager = nil
}
}
diff --git a/Adobe Downloader/Commons/Extensions.swift b/Adobe Downloader/Commons/Extensions.swift
index 45245c8..f0c2f08 100644
--- a/Adobe Downloader/Commons/Extensions.swift
+++ b/Adobe Downloader/Commons/Extensions.swift
@@ -5,17 +5,3 @@
//
import Foundation
import AppKit
-
-extension NewDownloadTask {
- var startTime: Date {
- switch totalStatus {
- case .downloading(let info): return info.startTime
- case .completed(let info): return info.timestamp - info.totalTime
- case .preparing(let info): return info.timestamp
- case .paused(let info): return info.timestamp
- case .failed(let info): return info.timestamp
- case .retrying(let info): return info.nextRetryDate - 60
- case .waiting, .none: return createAt
- }
- }
-}
diff --git a/Adobe Downloader/Commons/Globals.swift b/Adobe Downloader/Commons/Globals.swift
index de715cd..123baa5 100644
--- a/Adobe Downloader/Commons/Globals.swift
+++ b/Adobe Downloader/Commons/Globals.swift
@@ -5,15 +5,105 @@
// Created by X1a0He on 2/26/25.
//
-var globalStiResult: NewParseResult?
-var globalCcmResult: NewParseResult?
+// 下面是所有全局变量的私有存储
+private var _globalStiResult: NewParseResult?
+private var _globalCcmResult: NewParseResult?
+private var _globalCdn: String = ""
+private var _globalNetworkService: NewNetworkService?
+private var _globalNetworkManager: NetworkManager?
+private var _globalNewDownloadUtils: NewDownloadUtils?
+private var _globalCancelTracker: CancelTracker?
+
+// 计算属性,确保总是返回有效实例
+var globalStiResult: NewParseResult {
+ get {
+ if _globalStiResult == nil {
+ _globalStiResult = NewParseResult(products: [], cdn: "")
+ }
+ return _globalStiResult!
+ }
+ set {
+ _globalStiResult = newValue
+ }
+}
+
+var globalCcmResult: NewParseResult {
+ get {
+ if _globalCcmResult == nil {
+ _globalCcmResult = NewParseResult(products: [], cdn: "")
+ }
+ return _globalCcmResult!
+ }
+ set {
+ _globalCcmResult = newValue
+ }
+}
+
+var globalCdn: String {
+ get {
+ return _globalCdn
+ }
+ set {
+ _globalCdn = newValue
+ }
+}
+
+var globalNetworkService: NewNetworkService {
+ get {
+ if _globalNetworkService == nil {
+ fatalError("NewNetworkService 没有被初始化,请确保在应用启动时初始化")
+ }
+ return _globalNetworkService!
+ }
+ set {
+ _globalNetworkService = newValue
+ }
+}
+
+var globalNetworkManager: NetworkManager {
+ get {
+ if _globalNetworkManager == nil {
+ fatalError("NetworkManager 没有被初始化,请确保在应用启动时初始化")
+ }
+ return _globalNetworkManager!
+ }
+ set {
+ _globalNetworkManager = newValue
+ }
+}
+
+var globalNewDownloadUtils: NewDownloadUtils {
+ get {
+ if _globalNewDownloadUtils == nil {
+ fatalError("NewDownloadUtils 没有被初始化,请确保在应用启动时初始化")
+ }
+ return _globalNewDownloadUtils!
+ }
+ set {
+ _globalNewDownloadUtils = newValue
+ }
+}
+
+var globalCancelTracker: CancelTracker {
+ get {
+ if _globalCancelTracker == nil {
+ _globalCancelTracker = CancelTracker()
+ }
+ return _globalCancelTracker!
+ }
+ set {
+ _globalCancelTracker = newValue
+ }
+}
func getAllProducts() -> [Product] {
var allProducts = [Product]()
- if let stiProducts = globalStiResult?.products {
+ let stiProducts = globalStiResult.products
+ if !stiProducts.isEmpty {
allProducts.append(contentsOf: stiProducts)
}
- if let ccmProducts = globalCcmResult?.products {
+ let ccmProducts = globalCcmResult.products
+ if !ccmProducts.isEmpty {
allProducts.append(contentsOf: ccmProducts)
}
return allProducts
diff --git a/Adobe Downloader/Commons/NewStructs.swift b/Adobe Downloader/Commons/NewStructs.swift
index b0560cb..c55b513 100644
--- a/Adobe Downloader/Commons/NewStructs.swift
+++ b/Adobe Downloader/Commons/NewStructs.swift
@@ -1,4 +1,11 @@
-struct Product {
+//
+// Adobe Downloader
+//
+// Created by X1a0He on 2/26/25.
+//
+
+import Foundation
+struct Product: Codable, Equatable {
var type: String
var displayName: String
var family: String
@@ -10,19 +17,34 @@ struct Product {
var version: String
var id: String
var hidden: Bool
-
- struct ProductIcon {
- var value: String
- var size: String
+
+ func hasValidVersions(allowedPlatform: [String]) -> Bool {
+ return platforms.contains { platform in
+ allowedPlatform.contains(platform.id) && !platform.languageSet.isEmpty
+ }
}
- struct Platform {
+ struct ProductIcon: Codable, Equatable {
+ var value: String
+ var size: String
+
+ var dimension: Int {
+ let components = size.split(separator: "x")
+ if components.count == 2,
+ let dimension = Int(components[0]) {
+ return dimension
+ }
+ return 0
+ }
+ }
+
+ struct Platform: Codable, Equatable {
var languageSet: [LanguageSet]
var modules: [Module]
var range: [Range]
var id: String
- struct LanguageSet {
+ struct LanguageSet: Codable, Equatable {
var manifestURL: String
var dependencies: [Dependency]
var productCode: String
@@ -32,7 +54,7 @@ struct Product {
var baseVersion: String
var productVersion: String
- struct Dependency {
+ struct Dependency: Codable, Equatable {
var sapCode: String
var baseVersion: String
var productVersion: String
@@ -40,25 +62,251 @@ struct Product {
}
}
- struct Module {
+ struct Module: Codable, Equatable {
var displayName: String
var deploymentType: String
var id: String
}
- struct Range {
+ struct Range: Codable, Equatable {
var min: String
var max: String
}
}
- struct ReferencedProduct {
+ struct ReferencedProduct: Codable, Equatable {
var sapCode: String
var version: String
}
+
+ func getBestIcon() -> ProductIcon? {
+ if let icon = productIcons.first(where: { $0.size == "192x192" }) {
+ return icon
+ }
+ return productIcons.max(by: { $0.dimension < $1.dimension })
+ }
}
struct NewParseResult {
var products: [Product]
var cdn: String
}
+
+/* ========== */
+struct UniqueProduct {
+ var id: String
+ var displayName: String
+}
+
+/* ========== */
+class DependenciesToDownload: ObservableObject, Codable {
+ // 别人依赖就他吗叫sapCode,Adobe也是傻逼,一会id一会sapCode
+ var sapCode: String
+ var version: String
+ var buildGuid: String
+ var applicationJson: String?
+ @Published var packages: [Package] = []
+ @Published var completedPackages: Int = 0
+
+ var totalPackages: Int {
+ packages.count
+ }
+
+ init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") {
+ self.sapCode = sapCode
+ self.version = version
+ self.buildGuid = buildGuid
+ self.applicationJson = applicationJson
+ }
+
+ func updateCompletedPackages() {
+ Task { @MainActor in
+ completedPackages = packages.filter { $0.downloaded }.count
+ objectWillChange.send()
+ }
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case sapCode, version, buildGuid, applicationJson, packages
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(sapCode, forKey: .sapCode)
+ try container.encode(version, forKey: .version)
+ try container.encode(buildGuid, forKey: .buildGuid)
+ try container.encodeIfPresent(applicationJson, forKey: .applicationJson)
+ try container.encode(packages, forKey: .packages)
+ }
+
+ required init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ sapCode = try container.decode(String.self, forKey: .sapCode)
+ version = try container.decode(String.self, forKey: .version)
+ buildGuid = try container.decode(String.self, forKey: .buildGuid)
+ applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson)
+ packages = try container.decode([Package].self, forKey: .packages)
+ completedPackages = 0
+ }
+}
+
+/* ========== */
+class Package: Identifiable, ObservableObject, Codable {
+ var id = UUID()
+ var type: String
+ var fullPackageName: String
+ var downloadSize: Int64
+ var downloadURL: String
+ var packageVersion: String
+
+ @Published var downloadedSize: Int64 = 0 {
+ didSet {
+ if downloadSize > 0 {
+ progress = Double(downloadedSize) / Double(downloadSize)
+ }
+ }
+ }
+ @Published var progress: Double = 0
+ @Published var speed: Double = 0
+ @Published var status: PackageStatus = .waiting
+ @Published var downloaded: Bool = false
+
+ var lastUpdated: Date = Date()
+ var lastRecordedSize: Int64 = 0
+ var retryCount: Int = 0
+ var lastError: Error?
+
+ var canRetry: Bool {
+ if case .failed = status {
+ return retryCount < 3
+ }
+ return false
+ }
+
+ func markAsFailed(_ error: Error) {
+ Task { @MainActor in
+ self.lastError = error
+ self.status = .failed(error.localizedDescription)
+ objectWillChange.send()
+ }
+ }
+
+ func prepareForRetry() {
+ Task { @MainActor in
+ self.retryCount += 1
+ self.status = .waiting
+ self.progress = 0
+ self.speed = 0
+ self.downloadedSize = 0
+ objectWillChange.send()
+ }
+ }
+
+ init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String) {
+ self.type = type
+ self.fullPackageName = fullPackageName
+ self.downloadSize = downloadSize
+ self.downloadURL = downloadURL
+ self.packageVersion = packageVersion
+ }
+
+ func updateProgress(downloadedSize: Int64, speed: Double) {
+ Task { @MainActor in
+ self.downloadedSize = downloadedSize
+ self.speed = speed
+ self.status = .downloading
+ objectWillChange.send()
+ }
+ }
+
+ func markAsCompleted() {
+ Task { @MainActor in
+ self.downloaded = true
+ self.progress = 1.0
+ self.speed = 0
+ self.status = .completed
+ self.downloadedSize = downloadSize
+ objectWillChange.send()
+ }
+ }
+
+ var formattedSize: String {
+ ByteCountFormatter.string(fromByteCount: downloadSize, countStyle: .file)
+ }
+
+ var formattedDownloadedSize: String {
+ ByteCountFormatter.string(fromByteCount: downloadedSize, countStyle: .file)
+ }
+
+ var formattedSpeed: String {
+ ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .file) + "/s"
+ }
+
+ var hasValidSize: Bool {
+ downloadSize > 0
+ }
+
+ func updateStatus(_ status: PackageStatus) {
+ Task { @MainActor in
+ self.status = status
+ objectWillChange.send()
+ }
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case id, type, fullPackageName, downloadSize, downloadURL, packageVersion
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(id, forKey: .id)
+ try container.encode(type, forKey: .type)
+ try container.encode(fullPackageName, forKey: .fullPackageName)
+ try container.encode(downloadSize, forKey: .downloadSize)
+ try container.encode(downloadURL, forKey: .downloadURL)
+ }
+
+ required init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ id = try container.decode(UUID.self, forKey: .id)
+ type = try container.decode(String.self, forKey: .type)
+ fullPackageName = try container.decode(String.self, forKey: .fullPackageName)
+ downloadSize = try container.decode(Int64.self, forKey: .downloadSize)
+ downloadURL = try container.decode(String.self, forKey: .downloadURL)
+ packageVersion = try container.decode(String.self, forKey: .packageVersion)
+ }
+}
+
+struct NetworkConstants {
+ static let downloadTimeout: TimeInterval = 300
+ static let maxRetryAttempts = 3
+ static let retryDelay: UInt64 = 3_000_000_000
+ static let bufferSize = 1024 * 1024
+ static let maxConcurrentDownloads = 3
+ static let progressUpdateInterval: TimeInterval = 1
+
+ static func generateCookie() -> String {
+ let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ let randomString = (0..<26).map { _ in chars.randomElement()! }
+ return "fg=\(String(randomString))======"
+ }
+
+ static var productsJSONURL: String {
+ "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")/products/all"
+ }
+
+ static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications"
+
+ static var adobeRequestHeaders: [String: String] {
+ [
+ "x-adobe-app-id": "accc-apps-panel-desktop",
+ "x-api-key": "Creative Cloud_v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")_4",
+ "User-Agent": "Creative Cloud/6.4.0.361/Mac-15.1",
+ "Cookie": generateCookie()
+ ]
+ }
+
+ static let downloadHeaders = [
+ "User-Agent": "Creative Cloud"
+ ]
+}
diff --git a/Adobe Downloader/Commons/Structs.swift b/Adobe Downloader/Commons/Structs.swift
index 00e0345..39566c0 100644
--- a/Adobe Downloader/Commons/Structs.swift
+++ b/Adobe Downloader/Commons/Structs.swift
@@ -1,299 +1,141 @@
+////
+//// Adobe Downloader
+////
+//// Created by X1a0He on 2024/10/30.
+////
+//import Foundation
//
-// Adobe Downloader
//
-// Created by X1a0He on 2024/10/30.
+//class ProductsToDownload: ObservableObject, Codable {
+// var sapCode: String
+// var version: String
+// var buildGuid: String
+// var applicationJson: String?
+// @Published var packages: [Package] = []
+// @Published var completedPackages: Int = 0
+//
+// var totalPackages: Int {
+// packages.count
+// }
//
-import Foundation
-
-class Package: Identifiable, ObservableObject, Codable {
- var id = UUID()
- var type: String
- var fullPackageName: String
- var downloadSize: Int64
- var downloadURL: String
- var packageVersion: String
-
- @Published var downloadedSize: Int64 = 0 {
- didSet {
- if downloadSize > 0 {
- progress = Double(downloadedSize) / Double(downloadSize)
- }
- }
- }
- @Published var progress: Double = 0
- @Published var speed: Double = 0
- @Published var status: PackageStatus = .waiting
- @Published var downloaded: Bool = false
-
- var lastUpdated: Date = Date()
- var lastRecordedSize: Int64 = 0
- var retryCount: Int = 0
- var lastError: Error?
-
- var canRetry: Bool {
- if case .failed = status {
- return retryCount < 3
- }
- return false
- }
-
- func markAsFailed(_ error: Error) {
- Task { @MainActor in
- self.lastError = error
- self.status = .failed(error.localizedDescription)
- objectWillChange.send()
- }
- }
-
- func prepareForRetry() {
- Task { @MainActor in
- self.retryCount += 1
- self.status = .waiting
- self.progress = 0
- self.speed = 0
- self.downloadedSize = 0
- objectWillChange.send()
- }
- }
-
- init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String) {
- self.type = type
- self.fullPackageName = fullPackageName
- self.downloadSize = downloadSize
- self.downloadURL = downloadURL
- self.packageVersion = packageVersion
- }
-
- func updateProgress(downloadedSize: Int64, speed: Double) {
- Task { @MainActor in
- self.downloadedSize = downloadedSize
- self.speed = speed
- self.status = .downloading
- objectWillChange.send()
- }
- }
-
- func markAsCompleted() {
- Task { @MainActor in
- self.downloaded = true
- self.progress = 1.0
- self.speed = 0
- self.status = .completed
- self.downloadedSize = downloadSize
- objectWillChange.send()
- }
- }
-
- var formattedSize: String {
- ByteCountFormatter.string(fromByteCount: downloadSize, countStyle: .file)
- }
-
- var formattedDownloadedSize: String {
- ByteCountFormatter.string(fromByteCount: downloadedSize, countStyle: .file)
- }
-
- var formattedSpeed: String {
- ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .file) + "/s"
- }
-
- var hasValidSize: Bool {
- downloadSize > 0
- }
-
- func updateStatus(_ status: PackageStatus) {
- Task { @MainActor in
- self.status = status
- objectWillChange.send()
- }
- }
-
- enum CodingKeys: String, CodingKey {
- case id, type, fullPackageName, downloadSize, downloadURL, packageVersion
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(id, forKey: .id)
- try container.encode(type, forKey: .type)
- try container.encode(fullPackageName, forKey: .fullPackageName)
- try container.encode(downloadSize, forKey: .downloadSize)
- try container.encode(downloadURL, forKey: .downloadURL)
- }
-
- required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- id = try container.decode(UUID.self, forKey: .id)
- type = try container.decode(String.self, forKey: .type)
- fullPackageName = try container.decode(String.self, forKey: .fullPackageName)
- downloadSize = try container.decode(Int64.self, forKey: .downloadSize)
- downloadURL = try container.decode(String.self, forKey: .downloadURL)
- packageVersion = try container.decode(String.self, forKey: .packageVersion)
- }
-}
-
-class ProductsToDownload: ObservableObject, Codable {
- var sapCode: String
- var version: String
- var buildGuid: String
- var applicationJson: String?
- @Published var packages: [Package] = []
- @Published var completedPackages: Int = 0
-
- var totalPackages: Int {
- packages.count
- }
-
- init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") {
- self.sapCode = sapCode
- self.version = version
- self.buildGuid = buildGuid
- self.applicationJson = applicationJson
- }
-
- func updateCompletedPackages() {
- Task { @MainActor in
- completedPackages = packages.filter { $0.downloaded }.count
- objectWillChange.send()
- }
- }
-
- enum CodingKeys: String, CodingKey {
- case sapCode, version, buildGuid, applicationJson, packages
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(sapCode, forKey: .sapCode)
- try container.encode(version, forKey: .version)
- try container.encode(buildGuid, forKey: .buildGuid)
- try container.encodeIfPresent(applicationJson, forKey: .applicationJson)
- try container.encode(packages, forKey: .packages)
- }
-
- required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- sapCode = try container.decode(String.self, forKey: .sapCode)
- version = try container.decode(String.self, forKey: .version)
- buildGuid = try container.decode(String.self, forKey: .buildGuid)
- applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson)
- packages = try container.decode([Package].self, forKey: .packages)
- completedPackages = 0
- }
-}
-
-struct SapCodes: Identifiable {
- var id: String { sapCode }
- var sapCode: String
- var displayName: String
-}
-
-struct Sap: Codable, Equatable {
- var id: String { sapCode }
- var hidden: Bool
- var displayName: String
- var sapCode: String
- var versions: [String: Versions]
- var icons: [ProductIcon]
- var productsToDownload: [ProductsToDownload]? = nil
-
- enum CodingKeys: String, CodingKey {
- case hidden, displayName, sapCode, versions, icons
- }
-
- static func == (lhs: Sap, rhs: Sap) -> Bool {
- return lhs.sapCode == rhs.sapCode &&
- lhs.hidden == rhs.hidden &&
- lhs.displayName == rhs.displayName &&
- lhs.versions == rhs.versions &&
- lhs.icons == rhs.icons
- }
-
- struct Versions: Codable, Equatable {
- var sapCode: String
- var baseVersion: String
- var productVersion: String
- var apPlatform: String
- var dependencies: [Dependencies]
- var buildGuid: String
-
- struct Dependencies: Codable, Equatable {
- var sapCode: String
- var version: String
- }
- }
-
- struct ProductIcon: Codable, Equatable {
- let size: String
- let url: String
-
- var dimension: Int {
- let components = size.split(separator: "x")
- if components.count == 2,
- let dimension = Int(components[0]) {
- return dimension
- }
- return 0
- }
- }
-
- var isValid: Bool { !hidden }
-
- func getBestIcon() -> ProductIcon? {
- if let icon = icons.first(where: { $0.size == "192x192" }) {
- return icon
- }
- return icons.max(by: { $0.dimension < $1.dimension })
- }
-
- func hasValidVersions(allowedPlatform: [String]) -> Bool {
- if hidden { return false }
-
- for version in Array(versions.values).reversed() {
- if !version.buildGuid.isEmpty &&
- (!version.buildGuid.contains("/") || sapCode == "APRO") &&
- allowedPlatform.contains(version.apPlatform) {
- return true
- }
- }
- return false
- }
-}
-
-struct NetworkConstants {
- static let downloadTimeout: TimeInterval = 300
- static let maxRetryAttempts = 3
- static let retryDelay: UInt64 = 3_000_000_000
- static let bufferSize = 1024 * 1024
- static let maxConcurrentDownloads = 3
- static let progressUpdateInterval: TimeInterval = 1
-
- static func generateCookie() -> String {
- let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- let randomString = (0..<26).map { _ in chars.randomElement()! }
- return "fg=\(String(randomString))======"
- }
-
- static var productsXmlURL: String {
- "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")/products/all"
- }
-
- static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications"
-
- static var adobeRequestHeaders: [String: String] {
- [
- "x-adobe-app-id": "accc-apps-panel-desktop",
- "x-api-key": "Creative Cloud_v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")_4",
- "User-Agent": "Creative Cloud/6.4.0.361/Mac-15.1",
- "Cookie": generateCookie()
- ]
- }
-
- static let downloadHeaders = [
- "User-Agent": "Creative Cloud"
- ]
-}
-
-struct ProductsResponse: Codable {
- let products: [String: Sap]
- let cdn: String
-}
+// init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") {
+// self.sapCode = sapCode
+// self.version = version
+// self.buildGuid = buildGuid
+// self.applicationJson = applicationJson
+// }
+//
+// func updateCompletedPackages() {
+// Task { @MainActor in
+// completedPackages = packages.filter { $0.downloaded }.count
+// objectWillChange.send()
+// }
+// }
+//
+// enum CodingKeys: String, CodingKey {
+// case sapCode, version, buildGuid, applicationJson, packages
+// }
+//
+// func encode(to encoder: Encoder) throws {
+// var container = encoder.container(keyedBy: CodingKeys.self)
+// try container.encode(sapCode, forKey: .sapCode)
+// try container.encode(version, forKey: .version)
+// try container.encode(buildGuid, forKey: .buildGuid)
+// try container.encodeIfPresent(applicationJson, forKey: .applicationJson)
+// try container.encode(packages, forKey: .packages)
+// }
+//
+// required init(from decoder: Decoder) throws {
+// let container = try decoder.container(keyedBy: CodingKeys.self)
+// sapCode = try container.decode(String.self, forKey: .sapCode)
+// version = try container.decode(String.self, forKey: .version)
+// buildGuid = try container.decode(String.self, forKey: .buildGuid)
+// applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson)
+// packages = try container.decode([Package].self, forKey: .packages)
+// completedPackages = 0
+// }
+//}
+//
+//struct SapCodes: Identifiable {
+// var id: String { sapCode }
+// var sapCode: String
+// var displayName: String
+//}
+//
+//struct Sap: Codable, Equatable {
+// var id: String { sapCode }
+// var hidden: Bool
+// var displayName: String
+// var sapCode: String
+// var versions: [String: Versions]
+// var icons: [ProductIcon]
+// var productsToDownload: [ProductsToDownload]? = nil
+//
+// enum CodingKeys: String, CodingKey {
+// case hidden, displayName, sapCode, versions, icons
+// }
+//
+// static func == (lhs: Sap, rhs: Sap) -> Bool {
+// return lhs.sapCode == rhs.sapCode &&
+// lhs.hidden == rhs.hidden &&
+// lhs.displayName == rhs.displayName &&
+// lhs.versions == rhs.versions &&
+// lhs.icons == rhs.icons
+// }
+//
+// struct Versions: Codable, Equatable {
+// var sapCode: String
+// var baseVersion: String
+// var productVersion: String
+// var apPlatform: String
+// var dependencies: [Dependencies]
+// var buildGuid: String
+//
+// struct Dependencies: Codable, Equatable {
+// var sapCode: String
+// var version: String
+// }
+// }
+//
+// struct ProductIcon: Codable, Equatable {
+// let size: String
+// let url: String
+//
+// var dimension: Int {
+// let components = size.split(separator: "x")
+// if components.count == 2,
+// let dimension = Int(components[0]) {
+// return dimension
+// }
+// return 0
+// }
+// }
+//
+// var isValid: Bool { !hidden }
+//
+// func getBestIcon() -> ProductIcon? {
+// if let icon = icons.first(where: { $0.size == "192x192" }) {
+// return icon
+// }
+// return icons.max(by: { $0.dimension < $1.dimension })
+// }
+//
+// func hasValidVersions(allowedPlatform: [String]) -> Bool {
+// if hidden { return false }
+//
+// for version in Array(versions.values).reversed() {
+// if !version.buildGuid.isEmpty &&
+// (!version.buildGuid.contains("/") || sapCode == "APRO") &&
+// allowedPlatform.contains(version.apPlatform) {
+// return true
+// }
+// }
+// return false
+// }
+//}
+//
+//
+//struct ProductsResponse: Codable {
+// let products: [String: Sap]
+// let cdn: String
+//}
diff --git a/Adobe Downloader/ContentView.swift b/Adobe Downloader/ContentView.swift
index 703cbaa..5d2ffcf 100644
--- a/Adobe Downloader/ContentView.swift
+++ b/Adobe Downloader/ContentView.swift
@@ -1,13 +1,12 @@
import SwiftUI
struct ContentView: View {
- @EnvironmentObject private var networkManager: NetworkManager
@State private var isRefreshing = false
@State private var errorMessage: String?
@State private var showDownloadManager = false
@State private var searchText = ""
@State private var currentApiVersion = StorageData.shared.apiVersion
- @State private var cachedProducts: [Sap] = []
+ @State private var cachedProducts: [UniqueProduct] = []
private var apiVersion: String {
get { StorageData.shared.apiVersion }
@@ -17,14 +16,14 @@ struct ContentView: View {
}
}
- private var filteredProducts: [Sap] {
+ private var filteredProducts: [UniqueProduct] {
if searchText.isEmpty {
return cachedProducts
}
return cachedProducts.filter {
$0.displayName.localizedCaseInsensitiveContains(searchText) ||
- $0.sapCode.localizedCaseInsensitiveContains(searchText)
+ $0.id.localizedCaseInsensitiveContains(searchText)
}
}
@@ -33,10 +32,21 @@ struct ContentView: View {
}
private func updateProductsCache() {
- let products = networkManager.saps.values
+ // 先获取有效的产品
+ let validProducts = globalCcmResult.products
.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
+
+ // 使用字典合并相同ID的产品
+ var uniqueProductsDict = [String: UniqueProduct]()
+ for product in validProducts {
+ uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName)
+ }
+
+ // 转换为数组并按显示名称排序
+ let uniqueProducts = Array(uniqueProductsDict.values)
.sorted { $0.displayName < $1.displayName }
- cachedProducts = products
+
+ cachedProducts = uniqueProducts
}
var body: some View {
@@ -47,7 +57,7 @@ struct ContentView: View {
set: { newValue in
StorageData.shared.downloadAppleSilicon = newValue
Task {
- await networkManager.fetchProducts()
+ await globalNetworkManager.fetchProducts()
}
}
)) {
@@ -106,8 +116,8 @@ struct ContentView: View {
.buttonStyle(.borderless)
.overlay(
Group {
- if !networkManager.downloadTasks.isEmpty {
- Text("\(networkManager.downloadTasks.count)")
+ if !globalNetworkManager.downloadTasks.isEmpty {
+ Text("\(globalNetworkManager.downloadTasks.count)")
.font(.caption2)
.padding(3)
.background(Color.blue)
@@ -138,7 +148,7 @@ struct ContentView: View {
Color(NSColor.windowBackgroundColor)
.ignoresSafeArea()
- switch networkManager.loadingState {
+ switch globalNetworkManager.loadingState {
case .idle, .loading:
ProgressView("正在加载...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -160,7 +170,7 @@ struct ContentView: View {
.padding(.bottom, 10)
Button(action: {
- networkManager.retryFetchData()
+ globalNetworkManager.retryFetchData()
}) {
HStack() {
Image(systemName: "arrow.clockwise")
@@ -216,16 +226,16 @@ struct ContentView: View {
}
.sheet(isPresented: $showDownloadManager) {
DownloadManagerView()
- .environmentObject(networkManager)
+ .environmentObject(globalNetworkManager)
}
.onAppear {
- if networkManager.saps.isEmpty {
+ if globalCcmResult.products.isEmpty {
refreshData()
} else {
updateProductsCache()
}
}
- .onChange(of: networkManager.saps) { _ in
+ .onChange(of: globalNetworkManager.saps) { _ in
updateProductsCache()
}
}
@@ -235,7 +245,7 @@ struct ContentView: View {
errorMessage = nil
Task {
- await networkManager.fetchProducts()
+ await globalNetworkManager.fetchProducts()
await MainActor.run {
updateProductsCache()
isRefreshing = false
@@ -274,4 +284,3 @@ struct SearchField: View {
.environmentObject(networkManager)
.frame(width: 792, height: 600)
}
-
diff --git a/Adobe Downloader/Models/DownloadTask.swift b/Adobe Downloader/Models/DownloadTask.swift
index 45aea18..1288905 100644
--- a/Adobe Downloader/Models/DownloadTask.swift
+++ b/Adobe Downloader/Models/DownloadTask.swift
@@ -21,98 +21,98 @@ extension DownloadStatus {
}
}
-class NewDownloadTask: Identifiable, ObservableObject, Equatable {
- let id = UUID()
- var sapCode: String
- let version: String
- let language: String
- let displayName: String
- let directory: URL
- var productsToDownload: [ProductsToDownload]
- var retryCount: Int
- let createAt: Date
- var displayInstallButton: Bool
- @Published var totalStatus: DownloadStatus?
- @Published var totalProgress: Double
- @Published var totalDownloadedSize: Int64
- @Published var totalSize: Int64
- @Published var totalSpeed: Double
- @Published var currentPackage: Package? {
- didSet {
- objectWillChange.send()
- }
- }
- let platform: String
-
- var status: DownloadStatus {
- totalStatus ?? .waiting
- }
-
- var destinationURL: URL { directory }
-
- var downloadedSize: Int64 {
- get { totalDownloadedSize }
- set { totalDownloadedSize = newValue }
- }
-
- var progress: Double {
- get { totalProgress }
- set { totalProgress = newValue }
- }
-
- var speed: Double {
- get { totalSpeed }
- set { totalSpeed = newValue }
- }
-
- var formattedTotalSize: String {
- ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
- }
-
- var formattedDownloadedSize: String {
- ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file)
- }
-
- @Published var completedPackages: Int = 0
- @Published var totalPackages: Int = 0
-
- func setStatus(_ newStatus: DownloadStatus) {
- DispatchQueue.main.async {
- self.totalStatus = newStatus
- self.objectWillChange.send()
- }
- }
-
- func updateProgress(downloaded: Int64, total: Int64, speed: Double) {
- DispatchQueue.main.async {
- self.totalDownloadedSize = downloaded
- self.totalSize = total
- self.totalSpeed = speed
- self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0
- self.objectWillChange.send()
- }
- }
-
- init(sapCode: String, version: String, language: String, displayName: String, directory: URL, productsToDownload: [ProductsToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) {
- self.sapCode = sapCode
- self.version = version
- self.language = language
- self.displayName = displayName
- self.directory = directory
- self.productsToDownload = productsToDownload
- self.retryCount = retryCount
- self.createAt = createAt
- self.totalStatus = totalStatus
- self.totalProgress = totalProgress
- self.totalDownloadedSize = totalDownloadedSize
- self.totalSize = totalSize
- self.totalSpeed = totalSpeed
- self.currentPackage = currentPackage
- self.displayInstallButton = sapCode != "APRO"
- self.platform = platform
- }
-
- static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool {
- return lhs.id == rhs.id
- }
-}
+//class NewDownloadTask: Identifiable, ObservableObject, Equatable {
+// let id = UUID()
+// var sapCode: String
+// let version: String
+// let language: String
+// let displayName: String
+// let directory: URL
+// var productsToDownload: [ProductsToDownload]
+// var retryCount: Int
+// let createAt: Date
+// var displayInstallButton: Bool
+// @Published var totalStatus: DownloadStatus?
+// @Published var totalProgress: Double
+// @Published var totalDownloadedSize: Int64
+// @Published var totalSize: Int64
+// @Published var totalSpeed: Double
+// @Published var currentPackage: Package? {
+// didSet {
+// objectWillChange.send()
+// }
+// }
+// let platform: String
+//
+// var status: DownloadStatus {
+// totalStatus ?? .waiting
+// }
+//
+// var destinationURL: URL { directory }
+//
+// var downloadedSize: Int64 {
+// get { totalDownloadedSize }
+// set { totalDownloadedSize = newValue }
+// }
+//
+// var progress: Double {
+// get { totalProgress }
+// set { totalProgress = newValue }
+// }
+//
+// var speed: Double {
+// get { totalSpeed }
+// set { totalSpeed = newValue }
+// }
+//
+// var formattedTotalSize: String {
+// ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
+// }
+//
+// var formattedDownloadedSize: String {
+// ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file)
+// }
+//
+// @Published var completedPackages: Int = 0
+// @Published var totalPackages: Int = 0
+//
+// func setStatus(_ newStatus: DownloadStatus) {
+// DispatchQueue.main.async {
+// self.totalStatus = newStatus
+// self.objectWillChange.send()
+// }
+// }
+//
+// func updateProgress(downloaded: Int64, total: Int64, speed: Double) {
+// DispatchQueue.main.async {
+// self.totalDownloadedSize = downloaded
+// self.totalSize = total
+// self.totalSpeed = speed
+// self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0
+// self.objectWillChange.send()
+// }
+// }
+//
+// init(sapCode: String, version: String, language: String, displayName: String, directory: URL, productsToDownload: [ProductsToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) {
+// self.sapCode = sapCode
+// self.version = version
+// self.language = language
+// self.displayName = displayName
+// self.directory = directory
+// self.productsToDownload = productsToDownload
+// self.retryCount = retryCount
+// self.createAt = createAt
+// self.totalStatus = totalStatus
+// self.totalProgress = totalProgress
+// self.totalDownloadedSize = totalDownloadedSize
+// self.totalSize = totalSize
+// self.totalSpeed = totalSpeed
+// self.currentPackage = currentPackage
+// self.displayInstallButton = sapCode != "APRO"
+// self.platform = platform
+// }
+//
+// static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool {
+// return lhs.id == rhs.id
+// }
+//}
diff --git a/Adobe Downloader/Models/NewDownloadTask.swift b/Adobe Downloader/Models/NewDownloadTask.swift
new file mode 100644
index 0000000..818f485
--- /dev/null
+++ b/Adobe Downloader/Models/NewDownloadTask.swift
@@ -0,0 +1,99 @@
+//
+// NewDownloadTask.swift
+// Adobe Downloader
+//
+// Created by X1a0He on 2/26/25.
+//
+import Foundation
+
+class NewDownloadTask: Identifiable, ObservableObject {
+ let id = UUID()
+ var productId: String
+ let productVersion: String
+ let language: String
+ let displayName: String
+ let directory: URL
+ var dependenciesToDownload: [DependenciesToDownload]
+ var retryCount: Int
+ let createAt: Date
+ var displayInstallButton: Bool
+
+ let platform: String
+
+ @Published var totalStatus: DownloadStatus?
+ @Published var totalProgress: Double
+ @Published var totalDownloadedSize: Int64
+ @Published var totalSize: Int64
+ @Published var totalSpeed: Double
+ @Published var completedPackages: Int = 0
+ @Published var totalPackages: Int = 0
+ @Published var currentPackage: Package? {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ var status: DownloadStatus {
+ totalStatus ?? .waiting
+ }
+
+ var destinationURL: URL { directory }
+
+ var formattedTotalSize: String {
+ ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
+ }
+
+ var formattedDownloadedSize: String {
+ ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file)
+ }
+
+ var startTime: Date {
+ switch totalStatus {
+ case .downloading(let info): return info.startTime
+ case .completed(let info): return info.timestamp - info.totalTime
+ case .preparing(let info): return info.timestamp
+ case .paused(let info): return info.timestamp
+ case .failed(let info): return info.timestamp
+ case .retrying(let info): return info.nextRetryDate - 60
+ case .waiting, .none: return createAt
+ }
+ }
+
+ func setStatus(_ newStatus: DownloadStatus) {
+ DispatchQueue.main.async {
+ self.totalStatus = newStatus
+ self.objectWillChange.send()
+ }
+ }
+
+ func updateProgress(downloaded: Int64, total: Int64, speed: Double) {
+ DispatchQueue.main.async {
+ self.totalDownloadedSize = downloaded
+ self.totalSize = total
+ self.totalSpeed = speed
+ self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0
+ self.objectWillChange.send()
+ }
+ }
+
+ init(productId: String, productVersion: String, language: String, displayName: String, directory: URL, dependenciesToDownload: [DependenciesToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) {
+ self.productId = productId
+ self.productVersion = productVersion
+ self.language = language
+ self.displayName = displayName
+ self.directory = directory
+ self.dependenciesToDownload = dependenciesToDownload
+ self.retryCount = retryCount
+ self.createAt = createAt
+ self.totalStatus = totalStatus
+ self.totalProgress = totalProgress
+ self.totalDownloadedSize = totalDownloadedSize
+ self.totalSize = totalSize
+ self.totalSpeed = totalSpeed
+ self.currentPackage = currentPackage
+ self.displayInstallButton = productId != "APRO"
+ self.platform = platform
+ }
+
+
+}
diff --git a/Adobe Downloader/Services/NetworkService.swift b/Adobe Downloader/Services/NetworkService.swift
index 4280815..610e9b4 100644
--- a/Adobe Downloader/Services/NetworkService.swift
+++ b/Adobe Downloader/Services/NetworkService.swift
@@ -1,92 +1,91 @@
-import Foundation
-
-class NetworkService {
- typealias ProductsData = (products: [String: Sap], cdn: String, sapCodes: [SapCodes])
-
- private func makeProductsURL() throws -> URL {
- var components = URLComponents(string: NetworkConstants.productsXmlURL)
- components?.queryItems = [
- URLQueryItem(name: "channel", value: "ccm"),
- URLQueryItem(name: "channel", value: "sti"),
- URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"),
- URLQueryItem(name: "_type", value: "json"),
- URLQueryItem(name: "productType", value: "Desktop")
- ]
-
- guard let url = components?.url else {
- throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
- }
- return url
- }
-
- private func configureRequest(_ request: inout URLRequest, headers: [String: String]) {
- headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
- }
-
- func fetchProductsData() async throws -> ProductsData {
- let url = try makeProductsURL()
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
- configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders)
-
- let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse else {
- throw NetworkError.invalidResponse
- }
-
- guard (200...299).contains(httpResponse.statusCode) else {
- throw NetworkError.httpError(httpResponse.statusCode, nil)
- }
-
- guard let jsonString = String(data: data, encoding: .utf8) else {
- throw NetworkError.invalidData("无法解码JSON数据")
- }
-
- let result: ProductsData = try await Task.detached(priority: .userInitiated) {
- let parseResult = try JSONParser.parse(jsonString: jsonString)
- // 测试新API
- try NewJSONParser.parseStiProducts(jsonString: jsonString)
- try NewJSONParser.parseCcmProducts(jsonString: jsonString)
- let products = parseResult.products, cdn = parseResult.cdn
-
- let sapCodes = products.values
- .filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
- .map { SapCodes(sapCode: $0.sapCode, displayName: $0.displayName) }
-
- return (products, cdn, sapCodes)
- }.value
-
- return result
- }
-
- func getApplicationInfo(buildGuid: String) async throws -> String {
- guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
- throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
- }
-
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
-
- var headers = NetworkConstants.adobeRequestHeaders
- headers["x-adobe-build-guid"] = buildGuid
-
- headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
-
- let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse else {
- throw NetworkError.invalidResponse
- }
-
- guard (200...299).contains(httpResponse.statusCode) else {
- throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
- }
-
- guard let jsonString = String(data: data, encoding: .utf8) else {
- throw NetworkError.invalidData("无法将响应数据转换为json符串")
- }
-
- return jsonString
- }
-}
+//import Foundation
+//
+//class NetworkService {
+// typealias ProductsData = (products: [String: Sap], sapCodes: [SapCodes])
+//
+// private func makeProductsURL() throws -> URL {
+// var components = URLComponents(string: NetworkConstants.productsJSONURL)
+// components?.queryItems = [
+// URLQueryItem(name: "channel", value: "ccm"),
+// URLQueryItem(name: "channel", value: "sti"),
+// URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"),
+// URLQueryItem(name: "_type", value: "json"),
+// URLQueryItem(name: "productType", value: "Desktop")
+// ]
+//
+// guard let url = components?.url else {
+// throw NetworkError.invalidURL(NetworkConstants.productsJSONURL)
+// }
+// return url
+// }
+//
+// private func configureRequest(_ request: inout URLRequest, headers: [String: String]) {
+// headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+// }
+//
+// func fetchProductsData() async throws -> ProductsData {
+// let url = try makeProductsURL()
+// var request = URLRequest(url: url)
+// request.httpMethod = "GET"
+// configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders)
+//
+// let (data, response) = try await URLSession.shared.data(for: request)
+//
+// guard let httpResponse = response as? HTTPURLResponse else {
+// throw NetworkError.invalidResponse
+// }
+//
+// guard (200...299).contains(httpResponse.statusCode) else {
+// throw NetworkError.httpError(httpResponse.statusCode, nil)
+// }
+//
+// guard let jsonString = String(data: data, encoding: .utf8) else {
+// throw NetworkError.invalidData("无法解码JSON数据")
+// }
+//
+// let result: ProductsData = try await Task.detached(priority: .userInitiated) {
+// let parseResult = try JSONParser.parse(jsonString: jsonString)
+// // 测试新API
+// try NewJSONParser.parse(jsonString: jsonString)
+// let products = parseResult.products, cdn = parseResult.cdn
+//
+// let sapCodes = products.values
+// .filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
+// .map { SapCodes(sapCode: $0.sapCode, displayName: $0.displayName) }
+//
+// return (products, sapCodes)
+// }.value
+//
+// return result
+// }
+//
+// func getApplicationInfo(buildGuid: String) async throws -> String {
+// guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
+// throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
+// }
+//
+// var request = URLRequest(url: url)
+// request.httpMethod = "GET"
+//
+// var headers = NetworkConstants.adobeRequestHeaders
+// headers["x-adobe-build-guid"] = buildGuid
+//
+// headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+//
+// let (data, response) = try await URLSession.shared.data(for: request)
+//
+// guard let httpResponse = response as? HTTPURLResponse else {
+// throw NetworkError.invalidResponse
+// }
+//
+// guard (200...299).contains(httpResponse.statusCode) else {
+// throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
+// }
+//
+// guard let jsonString = String(data: data, encoding: .utf8) else {
+// throw NetworkError.invalidData("无法将响应数据转换为json符串")
+// }
+//
+// return jsonString
+// }
+//}
diff --git a/Adobe Downloader/Services/NewNetworkService.swift b/Adobe Downloader/Services/NewNetworkService.swift
new file mode 100644
index 0000000..bd0bffa
--- /dev/null
+++ b/Adobe Downloader/Services/NewNetworkService.swift
@@ -0,0 +1,107 @@
+//
+// NewNetworkService.swift
+// Adobe Downloader
+//
+// Created by X1a0He on 2/26/25.
+//
+
+import Foundation
+
+class NewNetworkService {
+ typealias ProductsData = ([Product], [UniqueProduct])
+
+ private func makeProductsURL() throws -> URL {
+ var components = URLComponents(string: NetworkConstants.productsJSONURL)
+ components?.queryItems = [
+ URLQueryItem(name: "channel", value: "ccm"),
+ URLQueryItem(name: "channel", value: "sti"),
+ URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"),
+ URLQueryItem(name: "_type", value: "json"),
+ URLQueryItem(name: "productType", value: "Desktop")
+ ]
+
+ guard let url = components?.url else {
+ throw NetworkError.invalidURL(NetworkConstants.productsJSONURL)
+ }
+ return url
+ }
+
+ private func configureRequest(_ request: inout URLRequest, headers: [String: String]) {
+ headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+ }
+
+ func fetchProductsData() async throws -> ProductsData {
+ let url = try makeProductsURL()
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+ configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders)
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw NetworkError.invalidResponse
+ }
+
+ guard (200...299).contains(httpResponse.statusCode) else {
+ throw NetworkError.httpError(httpResponse.statusCode, nil)
+ }
+
+ guard let jsonString = String(data: data, encoding: .utf8) else {
+ throw NetworkError.invalidData("无法解码JSON数据")
+ }
+
+ let result: ProductsData = try await Task.detached(priority: .userInitiated) {
+ try NewJSONParser.parse(jsonString: jsonString)
+
+ let products = globalCcmResult.products
+
+ if products.isEmpty {
+ return ([], [])
+ }
+
+ let validProducts = products.filter {
+ $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform)
+ }
+
+ var uniqueProductsDict = [String: UniqueProduct]()
+ for product in validProducts {
+ uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName)
+ }
+ let uniqueProducts = Array(uniqueProductsDict.values)
+
+ return (products, uniqueProducts)
+ }.value
+
+ return result
+ }
+
+ func getApplicationInfo(buildGuid: String) async throws -> String {
+ guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
+ throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
+ }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+
+ var headers = NetworkConstants.adobeRequestHeaders
+ headers["x-adobe-build-guid"] = buildGuid
+
+ headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw NetworkError.invalidResponse
+ }
+
+ guard (200...299).contains(httpResponse.statusCode) else {
+ throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
+ }
+
+ guard let jsonString = String(data: data, encoding: .utf8) else {
+ throw NetworkError.invalidData("无法将响应数据转换为json符串")
+ }
+
+ return jsonString
+ }
+}
diff --git a/Adobe Downloader/Utils/DownloadUtils.swift b/Adobe Downloader/Utils/DownloadUtils.swift
index dbc1239..094b5e8 100644
--- a/Adobe Downloader/Utils/DownloadUtils.swift
+++ b/Adobe Downloader/Utils/DownloadUtils.swift
@@ -1,1363 +1,1358 @@
+////
+//// Adobe Downloader
+////
+//// Created by X1a0He on 2024/10/30.
+////
+//import Foundation
+//import Network
+//import Combine
+//import AppKit
//
-// Adobe Downloader
+//class DownloadUtils {
+// typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64)
//
-// Created by X1a0He on 2024/10/30.
+// private weak var networkManager: NetworkManager?
+// private let cancelTracker: CancelTracker
//
-import Foundation
-import Network
-import Combine
-import AppKit
-
-class DownloadUtils {
- typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64)
-
- private weak var networkManager: NetworkManager?
- private let cancelTracker: CancelTracker
-
- init(networkManager: NetworkManager, cancelTracker: CancelTracker) {
- self.networkManager = networkManager
- self.cancelTracker = cancelTracker
- }
-
- private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
- var completionHandler: (URL?, URLResponse?, Error?) -> Void
- var progressHandler: ((Int64, Int64, Int64) -> Void)?
- var destinationDirectory: URL
- var fileName: String
- private var hasCompleted = false
- private let completionLock = NSLock()
- private var lastUpdateTime = Date()
- private var lastBytes: Int64 = 0
-
- init(destinationDirectory: URL,
- fileName: String,
- completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void,
- progressHandler: ((Int64, Int64, Int64) -> Void)? = nil) {
- self.destinationDirectory = destinationDirectory
- self.fileName = fileName
- self.completionHandler = completionHandler
- self.progressHandler = progressHandler
- super.init()
- }
-
- func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
- completionLock.lock()
- defer { completionLock.unlock() }
-
- guard !hasCompleted else { return }
- hasCompleted = true
-
- do {
- if !FileManager.default.fileExists(atPath: destinationDirectory.path) {
- try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
- }
-
- let destinationURL = destinationDirectory.appendingPathComponent(fileName)
-
- if FileManager.default.fileExists(atPath: destinationURL.path) {
- try FileManager.default.removeItem(at: destinationURL)
- }
-
- try FileManager.default.moveItem(at: location, to: destinationURL)
- completionHandler(destinationURL, downloadTask.response, nil)
-
- } catch {
- print("File operation error in delegate: \(error.localizedDescription)")
- completionHandler(nil, downloadTask.response, error)
- }
- }
-
- func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
- completionLock.lock()
- defer { completionLock.unlock() }
-
- guard !hasCompleted else { return }
- hasCompleted = true
-
- if let error = error {
- switch (error as NSError).code {
- case NSURLErrorCancelled:
- return
- case NSURLErrorTimedOut:
- completionHandler(nil, task.response, NetworkError.downloadError("下载超时", error))
- case NSURLErrorNotConnectedToInternet:
- completionHandler(nil, task.response, NetworkError.noConnection)
- default:
- completionHandler(nil, task.response, error)
- }
- }
- }
-
- func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
- didWriteData bytesWritten: Int64,
- totalBytesWritten: Int64,
- totalBytesExpectedToWrite: Int64) {
- guard totalBytesExpectedToWrite > 0 else { return }
- guard bytesWritten > 0 else { return }
-
- handleProgressUpdate(
- bytesWritten: bytesWritten,
- totalBytesWritten: totalBytesWritten,
- totalBytesExpectedToWrite: totalBytesExpectedToWrite
- )
- }
-
- func cleanup() {
- completionHandler = { _, _, _ in }
- progressHandler = nil
- }
-
- private func handleProgressUpdate(bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
- let now = Date()
- let timeDiff = now.timeIntervalSince(lastUpdateTime)
-
- guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
-
- Task {
- progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
- }
-
- lastUpdateTime = now
- lastBytes = totalBytesWritten
- }
- }
-
- func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
- let task = await cancelTracker.downloadTasks[taskId]
- if let downloadTask = task {
- let data = await withCheckedContinuation { continuation in
- downloadTask.cancel(byProducingResumeData: { data in
- continuation.resume(returning: data)
- })
- }
- if let data = data {
- await cancelTracker.storeResumeData(taskId, data: data)
- }
- }
-
- if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
- task.setStatus(.paused(DownloadStatus.PauseInfo(
- reason: reason,
- timestamp: Date(),
- resumable: true
- )))
- await networkManager?.saveTask(task)
- await MainActor.run {
- networkManager?.objectWillChange.send()
- }
- }
- }
-
- func resumeDownloadTask(taskId: UUID) async {
- if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
- task.setStatus(.downloading(DownloadStatus.DownloadInfo(
- fileName: task.currentPackage?.fullPackageName ?? "",
- currentPackageIndex: 0,
- totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count },
- startTime: Date(),
- estimatedTimeRemaining: nil
- )))
- await networkManager?.saveTask(task)
- await MainActor.run {
- networkManager?.objectWillChange.send()
- }
-
- if task.sapCode == "APRO" {
- if let resumeData = await cancelTracker.getResumeData(taskId),
- let currentPackage = task.currentPackage,
- let product = task.productsToDownload.first {
- try? await downloadPackage(
- package: currentPackage,
- task: task,
- product: product,
- resumeData: resumeData
- )
- }
- } else {
- await startDownloadProcess(task: task)
- }
- }
- }
-
- func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
- await cancelTracker.cancel(taskId)
-
- if let task = await networkManager?.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 networkManager?.saveTask(task)
- await MainActor.run {
- networkManager?.updateDockBadge()
- networkManager?.objectWillChange.send()
- }
- }
- }
-
-
- func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Sap.Versions, displayName: String) -> String {
- let dependencies = productInfo.dependencies.map { dependency in
- """
-
- \(dependency.sapCode)
- \(dependency.version)
- \(dependency.sapCode)
-
- """
- }.joined(separator: "\n")
-
- return """
-
-
- Adobe \(displayName)
- \(sapCode)
- \(version)
- \(productInfo.apPlatform)
- \(sapCode)
-
- \(dependencies)
-
-
-
- /Applications
- \(language)
-
-
- """
- }
-
- 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)
- }
- }
- }
-
- private func downloadPackage(package: Package, task: NewDownloadTask, product: ProductsToDownload, url: URL? = nil, resumeData: Data? = nil) async throws {
- var lastUpdateTime = Date()
- var lastBytes: Int64 = 0
-
- return try await withCheckedThrowingContinuation { continuation in
- let delegate = DownloadDelegate(
- destinationDirectory: task.directory.appendingPathComponent(product.sapCode),
- fileName: package.fullPackageName,
- completionHandler: { [weak networkManager] localURL, response, error in
- if let error = error {
- if (error as NSError).code == NSURLErrorCancelled {
- continuation.resume()
- } else {
- continuation.resume(throwing: error)
- }
- return
- }
-
- Task {
- await MainActor.run {
- package.downloadedSize = package.downloadSize
- package.progress = 1.0
- package.status = .completed
- package.downloaded = true
-
- var totalDownloaded: Int64 = 0
- var totalSize: Int64 = 0
-
- for prod in task.productsToDownload {
- for pkg in prod.packages {
- totalSize += pkg.downloadSize
- if pkg.downloaded {
- totalDownloaded += pkg.downloadSize
- }
- }
- }
-
- task.totalSize = totalSize
- task.totalDownloadedSize = totalDownloaded
- task.totalProgress = Double(totalDownloaded) / Double(totalSize)
- task.totalSpeed = 0
-
- let allCompleted = task.productsToDownload.allSatisfy {
- product in product.packages.allSatisfy { $0.downloaded }
- }
-
- if allCompleted {
- task.setStatus(.completed(DownloadStatus.CompletionInfo(
- timestamp: Date(),
- totalTime: Date().timeIntervalSince(task.createAt),
- totalSize: totalSize
- )))
- }
-
- product.updateCompletedPackages()
- }
- await networkManager?.saveTask(task)
- await MainActor.run {
- networkManager?.objectWillChange.send()
- }
- continuation.resume()
- }
- },
- progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
- Task { @MainActor in
- let now = Date()
- let timeDiff = now.timeIntervalSince(lastUpdateTime)
-
- if timeDiff >= 1.0 {
- let bytesDiff = totalBytesWritten - lastBytes
- let speed = Double(bytesDiff) / timeDiff
-
- package.updateProgress(
- downloadedSize: totalBytesWritten,
- speed: speed
- )
-
- var totalDownloaded: Int64 = 0
- var totalSize: Int64 = 0
- var currentSpeed: Double = 0
-
- for prod in task.productsToDownload {
- for pkg in prod.packages {
- totalSize += pkg.downloadSize
- if pkg.downloaded {
- totalDownloaded += pkg.downloadSize
- } else if pkg.id == package.id {
- totalDownloaded += totalBytesWritten
- currentSpeed = speed
- }
- }
- }
-
- task.totalSize = totalSize
- task.totalDownloadedSize = totalDownloaded
- task.totalProgress = totalSize > 0 ? Double(totalDownloaded) / Double(totalSize) : 0
- task.totalSpeed = currentSpeed
-
- lastUpdateTime = now
- lastBytes = totalBytesWritten
-
- networkManager?.objectWillChange.send()
- }
- }
- }
- )
-
- let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
-
- Task {
- let downloadTask: URLSessionDownloadTask
- if let resumeData = resumeData {
- downloadTask = session.downloadTask(withResumeData: resumeData)
- } else if let url = url {
- var request = URLRequest(url: url)
- NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
- downloadTask = session.downloadTask(with: request)
- } else {
- continuation.resume(throwing: NetworkError.invalidData("Neither URL nor resume data provided"))
- return
- }
-
- await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
- await cancelTracker.clearResumeData(task.id)
- downloadTask.resume()
- }
- }
- }
-
- private func startDownloadProcess(task: NewDownloadTask) async {
- actor DownloadProgress {
- var currentPackageIndex: Int = 0
- func increment() { currentPackageIndex += 1 }
- func get() -> Int { return currentPackageIndex }
- }
-
- let progress = DownloadProgress()
-
- await MainActor.run {
- let totalPackages = task.productsToDownload.reduce(0) { $0 + $1.packages.count }
- task.setStatus(.downloading(DownloadStatus.DownloadInfo(
- fileName: task.currentPackage?.fullPackageName ?? "",
- currentPackageIndex: 0,
- totalPackages: totalPackages,
- startTime: Date(),
- estimatedTimeRemaining: nil
- )))
- task.objectWillChange.send()
- }
-
- let driverPath = task.directory.appendingPathComponent("driver.xml")
- if !FileManager.default.fileExists(atPath: driverPath.path) {
- if let productInfo = await networkManager?.saps[task.sapCode]?.versions[task.version] {
- let driverXml = generateDriverXML(
- sapCode: task.sapCode,
- version: task.version,
- language: task.language,
- productInfo: productInfo,
- displayName: task.displayName
- )
- do {
- try driverXml.write(to: driverPath, atomically: true, encoding: .utf8)
- } catch {
- print("Error generating driver.xml:", error.localizedDescription)
- await MainActor.run {
- task.setStatus(.failed(DownloadStatus.FailureInfo(
- message: "生成 driver.xml 失败: \(error.localizedDescription)",
- error: error,
- timestamp: Date(),
- recoverable: false
- )))
- }
- return
- }
- }
- }
-
- for product in task.productsToDownload {
- let productDir = task.directory.appendingPathComponent(product.sapCode)
- if !FileManager.default.fileExists(atPath: productDir.path) {
- do {
- try FileManager.default.createDirectory(
- at: productDir,
- withIntermediateDirectories: true,
- attributes: nil
- )
- } catch {
- print("Error creating directory for \(product.sapCode): \(error)")
- continue
- }
- }
- }
-
- for product in task.productsToDownload {
- for package in product.packages where !package.downloaded {
- let currentIndex = await progress.get()
-
- await MainActor.run {
- task.currentPackage = package
- task.setStatus(.downloading(DownloadStatus.DownloadInfo(
- fileName: package.fullPackageName,
- currentPackageIndex: currentIndex,
- totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count },
- startTime: Date(),
- estimatedTimeRemaining: nil
- )))
- }
- await networkManager?.saveTask(task)
-
- await progress.increment()
-
- guard !package.fullPackageName.isEmpty,
- !package.downloadURL.isEmpty,
- package.downloadSize > 0 else {
- continue
- }
-
- let cdn = await networkManager?.cdn ?? ""
- let cleanCdn = cdn.hasSuffix("/") ? String(cdn.dropLast()) : cdn
- let cleanPath = package.downloadURL.hasPrefix("/") ? package.downloadURL : "/\(package.downloadURL)"
- let downloadURL = cleanCdn + cleanPath
-
- guard let url = URL(string: downloadURL) else { continue }
-
- do {
- if let resumeData = await cancelTracker.getResumeData(task.id) {
- try await downloadPackage(package: package, task: task, product: product, resumeData: resumeData)
- } else {
- try await downloadPackage(package: package, task: task, product: product, url: url)
- }
- } catch {
- print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)")
- await handleError(task.id, error)
- return
- }
- }
- }
-
- let allPackagesDownloaded = task.productsToDownload.allSatisfy { product in
- product.packages.allSatisfy { $0.downloaded }
- }
-
- if allPackagesDownloaded {
- await MainActor.run {
- task.setStatus(.completed(DownloadStatus.CompletionInfo(
- timestamp: Date(),
- totalTime: Date().timeIntervalSince(task.createAt),
- totalSize: task.totalSize
- )))
- }
- await networkManager?.saveTask(task)
- }
- }
-
- func retryPackage(task: NewDownloadTask, package: Package) async throws {
- guard package.canRetry else { return }
-
- package.prepareForRetry()
-
- if let product = task.productsToDownload.first(where: { $0.packages.contains(where: { $0.id == package.id }) }) {
- await MainActor.run {
- task.currentPackage = package
- }
-
- if let cdn = await networkManager?.cdnUrl {
- try await downloadPackage(package: package, task: task, product: product, url: URL(string: cdn + package.downloadURL)!)
- } else {
- throw NetworkError.invalidData("无法取 CDN 地址")
- }
- }
- }
-
- func downloadAPRO(task: NewDownloadTask, productInfo: Sap.Versions) async throws {
- guard let networkManager = networkManager else { return }
-
- let manifestURL = await networkManager.cdnUrl + productInfo.buildGuid
- guard let url = URL(string: manifestURL) else {
- throw NetworkError.invalidURL(manifestURL)
- }
-
- var request = URLRequest(url: url)
- NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
-
- let (manifestData, _) = try await URLSession.shared.data(for: request)
-
- let manifestDoc = try XMLDocument(data: manifestData)
-
- guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
- let assetSizeStr = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_size").first?.stringValue,
- let assetSize = Int64(assetSizeStr) else {
- throw NetworkError.invalidData("无法从manifest中获取下载信息")
- }
-
- guard let downloadURL = URL(string: downloadPath) else {
- throw NetworkError.invalidURL(downloadPath)
- }
-
- let aproPackage = Package(
- type: "dmg",
- fullPackageName: "Adobe Downloader \(task.sapCode)_\(productInfo.productVersion)_\(productInfo.apPlatform).dmg",
- downloadSize: assetSize,
- downloadURL: downloadPath,
- packageVersion: ""
- )
-
- await MainActor.run {
- let product = ProductsToDownload(
- sapCode: task.sapCode,
- version: task.version,
- buildGuid: productInfo.buildGuid
- )
- product.packages = [aproPackage]
- task.productsToDownload = [product]
- task.totalSize = assetSize
- task.currentPackage = aproPackage
- task.setStatus(.downloading(DownloadStatus.DownloadInfo(
- fileName: aproPackage.fullPackageName,
- currentPackageIndex: 0,
- totalPackages: 1,
- startTime: Date(),
- estimatedTimeRemaining: nil
- )))
- }
-
- let tempDownloadDir = task.directory.deletingLastPathComponent()
- var lastUpdateTime = Date()
- var lastBytes: Int64 = 0
-
- return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
- let delegate = DownloadDelegate(
- destinationDirectory: tempDownloadDir,
- fileName: aproPackage.fullPackageName,
- completionHandler: { [weak networkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
- if let error = error {
- if (error as NSError).code == NSURLErrorCancelled {
- continuation.resume(throwing: NetworkError.cancelled)
- return
- }
- print("Download error:", error)
- continuation.resume(throwing: error)
- return
- }
-
- Task {
- await MainActor.run {
- aproPackage.downloadedSize = aproPackage.downloadSize
- aproPackage.progress = 1.0
- aproPackage.status = .completed
- aproPackage.downloaded = true
-
- var totalDownloaded: Int64 = 0
- var totalSize: Int64 = 0
-
- totalSize += aproPackage.downloadSize
- if aproPackage.downloaded {
- totalDownloaded += aproPackage.downloadSize
- }
-
- task.totalSize = totalSize
- task.totalDownloadedSize = totalDownloaded
- task.totalProgress = Double(totalDownloaded) / Double(totalSize)
- task.totalSpeed = 0
-
- task.setStatus(.completed(DownloadStatus.CompletionInfo(
- timestamp: Date(),
- totalTime: Date().timeIntervalSince(task.createAt),
- totalSize: totalSize
- )))
-
- task.objectWillChange.send()
- }
-
- await networkManager?.saveTask(task)
-
- await MainActor.run {
- networkManager?.updateDockBadge()
- networkManager?.objectWillChange.send()
- }
- continuation.resume()
- }
- },
- progressHandler: { [weak networkManager] (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) in
- Task { @MainActor in
- let now = Date()
- let timeDiff = now.timeIntervalSince(lastUpdateTime)
-
- if timeDiff >= 1.0 {
- let bytesDiff = totalBytesWritten - lastBytes
- let speed = Double(bytesDiff) / timeDiff
-
- aproPackage.updateProgress(
- downloadedSize: totalBytesWritten,
- speed: speed
- )
-
- task.totalDownloadedSize = totalBytesWritten
- task.totalProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
- task.totalSpeed = speed
-
- lastUpdateTime = now
- lastBytes = totalBytesWritten
-
- task.objectWillChange.send()
- networkManager?.objectWillChange.send()
-
- Task {
- await networkManager?.saveTask(task)
- }
- }
- }
- }
- )
-
- let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
-
- var downloadRequest = URLRequest(url: downloadURL)
- NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
-
- let downloadTask = session.downloadTask(with: downloadRequest)
-
- Task {
- await self.cancelTracker.registerTask(task.id, task: downloadTask, session: session)
-
- if await self.cancelTracker.isCancelled(task.id) {
- continuation.resume(throwing: NetworkError.cancelled)
- return
- }
-
- downloadTask.resume()
- }
- }
- }
-
- func handleDownload(task: NewDownloadTask, productInfo: Sap.Versions, allowedPlatform: [String], saps: [String: Sap]) async throws {
- if task.sapCode == "APRO" {
- try await downloadAPRO(task: task, productInfo: productInfo)
- return
- }
-
- var productsToDownload: [ProductsToDownload] = []
-
- productsToDownload.append(ProductsToDownload(
- sapCode: task.sapCode,
- version: task.version,
- buildGuid: productInfo.buildGuid
- ))
-
- for dependency in productInfo.dependencies {
- if let dependencyVersions = saps[dependency.sapCode]?.versions {
- var processedVersions = Set()
- var firstGuid: String?
- var buildGuid: String?
-
- let sortedVersions = dependencyVersions.sorted { version1, version2 in
- let v1Components = version1.key.split(separator: ".").compactMap { Int($0) }
- let v2Components = version2.key.split(separator: ".").compactMap { Int($0) }
-
- for i in 0.. v2Components[i]
- }
- }
- return v1Components.count > v2Components.count
- }
-
- for version in sortedVersions {
- if buildGuid != nil { break }
-
- if processedVersions.contains(version.key) { continue }
- processedVersions.insert(version.key)
-
- if version.value.baseVersion == dependency.version {
- if firstGuid == nil { firstGuid = version.value.buildGuid }
-
- print("\(version.value.sapCode), \(version.key), \(allowedPlatform), \(version.value.apPlatform), \(allowedPlatform.contains(version.value.apPlatform))")
-
- if allowedPlatform.contains(version.value.apPlatform) {
- buildGuid = version.value.buildGuid
- break
- }
- }
- }
-
- if buildGuid == nil { buildGuid = firstGuid }
- if let finalBuildGuid = buildGuid {
- let alreadyAdded = productsToDownload.contains { product in
- product.sapCode == dependency.sapCode &&
- product.version == dependency.version
- }
-
- if !alreadyAdded {
- productsToDownload.append(ProductsToDownload(
- sapCode: dependency.sapCode,
- version: dependency.version,
- buildGuid: finalBuildGuid
- ))
- }
- }
- }
- }
-
- for product in productsToDownload {
- print("\(product.sapCode), \(product.version), \(product.buildGuid)")
- }
-
- for product in productsToDownload {
- await MainActor.run {
- task.setStatus(.preparing(DownloadStatus.PrepareInfo(
- message: String(localized: "正在处理 \(product.sapCode) 的包信息..."),
- timestamp: Date(),
- stage: .fetchingInfo
- )))
- }
-
- let jsonString = try await getApplicationInfo(buildGuid: product.buildGuid)
- let productDir = task.directory.appendingPathComponent("\(product.sapCode)")
- if !FileManager.default.fileExists(atPath: productDir.path) {
- try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
- }
- let jsonURL = productDir.appendingPathComponent("application.json")
- try jsonString.write(to: jsonURL, atomically: true, encoding: .utf8)
-
- guard let jsonData = jsonString.data(using: .utf8),
- let appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
- let packages = appInfo["Packages"] as? [String: Any],
- let packageArray = packages["Package"] as? [[String: Any]] else {
- throw NetworkError.invalidData("无法解析产品信息")
- }
-
- var corePackageCount = 0
- var nonCorePackageCount = 0
-
- /*
- 这里是对包的过滤,一般规则在
- 1. 如果没有Condition,那么就视为需要下载的包
- 2. 如果存在Condition,那么按照以下规则下载
- [OSVersion]>=10.15 : 系统版本大于等于10.15就下载,所以需要一个函数来获取系统版本号
- [OSArchitecture]==arm64 : 系统架构为arm64的就下载,官方并没有下载另外一个架构的包
- [OSArchitecture]==x64 : 同上
- [installLanguage]==zh_CN : 目标安装语言为 zh_CN 的就下载
-
- PS: 下面是留给看源码的人的
- 哪怕是官方的ACC下载任何一款App,都是这个逻辑,不信自己去翻,你可能会说,为什么官方能下通用的,你问这个问题之前,可以自己去拿正版的看看他是怎么下载的,他下载的包数量跟我的是不是一致的,他也只是下载了对应架构的包
-
- 其实要下载通用的也很简单,不是判断架构吗,那下载通用的时候,两个架构同时成立不就好了,但我并没有在官方的下载逻辑中看到,也没尝试过,如果你尝试之后发现可以,请你告诉我
- */
-
- for package in packageArray {
- var shouldDownload = false
- let packageType = package["Type"] as? String ?? "non-core"
- let isCore = packageType == "core"
-
- guard let downloadURL = package["Path"] as? String, !downloadURL.isEmpty else { continue }
-
- let fullPackageName: String
- let packageVersion: String
- if let name = package["fullPackageName"] as? String, !name.isEmpty {
- fullPackageName = name
- packageVersion = package["PackageVersion"] as? String ?? ""
- } else if let name = package["PackageName"] as? String, !name.isEmpty {
- fullPackageName = "\(name).zip"
- packageVersion = package["PackageVersion"] as? String ?? ""
- } else {
- continue
- }
-
- let downloadSize: Int64
- if let sizeNumber = package["DownloadSize"] as? NSNumber {
- downloadSize = sizeNumber.int64Value
- } else if let sizeString = package["DownloadSize"] as? String,
- let parsedSize = Int64(sizeString) {
- downloadSize = parsedSize
- } else if let sizeInt = package["DownloadSize"] as? Int {
- downloadSize = Int64(sizeInt)
- } else { continue }
-
- let installLanguage = "[installLanguage]==\(task.language)"
- if let condition = package["Condition"] as? String {
- if condition.isEmpty {
- shouldDownload = true
- } else {
- if condition.contains("[OSVersion]") {
- let osVersion = ProcessInfo.processInfo.operatingSystemVersion
- let currentVersion = Double("\(osVersion.majorVersion).\(osVersion.minorVersion)") ?? 0.0
-
- let versionPattern = #"\[OSVersion\](>=|<=|<|>|==)([\d.]+)"#
- let regex = try? NSRegularExpression(pattern: versionPattern)
- let range = NSRange(condition.startIndex.. 0 ? pkg.downloadSize : 0)
- }
- }
-
- await MainActor.run {
- task.productsToDownload = finalProducts
- task.totalSize = totalSize
- }
-
- await startDownloadProcess(task: task)
- }
-
- func getApplicationInfo(buildGuid: String) async throws -> String {
- guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
- throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
- }
-
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
-
- var headers = NetworkConstants.adobeRequestHeaders
- headers["x-adobe-build-guid"] = buildGuid
-
- headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
-
- let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse else {
- throw NetworkError.invalidResponse
- }
-
- guard (200...299).contains(httpResponse.statusCode) else {
- throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
- }
-
- guard let jsonString = String(data: data, encoding: .utf8) else {
- throw NetworkError.invalidData(String(localized: "无法将响应数据转换为json字符串"))
- }
-
- return jsonString
- }
-
- func handleError(_ taskId: UUID, _ error: Error) async {
- guard let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) else { return }
-
- let (errorMessage, isRecoverable) = classifyError(error)
-
- if isRecoverable,
- let downloadTask = await cancelTracker.downloadTasks[taskId] {
- let resumeData = await withCheckedContinuation { continuation in
- downloadTask.cancel(byProducingResumeData: { data in
- continuation.resume(returning: data)
- })
- }
- if let resumeData = resumeData {
- await cancelTracker.storeResumeData(taskId, data: resumeData)
- }
- }
-
- if isRecoverable && task.retryCount < NetworkConstants.maxRetryAttempts {
- task.retryCount += 1
- let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000))
- task.setStatus(.retrying(DownloadStatus.RetryInfo(
- attempt: task.retryCount,
- maxAttempts: NetworkConstants.maxRetryAttempts,
- reason: errorMessage,
- nextRetryDate: nextRetryDate
- )))
-
- Task {
- do {
- try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
- if await !cancelTracker.isCancelled(taskId) {
- await resumeDownloadTask(taskId: taskId)
- }
- } catch {
- print("Retry cancelled for task: \(taskId)")
- }
- }
- } else {
- task.setStatus(.failed(DownloadStatus.FailureInfo(
- message: errorMessage,
- error: error,
- timestamp: Date(),
- recoverable: isRecoverable
- )))
-
- if let currentPackage = task.currentPackage {
- let destinationDir = task.directory
- .appendingPathComponent("\(task.sapCode)")
- let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
- try? FileManager.default.removeItem(at: fileURL)
- }
-
- await networkManager?.saveTask(task)
- await MainActor.run {
- networkManager?.updateDockBadge()
- networkManager?.objectWillChange.send()
- }
- }
- }
-
- private func classifyError(_ error: Error) -> (message: String, recoverable: Bool) {
- switch error {
- case let networkError as NetworkError:
- switch networkError {
- case .noConnection:
- return (String(localized: "网络连接已断开"), true)
- case .timeout:
- return (String(localized: "下载超时"), true)
- case .serverUnreachable:
- return (String(localized: "服务器无法访问"), true)
- case .insufficientStorage:
- return (String(localized: "存储空间不足"), false)
- case .filePermissionDenied:
- return (String(localized: "没有写入权限"), false)
- default:
- return (networkError.localizedDescription, false)
- }
- case let urlError as URLError:
- switch urlError.code {
- case .notConnectedToInternet, .networkConnectionLost, .dataNotAllowed:
- return (String(localized: "网络连接已断开"), true)
- case .timedOut:
- return (String(localized: "连接超时"), true)
- case .cancelled:
- return (String(localized: "下载已取消"), false)
- case .cannotConnectToHost, .dnsLookupFailed:
- return (String(localized: "无法连接到服务器"), true)
- default:
- return (urlError.localizedDescription, true)
- }
- default:
- return (error.localizedDescription, false)
- }
- }
-
- @MainActor
- func updateProgress(for taskId: UUID, progress: ProgressUpdate) {
- guard let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }),
- let currentPackage = task.currentPackage else { return }
-
- let now = Date()
- let timeDiff = now.timeIntervalSince(currentPackage.lastUpdated)
-
- if timeDiff >= NetworkConstants.progressUpdateInterval {
- currentPackage.updateProgress(
- downloadedSize: progress.totalWritten,
- speed: Double(progress.bytesWritten)
- )
-
- let totalDownloaded = task.productsToDownload.reduce(Int64(0)) { sum, prod in
- sum + prod.packages.reduce(Int64(0)) { sum, pkg in
- if pkg.downloaded {
- return sum + pkg.downloadSize
- } else if pkg.id == currentPackage.id {
- return sum + progress.totalWritten
- }
- return sum
- }
- }
-
- task.totalDownloadedSize = totalDownloaded
- task.totalProgress = Double(totalDownloaded) / Double(task.totalSize)
- task.totalSpeed = currentPackage.speed
-
- currentPackage.lastRecordedSize = progress.totalWritten
- currentPackage.lastUpdated = now
-
- task.objectWillChange.send()
- networkManager?.objectWillChange.send()
- }
- }
-
- @MainActor
- func updateTaskStatus(_ taskId: UUID, _ status: DownloadStatus) async {
- guard let networkManager = networkManager else { return }
-
- if let index = networkManager.downloadTasks.firstIndex(where: { $0.id == taskId }) {
- networkManager.downloadTasks[index].setStatus(status)
-
- switch status {
- case .completed, .failed:
- networkManager.progressObservers[taskId]?.invalidate()
- networkManager.progressObservers.removeValue(forKey: taskId)
- if networkManager.activeDownloadTaskId == taskId {
- networkManager.activeDownloadTaskId = nil
- }
-
- case .downloading:
- networkManager.activeDownloadTaskId = taskId
-
- case .paused:
- if networkManager.activeDownloadTaskId == taskId {
- networkManager.activeDownloadTaskId = nil
- }
-
- default:
- break
- }
-
- networkManager.updateDockBadge()
- networkManager.objectWillChange.send()
- }
- }
-
- 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
- }
- }
-
- 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")
- }
- }
-
- private func compareVersions(current: Double, required: Double, operator: String) -> Bool {
- switch `operator` {
- case ">=":
- return current >= required
- case "<=":
- return current <= required
- case ">":
- return current > required
- case "<":
- return current < required
- case "==":
- return current == required
- default:
- return false
- }
- }
-}
+// init(networkManager: NetworkManager, cancelTracker: CancelTracker) {
+// self.networkManager = networkManager
+// self.cancelTracker = cancelTracker
+// }
+//
+// private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
+// var completionHandler: (URL?, URLResponse?, Error?) -> Void
+// var progressHandler: ((Int64, Int64, Int64) -> Void)?
+// var destinationDirectory: URL
+// var fileName: String
+// private var hasCompleted = false
+// private let completionLock = NSLock()
+// private var lastUpdateTime = Date()
+// private var lastBytes: Int64 = 0
+//
+// init(destinationDirectory: URL,
+// fileName: String,
+// completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void,
+// progressHandler: ((Int64, Int64, Int64) -> Void)? = nil) {
+// self.destinationDirectory = destinationDirectory
+// self.fileName = fileName
+// self.completionHandler = completionHandler
+// self.progressHandler = progressHandler
+// super.init()
+// }
+//
+// func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+// completionLock.lock()
+// defer { completionLock.unlock() }
+//
+// guard !hasCompleted else { return }
+// hasCompleted = true
+//
+// do {
+// if !FileManager.default.fileExists(atPath: destinationDirectory.path) {
+// try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
+// }
+//
+// let destinationURL = destinationDirectory.appendingPathComponent(fileName)
+//
+// if FileManager.default.fileExists(atPath: destinationURL.path) {
+// try FileManager.default.removeItem(at: destinationURL)
+// }
+//
+// try FileManager.default.moveItem(at: location, to: destinationURL)
+// completionHandler(destinationURL, downloadTask.response, nil)
+//
+// } catch {
+// print("File operation error in delegate: \(error.localizedDescription)")
+// completionHandler(nil, downloadTask.response, error)
+// }
+// }
+//
+// func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+// completionLock.lock()
+// defer { completionLock.unlock() }
+//
+// guard !hasCompleted else { return }
+// hasCompleted = true
+//
+// if let error = error {
+// switch (error as NSError).code {
+// case NSURLErrorCancelled:
+// return
+// case NSURLErrorTimedOut:
+// completionHandler(nil, task.response, NetworkError.downloadError("下载超时", error))
+// case NSURLErrorNotConnectedToInternet:
+// completionHandler(nil, task.response, NetworkError.noConnection)
+// default:
+// completionHandler(nil, task.response, error)
+// }
+// }
+// }
+//
+// func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
+// didWriteData bytesWritten: Int64,
+// totalBytesWritten: Int64,
+// totalBytesExpectedToWrite: Int64) {
+// guard totalBytesExpectedToWrite > 0 else { return }
+// guard bytesWritten > 0 else { return }
+//
+// handleProgressUpdate(
+// bytesWritten: bytesWritten,
+// totalBytesWritten: totalBytesWritten,
+// totalBytesExpectedToWrite: totalBytesExpectedToWrite
+// )
+// }
+//
+// func cleanup() {
+// completionHandler = { _, _, _ in }
+// progressHandler = nil
+// }
+//
+// private func handleProgressUpdate(bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+// let now = Date()
+// let timeDiff = now.timeIntervalSince(lastUpdateTime)
+//
+// guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
+//
+// Task {
+// progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
+// }
+//
+// lastUpdateTime = now
+// lastBytes = totalBytesWritten
+// }
+// }
+//
+// func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
+// let task = await cancelTracker.downloadTasks[taskId]
+// if let downloadTask = task {
+// let data = await withCheckedContinuation { continuation in
+// downloadTask.cancel(byProducingResumeData: { data in
+// continuation.resume(returning: data)
+// })
+// }
+// if let data = data {
+// await cancelTracker.storeResumeData(taskId, data: data)
+// }
+// }
+//
+// if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
+// task.setStatus(.paused(DownloadStatus.PauseInfo(
+// reason: reason,
+// timestamp: Date(),
+// resumable: true
+// )))
+// await networkManager?.saveTask(task)
+// await MainActor.run {
+// networkManager?.objectWillChange.send()
+// }
+// }
+// }
+//
+// func resumeDownloadTask(taskId: UUID) async {
+// if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
+// task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+// fileName: task.currentPackage?.fullPackageName ?? "",
+// currentPackageIndex: 0,
+// totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count },
+// startTime: Date(),
+// estimatedTimeRemaining: nil
+// )))
+// await networkManager?.saveTask(task)
+// await MainActor.run {
+// networkManager?.objectWillChange.send()
+// }
+//
+// if task.sapCode == "APRO" {
+// if let resumeData = await cancelTracker.getResumeData(taskId),
+// let currentPackage = task.currentPackage,
+// let product = task.productsToDownload.first {
+// try? await downloadPackage(
+// package: currentPackage,
+// task: task,
+// product: product,
+// resumeData: resumeData
+// )
+// }
+// } else {
+// await startDownloadProcess(task: task)
+// }
+// }
+// }
+//
+// func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
+// await cancelTracker.cancel(taskId)
+//
+// if let task = await networkManager?.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 networkManager?.saveTask(task)
+// await MainActor.run {
+// networkManager?.updateDockBadge()
+// networkManager?.objectWillChange.send()
+// }
+// }
+// }
+//
+//
+// func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Sap.Versions, displayName: String) -> String {
+// let dependencies = productInfo.dependencies.map { dependency in
+// """
+//
+// \(dependency.sapCode)
+// \(dependency.version)
+// \(dependency.sapCode)
+//
+// """
+// }.joined(separator: "\n")
+//
+// return """
+//
+//
+// Adobe \(displayName)
+// \(sapCode)
+// \(version)
+// \(productInfo.apPlatform)
+// \(sapCode)
+//
+// \(dependencies)
+//
+//
+//
+// /Applications
+// \(language)
+//
+//
+// """
+// }
+//
+// 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)
+// }
+// }
+// }
+//
+// private func downloadPackage(package: Package, task: NewDownloadTask, product: ProductsToDownload, url: URL? = nil, resumeData: Data? = nil) async throws {
+// var lastUpdateTime = Date()
+// var lastBytes: Int64 = 0
+//
+// return try await withCheckedThrowingContinuation { continuation in
+// let delegate = DownloadDelegate(
+// destinationDirectory: task.directory.appendingPathComponent(product.sapCode),
+// fileName: package.fullPackageName,
+// completionHandler: { [weak networkManager] localURL, response, error in
+// if let error = error {
+// if (error as NSError).code == NSURLErrorCancelled {
+// continuation.resume()
+// } else {
+// continuation.resume(throwing: error)
+// }
+// return
+// }
+//
+// Task {
+// await MainActor.run {
+// package.downloadedSize = package.downloadSize
+// package.progress = 1.0
+// package.status = .completed
+// package.downloaded = true
+//
+// var totalDownloaded: Int64 = 0
+// var totalSize: Int64 = 0
+//
+// for prod in task.productsToDownload {
+// for pkg in prod.packages {
+// totalSize += pkg.downloadSize
+// if pkg.downloaded {
+// totalDownloaded += pkg.downloadSize
+// }
+// }
+// }
+//
+// task.totalSize = totalSize
+// task.totalDownloadedSize = totalDownloaded
+// task.totalProgress = Double(totalDownloaded) / Double(totalSize)
+// task.totalSpeed = 0
+//
+// let allCompleted = task.productsToDownload.allSatisfy {
+// product in product.packages.allSatisfy { $0.downloaded }
+// }
+//
+// if allCompleted {
+// task.setStatus(.completed(DownloadStatus.CompletionInfo(
+// timestamp: Date(),
+// totalTime: Date().timeIntervalSince(task.createAt),
+// totalSize: totalSize
+// )))
+// }
+//
+// product.updateCompletedPackages()
+// }
+// await networkManager?.saveTask(task)
+// await MainActor.run {
+// networkManager?.objectWillChange.send()
+// }
+// continuation.resume()
+// }
+// },
+// progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
+// Task { @MainActor in
+// let now = Date()
+// let timeDiff = now.timeIntervalSince(lastUpdateTime)
+//
+// if timeDiff >= 1.0 {
+// let bytesDiff = totalBytesWritten - lastBytes
+// let speed = Double(bytesDiff) / timeDiff
+//
+// package.updateProgress(
+// downloadedSize: totalBytesWritten,
+// speed: speed
+// )
+//
+// var totalDownloaded: Int64 = 0
+// var totalSize: Int64 = 0
+// var currentSpeed: Double = 0
+//
+// for prod in task.productsToDownload {
+// for pkg in prod.packages {
+// totalSize += pkg.downloadSize
+// if pkg.downloaded {
+// totalDownloaded += pkg.downloadSize
+// } else if pkg.id == package.id {
+// totalDownloaded += totalBytesWritten
+// currentSpeed = speed
+// }
+// }
+// }
+//
+// task.totalSize = totalSize
+// task.totalDownloadedSize = totalDownloaded
+// task.totalProgress = totalSize > 0 ? Double(totalDownloaded) / Double(totalSize) : 0
+// task.totalSpeed = currentSpeed
+//
+// lastUpdateTime = now
+// lastBytes = totalBytesWritten
+//
+// networkManager?.objectWillChange.send()
+// }
+// }
+// }
+// )
+//
+// let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
+//
+// Task {
+// let downloadTask: URLSessionDownloadTask
+// if let resumeData = resumeData {
+// downloadTask = session.downloadTask(withResumeData: resumeData)
+// } else if let url = url {
+// var request = URLRequest(url: url)
+// NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+// downloadTask = session.downloadTask(with: request)
+// } else {
+// continuation.resume(throwing: NetworkError.invalidData("Neither URL nor resume data provided"))
+// return
+// }
+//
+// await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
+// await cancelTracker.clearResumeData(task.id)
+// downloadTask.resume()
+// }
+// }
+// }
+//
+// private func startDownloadProcess(task: NewDownloadTask) async {
+// actor DownloadProgress {
+// var currentPackageIndex: Int = 0
+// func increment() { currentPackageIndex += 1 }
+// func get() -> Int { return currentPackageIndex }
+// }
+//
+// let progress = DownloadProgress()
+//
+// await MainActor.run {
+// let totalPackages = task.productsToDownload.reduce(0) { $0 + $1.packages.count }
+// task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+// fileName: task.currentPackage?.fullPackageName ?? "",
+// currentPackageIndex: 0,
+// totalPackages: totalPackages,
+// startTime: Date(),
+// estimatedTimeRemaining: nil
+// )))
+// task.objectWillChange.send()
+// }
+//
+// let driverPath = task.directory.appendingPathComponent("driver.xml")
+// if !FileManager.default.fileExists(atPath: driverPath.path) {
+// if let productInfo = await networkManager?.saps[task.sapCode]?.versions[task.version] {
+// let driverXml = generateDriverXML(
+// sapCode: task.sapCode,
+// version: task.version,
+// language: task.language,
+// productInfo: productInfo,
+// displayName: task.displayName
+// )
+// do {
+// try driverXml.write(to: driverPath, atomically: true, encoding: .utf8)
+// } catch {
+// print("Error generating driver.xml:", error.localizedDescription)
+// await MainActor.run {
+// task.setStatus(.failed(DownloadStatus.FailureInfo(
+// message: "生成 driver.xml 失败: \(error.localizedDescription)",
+// error: error,
+// timestamp: Date(),
+// recoverable: false
+// )))
+// }
+// return
+// }
+// }
+// }
+//
+// for product in task.productsToDownload {
+// let productDir = task.directory.appendingPathComponent(product.sapCode)
+// if !FileManager.default.fileExists(atPath: productDir.path) {
+// do {
+// try FileManager.default.createDirectory(
+// at: productDir,
+// withIntermediateDirectories: true,
+// attributes: nil
+// )
+// } catch {
+// print("Error creating directory for \(product.sapCode): \(error)")
+// continue
+// }
+// }
+// }
+//
+// for product in task.productsToDownload {
+// for package in product.packages where !package.downloaded {
+// let currentIndex = await progress.get()
+//
+// await MainActor.run {
+// task.currentPackage = package
+// task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+// fileName: package.fullPackageName,
+// currentPackageIndex: currentIndex,
+// totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count },
+// startTime: Date(),
+// estimatedTimeRemaining: nil
+// )))
+// }
+// await networkManager?.saveTask(task)
+//
+// await progress.increment()
+//
+// guard !package.fullPackageName.isEmpty,
+// !package.downloadURL.isEmpty,
+// package.downloadSize > 0 else {
+// continue
+// }
+//
+// let cleanCdn = globalCdn.hasSuffix("/") ? String(globalCdn.dropLast()) : globalCdn
+// let cleanPath = package.downloadURL.hasPrefix("/") ? package.downloadURL : "/\(package.downloadURL)"
+// let downloadURL = cleanCdn + cleanPath
+//
+// guard let url = URL(string: downloadURL) else { continue }
+//
+// do {
+// if let resumeData = await cancelTracker.getResumeData(task.id) {
+// try await downloadPackage(package: package, task: task, product: product, resumeData: resumeData)
+// } else {
+// try await downloadPackage(package: package, task: task, product: product, url: url)
+// }
+// } catch {
+// print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)")
+// await handleError(task.id, error)
+// return
+// }
+// }
+// }
+//
+// let allPackagesDownloaded = task.productsToDownload.allSatisfy { product in
+// product.packages.allSatisfy { $0.downloaded }
+// }
+//
+// if allPackagesDownloaded {
+// await MainActor.run {
+// task.setStatus(.completed(DownloadStatus.CompletionInfo(
+// timestamp: Date(),
+// totalTime: Date().timeIntervalSince(task.createAt),
+// totalSize: task.totalSize
+// )))
+// }
+// await networkManager?.saveTask(task)
+// }
+// }
+//
+// func retryPackage(task: NewDownloadTask, package: Package) async throws {
+// guard package.canRetry else { return }
+//
+// package.prepareForRetry()
+//
+// if let product = task.productsToDownload.first(where: { $0.packages.contains(where: { $0.id == package.id }) }) {
+// await MainActor.run {
+// task.currentPackage = package
+// }
+//
+// try await downloadPackage(package: package, task: task, product: product, url: URL(string: globalCdn + package.downloadURL)!)
+// }
+// }
+//
+// func downloadAPRO(task: NewDownloadTask, productInfo: Sap.Versions) async throws {
+// guard let networkManager = networkManager else { return }
+//
+// let manifestURL = globalCdn + productInfo.buildGuid
+// guard let url = URL(string: manifestURL) else {
+// throw NetworkError.invalidURL(manifestURL)
+// }
+//
+// var request = URLRequest(url: url)
+// NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+//
+// let (manifestData, _) = try await URLSession.shared.data(for: request)
+//
+// let manifestDoc = try XMLDocument(data: manifestData)
+//
+// guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
+// let assetSizeStr = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_size").first?.stringValue,
+// let assetSize = Int64(assetSizeStr) else {
+// throw NetworkError.invalidData("无法从manifest中获取下载信息")
+// }
+//
+// guard let downloadURL = URL(string: downloadPath) else {
+// throw NetworkError.invalidURL(downloadPath)
+// }
+//
+// let aproPackage = Package(
+// type: "dmg",
+// fullPackageName: "Adobe Downloader \(task.sapCode)_\(productInfo.productVersion)_\(productInfo.apPlatform).dmg",
+// downloadSize: assetSize,
+// downloadURL: downloadPath,
+// packageVersion: ""
+// )
+//
+// await MainActor.run {
+// let product = ProductsToDownload(
+// sapCode: task.sapCode,
+// version: task.version,
+// buildGuid: productInfo.buildGuid
+// )
+// product.packages = [aproPackage]
+// task.productsToDownload = [product]
+// task.totalSize = assetSize
+// task.currentPackage = aproPackage
+// task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+// fileName: aproPackage.fullPackageName,
+// currentPackageIndex: 0,
+// totalPackages: 1,
+// startTime: Date(),
+// estimatedTimeRemaining: nil
+// )))
+// }
+//
+// let tempDownloadDir = task.directory.deletingLastPathComponent()
+// var lastUpdateTime = Date()
+// var lastBytes: Int64 = 0
+//
+// return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+// let delegate = DownloadDelegate(
+// destinationDirectory: tempDownloadDir,
+// fileName: aproPackage.fullPackageName,
+// completionHandler: { [weak networkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
+// if let error = error {
+// if (error as NSError).code == NSURLErrorCancelled {
+// continuation.resume(throwing: NetworkError.cancelled)
+// return
+// }
+// print("Download error:", error)
+// continuation.resume(throwing: error)
+// return
+// }
+//
+// Task {
+// await MainActor.run {
+// aproPackage.downloadedSize = aproPackage.downloadSize
+// aproPackage.progress = 1.0
+// aproPackage.status = .completed
+// aproPackage.downloaded = true
+//
+// var totalDownloaded: Int64 = 0
+// var totalSize: Int64 = 0
+//
+// totalSize += aproPackage.downloadSize
+// if aproPackage.downloaded {
+// totalDownloaded += aproPackage.downloadSize
+// }
+//
+// task.totalSize = totalSize
+// task.totalDownloadedSize = totalDownloaded
+// task.totalProgress = Double(totalDownloaded) / Double(totalSize)
+// task.totalSpeed = 0
+//
+// task.setStatus(.completed(DownloadStatus.CompletionInfo(
+// timestamp: Date(),
+// totalTime: Date().timeIntervalSince(task.createAt),
+// totalSize: totalSize
+// )))
+//
+// task.objectWillChange.send()
+// }
+//
+// await networkManager?.saveTask(task)
+//
+// await MainActor.run {
+// networkManager?.updateDockBadge()
+// networkManager?.objectWillChange.send()
+// }
+// continuation.resume()
+// }
+// },
+// progressHandler: { [weak networkManager] (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) in
+// Task { @MainActor in
+// let now = Date()
+// let timeDiff = now.timeIntervalSince(lastUpdateTime)
+//
+// if timeDiff >= 1.0 {
+// let bytesDiff = totalBytesWritten - lastBytes
+// let speed = Double(bytesDiff) / timeDiff
+//
+// aproPackage.updateProgress(
+// downloadedSize: totalBytesWritten,
+// speed: speed
+// )
+//
+// task.totalDownloadedSize = totalBytesWritten
+// task.totalProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
+// task.totalSpeed = speed
+//
+// lastUpdateTime = now
+// lastBytes = totalBytesWritten
+//
+// task.objectWillChange.send()
+// networkManager?.objectWillChange.send()
+//
+// Task {
+// await networkManager?.saveTask(task)
+// }
+// }
+// }
+// }
+// )
+//
+// let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
+//
+// var downloadRequest = URLRequest(url: downloadURL)
+// NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
+//
+// let downloadTask = session.downloadTask(with: downloadRequest)
+//
+// Task {
+// await self.cancelTracker.registerTask(task.id, task: downloadTask, session: session)
+//
+// if await self.cancelTracker.isCancelled(task.id) {
+// continuation.resume(throwing: NetworkError.cancelled)
+// return
+// }
+//
+// downloadTask.resume()
+// }
+// }
+// }
+//
+// func handleDownload(task: NewDownloadTask, productInfo: Sap.Versions, allowedPlatform: [String], saps: [String: Sap]) async throws {
+// if task.sapCode == "APRO" {
+// try await downloadAPRO(task: task, productInfo: productInfo)
+// return
+// }
+//
+// var productsToDownload: [ProductsToDownload] = []
+//
+// productsToDownload.append(ProductsToDownload(
+// sapCode: task.sapCode,
+// version: task.version,
+// buildGuid: productInfo.buildGuid
+// ))
+//
+// for dependency in productInfo.dependencies {
+// if let dependencyVersions = saps[dependency.sapCode]?.versions {
+// var processedVersions = Set()
+// var firstGuid: String?
+// var buildGuid: String?
+//
+// let sortedVersions = dependencyVersions.sorted { version1, version2 in
+// let v1Components = version1.key.split(separator: ".").compactMap { Int($0) }
+// let v2Components = version2.key.split(separator: ".").compactMap { Int($0) }
+//
+// for i in 0.. v2Components[i]
+// }
+// }
+// return v1Components.count > v2Components.count
+// }
+//
+// for version in sortedVersions {
+// if buildGuid != nil { break }
+//
+// if processedVersions.contains(version.key) { continue }
+// processedVersions.insert(version.key)
+//
+// if version.value.baseVersion == dependency.version {
+// if firstGuid == nil { firstGuid = version.value.buildGuid }
+//
+// print("\(version.value.sapCode), \(version.key), \(allowedPlatform), \(version.value.apPlatform), \(allowedPlatform.contains(version.value.apPlatform))")
+//
+// if allowedPlatform.contains(version.value.apPlatform) {
+// buildGuid = version.value.buildGuid
+// break
+// }
+// }
+// }
+//
+// if buildGuid == nil { buildGuid = firstGuid }
+// if let finalBuildGuid = buildGuid {
+// let alreadyAdded = productsToDownload.contains { product in
+// product.sapCode == dependency.sapCode &&
+// product.version == dependency.version
+// }
+//
+// if !alreadyAdded {
+// productsToDownload.append(ProductsToDownload(
+// sapCode: dependency.sapCode,
+// version: dependency.version,
+// buildGuid: finalBuildGuid
+// ))
+// }
+// }
+// }
+// }
+//
+// for product in productsToDownload {
+// print("\(product.sapCode), \(product.version), \(product.buildGuid)")
+// }
+//
+// for product in productsToDownload {
+// await MainActor.run {
+// task.setStatus(.preparing(DownloadStatus.PrepareInfo(
+// message: String(localized: "正在处理 \(product.sapCode) 的包信息..."),
+// timestamp: Date(),
+// stage: .fetchingInfo
+// )))
+// }
+//
+// let jsonString = try await getApplicationInfo(buildGuid: product.buildGuid)
+// let productDir = task.directory.appendingPathComponent("\(product.sapCode)")
+// if !FileManager.default.fileExists(atPath: productDir.path) {
+// try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
+// }
+// let jsonURL = productDir.appendingPathComponent("application.json")
+// try jsonString.write(to: jsonURL, atomically: true, encoding: .utf8)
+//
+// guard let jsonData = jsonString.data(using: .utf8),
+// let appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
+// let packages = appInfo["Packages"] as? [String: Any],
+// let packageArray = packages["Package"] as? [[String: Any]] else {
+// throw NetworkError.invalidData("无法解析产品信息")
+// }
+//
+// var corePackageCount = 0
+// var nonCorePackageCount = 0
+//
+// /*
+// 这里是对包的过滤,一般规则在
+// 1. 如果没有Condition,那么就视为需要下载的包
+// 2. 如果存在Condition,那么按照以下规则下载
+// [OSVersion]>=10.15 : 系统版本大于等于10.15就下载,所以需要一个函数来获取系统版本号
+// [OSArchitecture]==arm64 : 系统架构为arm64的就下载,官方并没有下载另外一个架构的包
+// [OSArchitecture]==x64 : 同上
+// [installLanguage]==zh_CN : 目标安装语言为 zh_CN 的就下载
+//
+// PS: 下面是留给看源码的人的
+// 哪怕是官方的ACC下载任何一款App,都是这个逻辑,不信自己去翻,你可能会说,为什么官方能下通用的,你问这个问题之前,可以自己去拿正版的看看他是怎么下载的,他下载的包数量跟我的是不是一致的,他也只是下载了对应架构的包
+//
+// 其实要下载通用的也很简单,不是判断架构吗,那下载通用的时候,两个架构同时成立不就好了,但我并没有在官方的下载逻辑中看到,也没尝试过,如果你尝试之后发现可以,请你告诉我
+// */
+//
+// for package in packageArray {
+// var shouldDownload = false
+// let packageType = package["Type"] as? String ?? "non-core"
+// let isCore = packageType == "core"
+//
+// guard let downloadURL = package["Path"] as? String, !downloadURL.isEmpty else { continue }
+//
+// let fullPackageName: String
+// let packageVersion: String
+// if let name = package["fullPackageName"] as? String, !name.isEmpty {
+// fullPackageName = name
+// packageVersion = package["PackageVersion"] as? String ?? ""
+// } else if let name = package["PackageName"] as? String, !name.isEmpty {
+// fullPackageName = "\(name).zip"
+// packageVersion = package["PackageVersion"] as? String ?? ""
+// } else {
+// continue
+// }
+//
+// let downloadSize: Int64
+// if let sizeNumber = package["DownloadSize"] as? NSNumber {
+// downloadSize = sizeNumber.int64Value
+// } else if let sizeString = package["DownloadSize"] as? String,
+// let parsedSize = Int64(sizeString) {
+// downloadSize = parsedSize
+// } else if let sizeInt = package["DownloadSize"] as? Int {
+// downloadSize = Int64(sizeInt)
+// } else { continue }
+//
+// let installLanguage = "[installLanguage]==\(task.language)"
+// if let condition = package["Condition"] as? String {
+// if condition.isEmpty {
+// shouldDownload = true
+// } else {
+// if condition.contains("[OSVersion]") {
+// let osVersion = ProcessInfo.processInfo.operatingSystemVersion
+// let currentVersion = Double("\(osVersion.majorVersion).\(osVersion.minorVersion)") ?? 0.0
+//
+// let versionPattern = #"\[OSVersion\](>=|<=|<|>|==)([\d.]+)"#
+// let regex = try? NSRegularExpression(pattern: versionPattern)
+// let range = NSRange(condition.startIndex.. 0 ? pkg.downloadSize : 0)
+// }
+// }
+//
+// await MainActor.run {
+// task.productsToDownload = finalProducts
+// task.totalSize = totalSize
+// }
+//
+// await startDownloadProcess(task: task)
+// }
+//
+// func getApplicationInfo(buildGuid: String) async throws -> String {
+// guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
+// throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
+// }
+//
+// var request = URLRequest(url: url)
+// request.httpMethod = "GET"
+//
+// var headers = NetworkConstants.adobeRequestHeaders
+// headers["x-adobe-build-guid"] = buildGuid
+//
+// headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+//
+// let (data, response) = try await URLSession.shared.data(for: request)
+//
+// guard let httpResponse = response as? HTTPURLResponse else {
+// throw NetworkError.invalidResponse
+// }
+//
+// guard (200...299).contains(httpResponse.statusCode) else {
+// throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
+// }
+//
+// guard let jsonString = String(data: data, encoding: .utf8) else {
+// throw NetworkError.invalidData(String(localized: "无法将响应数据转换为json字符串"))
+// }
+//
+// return jsonString
+// }
+//
+// func handleError(_ taskId: UUID, _ error: Error) async {
+// guard let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) else { return }
+//
+// let (errorMessage, isRecoverable) = classifyError(error)
+//
+// if isRecoverable,
+// let downloadTask = await cancelTracker.downloadTasks[taskId] {
+// let resumeData = await withCheckedContinuation { continuation in
+// downloadTask.cancel(byProducingResumeData: { data in
+// continuation.resume(returning: data)
+// })
+// }
+// if let resumeData = resumeData {
+// await cancelTracker.storeResumeData(taskId, data: resumeData)
+// }
+// }
+//
+// if isRecoverable && task.retryCount < NetworkConstants.maxRetryAttempts {
+// task.retryCount += 1
+// let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000))
+// task.setStatus(.retrying(DownloadStatus.RetryInfo(
+// attempt: task.retryCount,
+// maxAttempts: NetworkConstants.maxRetryAttempts,
+// reason: errorMessage,
+// nextRetryDate: nextRetryDate
+// )))
+//
+// Task {
+// do {
+// try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
+// if await !cancelTracker.isCancelled(taskId) {
+// await resumeDownloadTask(taskId: taskId)
+// }
+// } catch {
+// print("Retry cancelled for task: \(taskId)")
+// }
+// }
+// } else {
+// task.setStatus(.failed(DownloadStatus.FailureInfo(
+// message: errorMessage,
+// error: error,
+// timestamp: Date(),
+// recoverable: isRecoverable
+// )))
+//
+// if let currentPackage = task.currentPackage {
+// let destinationDir = task.directory
+// .appendingPathComponent("\(task.sapCode)")
+// let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
+// try? FileManager.default.removeItem(at: fileURL)
+// }
+//
+// await networkManager?.saveTask(task)
+// await MainActor.run {
+// networkManager?.updateDockBadge()
+// networkManager?.objectWillChange.send()
+// }
+// }
+// }
+//
+// private func classifyError(_ error: Error) -> (message: String, recoverable: Bool) {
+// switch error {
+// case let networkError as NetworkError:
+// switch networkError {
+// case .noConnection:
+// return (String(localized: "网络连接已断开"), true)
+// case .timeout:
+// return (String(localized: "下载超时"), true)
+// case .serverUnreachable:
+// return (String(localized: "服务器无法访问"), true)
+// case .insufficientStorage:
+// return (String(localized: "存储空间不足"), false)
+// case .filePermissionDenied:
+// return (String(localized: "没有写入权限"), false)
+// default:
+// return (networkError.localizedDescription, false)
+// }
+// case let urlError as URLError:
+// switch urlError.code {
+// case .notConnectedToInternet, .networkConnectionLost, .dataNotAllowed:
+// return (String(localized: "网络连接已断开"), true)
+// case .timedOut:
+// return (String(localized: "连接超时"), true)
+// case .cancelled:
+// return (String(localized: "下载已取消"), false)
+// case .cannotConnectToHost, .dnsLookupFailed:
+// return (String(localized: "无法连接到服务器"), true)
+// default:
+// return (urlError.localizedDescription, true)
+// }
+// default:
+// return (error.localizedDescription, false)
+// }
+// }
+//
+// @MainActor
+// func updateProgress(for taskId: UUID, progress: ProgressUpdate) {
+// guard let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }),
+// let currentPackage = task.currentPackage else { return }
+//
+// let now = Date()
+// let timeDiff = now.timeIntervalSince(currentPackage.lastUpdated)
+//
+// if timeDiff >= NetworkConstants.progressUpdateInterval {
+// currentPackage.updateProgress(
+// downloadedSize: progress.totalWritten,
+// speed: Double(progress.bytesWritten)
+// )
+//
+// let totalDownloaded = task.productsToDownload.reduce(Int64(0)) { sum, prod in
+// sum + prod.packages.reduce(Int64(0)) { sum, pkg in
+// if pkg.downloaded {
+// return sum + pkg.downloadSize
+// } else if pkg.id == currentPackage.id {
+// return sum + progress.totalWritten
+// }
+// return sum
+// }
+// }
+//
+// task.totalDownloadedSize = totalDownloaded
+// task.totalProgress = Double(totalDownloaded) / Double(task.totalSize)
+// task.totalSpeed = currentPackage.speed
+//
+// currentPackage.lastRecordedSize = progress.totalWritten
+// currentPackage.lastUpdated = now
+//
+// task.objectWillChange.send()
+// networkManager?.objectWillChange.send()
+// }
+// }
+//
+// @MainActor
+// func updateTaskStatus(_ taskId: UUID, _ status: DownloadStatus) async {
+// guard let networkManager = networkManager else { return }
+//
+// if let index = networkManager.downloadTasks.firstIndex(where: { $0.id == taskId }) {
+// networkManager.downloadTasks[index].setStatus(status)
+//
+// switch status {
+// case .completed, .failed:
+// networkManager.progressObservers[taskId]?.invalidate()
+// networkManager.progressObservers.removeValue(forKey: taskId)
+// if networkManager.activeDownloadTaskId == taskId {
+// networkManager.activeDownloadTaskId = nil
+// }
+//
+// case .downloading:
+// networkManager.activeDownloadTaskId = taskId
+//
+// case .paused:
+// if networkManager.activeDownloadTaskId == taskId {
+// networkManager.activeDownloadTaskId = nil
+// }
+//
+// default:
+// break
+// }
+//
+// networkManager.updateDockBadge()
+// networkManager.objectWillChange.send()
+// }
+// }
+//
+// 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
+// }
+// }
+//
+// 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")
+// }
+// }
+//
+// private func compareVersions(current: Double, required: Double, operator: String) -> Bool {
+// switch `operator` {
+// case ">=":
+// return current >= required
+// case "<=":
+// return current <= required
+// case ">":
+// return current > required
+// case "<":
+// return current < required
+// case "==":
+// return current == required
+// default:
+// return false
+// }
+// }
+//}
diff --git a/Adobe Downloader/Utils/JSONParser.swift b/Adobe Downloader/Utils/JSONParser.swift
index 9c3df63..0cb31c9 100644
--- a/Adobe Downloader/Utils/JSONParser.swift
+++ b/Adobe Downloader/Utils/JSONParser.swift
@@ -1,172 +1,172 @@
+////
+//// JSONParser.swift
+//// Adobe Downloader
+////
+//// Created by X1a0He on 11/18/24.
+////
//
-// JSONParser.swift
-// Adobe Downloader
+//import Foundation
//
-// Created by X1a0He on 11/18/24.
+// struct ParseResult {
+// var products: [String: Sap]
+// var cdn: String
+// }
//
-
-import Foundation
-
- struct ParseResult {
- var products: [String: Sap]
- var cdn: String
- }
-
-class JSONParser {
- static func parse(jsonString: String) throws -> ParseResult {
- guard let jsonData = jsonString.data(using: .utf8),
- let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else {
- throw ParserError.invalidJSON
- }
- let apiVersion = Int(StorageData.shared.apiVersion) ?? 6
-
- return try parseProductsJSON(jsonObject: jsonObject, apiVersion: apiVersion)
- }
-
- private static func parseProductsJSON(jsonObject: [String: Any], apiVersion: Int) throws -> ParseResult {
- let cdnPath: [String]
- if apiVersion == 6 {
- cdnPath = ["channels", "channel"]
- } else {
- cdnPath = ["channel"]
- }
-
- func getValue(from dict: [String: Any], path: [String]) -> Any? {
- var current: Any = dict
- for key in path {
- guard let dict = current as? [String: Any],
- let value = dict[key] else {
- return nil
- }
- current = value
- }
- return current
- }
-
- var channelArray: [[String: Any]] = []
- if let channels = getValue(from: jsonObject, path: cdnPath) {
- if let array = channels as? [[String: Any]] {
- channelArray = array
- } else if let dict = channels as? [String: Any],
- let array = dict["channel"] as? [[String: Any]] {
- channelArray = array
- }
- }
-
- guard let firstChannel = channelArray.first,
- let cdn = (firstChannel["cdn"] as? [String: Any])?["secure"] as? String else {
- throw ParserError.missingCDN
- }
-
- var products = [String: Sap](minimumCapacity: 200)
-
- for channel in channelArray {
- let channelName = channel["name"] as? String
- let hidden = channelName != "ccm"
-
- guard let productsContainer = channel["products"] as? [String: Any],
- let productArray = productsContainer["product"] as? [[String: Any]] else {
- continue
- }
-
- for product in productArray {
- guard let sap = product["id"] as? String,
- let displayName = product["displayName"] as? String,
- let productVersion = product["version"] as? String else {
- continue
- }
-
- if products[sap] == nil {
- let icons = (product["productIcons"] as? [String: Any])?["icon"] as? [[String: Any]] ?? []
- let productIcons = icons.compactMap { icon -> Sap.ProductIcon? in
- guard let size = icon["size"] as? String,
- let value = icon["value"] as? String else {
- return nil
- }
- return Sap.ProductIcon(size: size, url: value)
- }
-
- products[sap] = Sap(
- hidden: hidden,
- displayName: displayName,
- sapCode: sap,
- versions: [:],
- icons: productIcons
- )
- }
-
- if let platforms = product["platforms"] as? [String: Any],
- let platformArray = platforms["platform"] as? [[String: Any]] {
-
- for platform in platformArray {
- guard let platformId = platform["id"] as? String,
- let languageSets = platform["languageSet"] as? [[String: Any]],
- let languageSet = languageSets.first else {
- continue
- }
-
- if let existingVersion = products[sap]?.versions[productVersion],
- StorageData.shared.allowedPlatform.contains(existingVersion.apPlatform) {
- break
- }
-
- var baseVersion = languageSet["baseVersion"] as? String ?? ""
- var buildGuid = languageSet["buildGuid"] as? String ?? ""
- var finalProductVersion = productVersion
-
- if sap == "APRO" {
- baseVersion = productVersion
- if apiVersion == 4 || apiVersion == 5 {
- if let appVersion = (languageSet["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String {
- finalProductVersion = appVersion
- }
- } else if apiVersion == 6 {
- if let builds = jsonObject["builds"] as? [String: Any],
- let buildArray = builds["build"] as? [[String: Any]] {
- for build in buildArray {
- if build["id"] as? String == sap && build["version"] as? String == baseVersion,
- let appVersion = (build["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String {
- finalProductVersion = appVersion
- break
- }
- }
- }
- }
-
- if let urls = languageSet["urls"] as? [String: Any],
- let manifestURL = urls["manifestURL"] as? String {
- buildGuid = manifestURL
- }
- }
-
- var dependencies: [Sap.Versions.Dependencies] = []
- if let deps = languageSet["dependencies"] as? [String: Any],
- let depArray = deps["dependency"] as? [[String: Any]] {
- dependencies = depArray.compactMap { dep in
- guard let sapCode = dep["sapCode"] as? String,
- let version = dep["baseVersion"] as? String else {
- return nil
- }
- return Sap.Versions.Dependencies(sapCode: sapCode, version: version)
- }
- }
-
- if !buildGuid.isEmpty {
- let version = Sap.Versions(
- sapCode: sap,
- baseVersion: baseVersion,
- productVersion: finalProductVersion,
- apPlatform: platformId,
- dependencies: dependencies,
- buildGuid: buildGuid
- )
- products[sap]?.versions[finalProductVersion] = version
- }
- }
- }
- }
- }
-
- return ParseResult(products: products, cdn: cdn)
- }
-}
+//class JSONParser {
+// static func parse(jsonString: String) throws -> ParseResult {
+// guard let jsonData = jsonString.data(using: .utf8),
+// let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else {
+// throw ParserError.invalidJSON
+// }
+// let apiVersion = Int(StorageData.shared.apiVersion) ?? 6
+//
+// return try parseProductsJSON(jsonObject: jsonObject, apiVersion: apiVersion)
+// }
+//
+// private static func parseProductsJSON(jsonObject: [String: Any], apiVersion: Int) throws -> ParseResult {
+// let cdnPath: [String]
+// if apiVersion == 6 {
+// cdnPath = ["channels", "channel"]
+// } else {
+// cdnPath = ["channel"]
+// }
+//
+// func getValue(from dict: [String: Any], path: [String]) -> Any? {
+// var current: Any = dict
+// for key in path {
+// guard let dict = current as? [String: Any],
+// let value = dict[key] else {
+// return nil
+// }
+// current = value
+// }
+// return current
+// }
+//
+// var channelArray: [[String: Any]] = []
+// if let channels = getValue(from: jsonObject, path: cdnPath) {
+// if let array = channels as? [[String: Any]] {
+// channelArray = array
+// } else if let dict = channels as? [String: Any],
+// let array = dict["channel"] as? [[String: Any]] {
+// channelArray = array
+// }
+// }
+//
+// guard let firstChannel = channelArray.first,
+// let cdn = (firstChannel["cdn"] as? [String: Any])?["secure"] as? String else {
+// throw ParserError.missingCDN
+// }
+//
+// var products = [String: Sap](minimumCapacity: 200)
+//
+// for channel in channelArray {
+// let channelName = channel["name"] as? String
+// let hidden = channelName != "ccm"
+//
+// guard let productsContainer = channel["products"] as? [String: Any],
+// let productArray = productsContainer["product"] as? [[String: Any]] else {
+// continue
+// }
+//
+// for product in productArray {
+// guard let sap = product["id"] as? String,
+// let displayName = product["displayName"] as? String,
+// let productVersion = product["version"] as? String else {
+// continue
+// }
+//
+// if products[sap] == nil {
+// let icons = (product["productIcons"] as? [String: Any])?["icon"] as? [[String: Any]] ?? []
+// let productIcons = icons.compactMap { icon -> Sap.ProductIcon? in
+// guard let size = icon["size"] as? String,
+// let value = icon["value"] as? String else {
+// return nil
+// }
+// return Sap.ProductIcon(size: size, url: value)
+// }
+//
+// products[sap] = Sap(
+// hidden: hidden,
+// displayName: displayName,
+// sapCode: sap,
+// versions: [:],
+// icons: productIcons
+// )
+// }
+//
+// if let platforms = product["platforms"] as? [String: Any],
+// let platformArray = platforms["platform"] as? [[String: Any]] {
+//
+// for platform in platformArray {
+// guard let platformId = platform["id"] as? String,
+// let languageSets = platform["languageSet"] as? [[String: Any]],
+// let languageSet = languageSets.first else {
+// continue
+// }
+//
+// if let existingVersion = products[sap]?.versions[productVersion],
+// StorageData.shared.allowedPlatform.contains(existingVersion.apPlatform) {
+// break
+// }
+//
+// var baseVersion = languageSet["baseVersion"] as? String ?? ""
+// var buildGuid = languageSet["buildGuid"] as? String ?? ""
+// var finalProductVersion = productVersion
+//
+// if sap == "APRO" {
+// baseVersion = productVersion
+// if apiVersion == 4 || apiVersion == 5 {
+// if let appVersion = (languageSet["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String {
+// finalProductVersion = appVersion
+// }
+// } else if apiVersion == 6 {
+// if let builds = jsonObject["builds"] as? [String: Any],
+// let buildArray = builds["build"] as? [[String: Any]] {
+// for build in buildArray {
+// if build["id"] as? String == sap && build["version"] as? String == baseVersion,
+// let appVersion = (build["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String {
+// finalProductVersion = appVersion
+// break
+// }
+// }
+// }
+// }
+//
+// if let urls = languageSet["urls"] as? [String: Any],
+// let manifestURL = urls["manifestURL"] as? String {
+// buildGuid = manifestURL
+// }
+// }
+//
+// var dependencies: [Sap.Versions.Dependencies] = []
+// if let deps = languageSet["dependencies"] as? [String: Any],
+// let depArray = deps["dependency"] as? [[String: Any]] {
+// dependencies = depArray.compactMap { dep in
+// guard let sapCode = dep["sapCode"] as? String,
+// let version = dep["baseVersion"] as? String else {
+// return nil
+// }
+// return Sap.Versions.Dependencies(sapCode: sapCode, version: version)
+// }
+// }
+//
+// if !buildGuid.isEmpty {
+// let version = Sap.Versions(
+// sapCode: sap,
+// baseVersion: baseVersion,
+// productVersion: finalProductVersion,
+// apPlatform: platformId,
+// dependencies: dependencies,
+// buildGuid: buildGuid
+// )
+// products[sap]?.versions[finalProductVersion] = version
+// }
+// }
+// }
+// }
+// }
+//
+// return ParseResult(products: products, cdn: cdn)
+// }
+//}
diff --git a/Adobe Downloader/NetworkManager.swift b/Adobe Downloader/Utils/NetworkManager.swift
similarity index 86%
rename from Adobe Downloader/NetworkManager.swift
rename to Adobe Downloader/Utils/NetworkManager.swift
index 838c0eb..924040d 100644
--- a/Adobe Downloader/NetworkManager.swift
+++ b/Adobe Downloader/Utils/NetworkManager.swift
@@ -8,15 +8,10 @@ import SwiftUI
class NetworkManager: ObservableObject {
typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64)
@Published var isConnected = false
- @Published var saps: [String: Sap] = [:]
- @Published var cdn: String = ""
- @Published var sapCodes: [SapCodes] = []
@Published var loadingState: LoadingState = .idle
@Published var downloadTasks: [NewDownloadTask] = []
@Published var installationState: InstallationState = .idle
@Published var installCommand: String = ""
- private let cancelTracker = CancelTracker()
- internal var downloadUtils: DownloadUtils!
internal var progressObservers: [UUID: NSKeyValueObservation] = [:]
internal var activeDownloadTaskId: UUID?
internal var monitor = NWPathMonitor()
@@ -46,26 +41,24 @@ class NetworkManager: ObservableObject {
case failed(Error)
}
- private let networkService: NetworkService
-
- init(networkService: NetworkService = NetworkService(),
- downloadUtils: DownloadUtils? = nil) {
-
- self.networkService = networkService
- self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
-
- TaskPersistenceManager.shared.setCancelTracker(cancelTracker)
+ init() {
+ TaskPersistenceManager.shared.setCancelTracker(globalCancelTracker)
configureNetworkMonitor()
}
func fetchProducts() async {
loadingState = .loading
do {
- let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
+ 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)")
+ }
+
+
await MainActor.run {
- self.saps = saps
- self.cdn = cdn
- self.sapCodes = sapCodes
self.loadingState = .success
}
} catch {
@@ -74,17 +67,19 @@ class NetworkManager: ObservableObject {
}
}
}
- func startDownload(sap: Sap, selectedVersion: String, language: String, destinationURL: URL) async throws {
- guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else {
- throw NetworkError.invalidData("无法获取产品信息")
+ func startDownload(productId: String, selectedVersion: String, language: String, destinationURL: URL) async throws {
+ // 从 globalCcmResult 中获取 productId 对应的 ProductInfo
+ guard let productInfo = globalCcmResult.products.first(where: { $0.id == productId }) else {
+ throw NetworkError.productNotFound
}
+
let task = NewDownloadTask(
- sapCode: sap.sapCode,
- version: selectedVersion,
+ productId: productInfo.id,
+ productVersion: selectedVersion,
language: language,
- displayName: sap.displayName,
+ displayName: productInfo.displayName,
directory: destinationURL,
- productsToDownload: [],
+ dependenciesToDownload: [],
createAt: Date(),
totalStatus: .preparing(DownloadStatus.PrepareInfo(
message: "正在准备下载...",
@@ -95,15 +90,14 @@ class NetworkManager: ObservableObject {
totalDownloadedSize: 0,
totalSize: 0,
totalSpeed: 0,
- platform: productInfo.apPlatform
- )
-
+ platform: "")
+
downloadTasks.append(task)
updateDockBadge()
await saveTask(task)
do {
- try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform, saps: saps)
+ try await globalNewDownloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform)
} catch {
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: error.localizedDescription,
@@ -118,16 +112,10 @@ class NetworkManager: ObservableObject {
}
}
- var cdnUrl: String {
- get async {
- await MainActor.run { cdn }
- }
- }
-
func removeTask(taskId: UUID, removeFiles: Bool = true) {
Task {
- await cancelTracker.cancel(taskId)
-
+ await globalCancelTracker.cancel(taskId)
+
if let task = downloadTasks.first(where: { $0.id == taskId }) {
if task.status.isActive {
task.setStatus(.failed(DownloadStatus.FailureInfo(
@@ -165,11 +153,8 @@ class NetworkManager: ObservableObject {
while retryCount < maxRetries {
do {
- let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
+ let (saps, sapCodes) = try await globalNetworkService.fetchProductsData()
await MainActor.run {
- self.saps = saps
- self.cdn = cdn
- self.sapCodes = sapCodes
self.loadingState = .success
self.isFetchingProducts = false
}
@@ -302,10 +287,10 @@ class NetworkManager: ObservableObject {
}
func getApplicationInfo(buildGuid: String) async throws -> String {
- return try await networkService.getApplicationInfo(buildGuid: buildGuid)
+ return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid)
}
- func isVersionDownloaded(sap: Sap, version: String, language: String) -> URL? {
+ func isVersionDownloaded(product: Product, version: String, language: String) -> URL? {
if let task = downloadTasks.first(where: {
$0.sapCode == sap.sapCode &&
$0.version == version &&
diff --git a/Adobe Downloader/Utils/NewDownloadUtils.swift b/Adobe Downloader/Utils/NewDownloadUtils.swift
new file mode 100644
index 0000000..cc34a57
--- /dev/null
+++ b/Adobe Downloader/Utils/NewDownloadUtils.swift
@@ -0,0 +1,947 @@
+//
+// NewDownloadUtils.swift
+// Adobe Downloader
+//
+// Created by X1a0He on 2/26/25.
+//
+import Foundation
+class NewDownloadUtils {
+ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
+ var completionHandler: (URL?, URLResponse?, Error?) -> Void
+ var progressHandler: ((Int64, Int64, Int64) -> Void)?
+ var destinationDirectory: URL
+ var fileName: String
+ private var hasCompleted = false
+ private let completionLock = NSLock()
+ private var lastUpdateTime = Date()
+ private var lastBytes: Int64 = 0
+
+ init(destinationDirectory: URL,
+ fileName: String,
+ completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void,
+ progressHandler: ((Int64, Int64, Int64) -> Void)? = nil) {
+ self.destinationDirectory = destinationDirectory
+ self.fileName = fileName
+ self.completionHandler = completionHandler
+ self.progressHandler = progressHandler
+ super.init()
+ }
+
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+ completionLock.lock()
+ defer { completionLock.unlock() }
+
+ guard !hasCompleted else { return }
+ hasCompleted = true
+
+ do {
+ if !FileManager.default.fileExists(atPath: destinationDirectory.path) {
+ try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
+ }
+
+ let destinationURL = destinationDirectory.appendingPathComponent(fileName)
+
+ if FileManager.default.fileExists(atPath: destinationURL.path) {
+ try FileManager.default.removeItem(at: destinationURL)
+ }
+
+ try FileManager.default.moveItem(at: location, to: destinationURL)
+ completionHandler(destinationURL, downloadTask.response, nil)
+
+ } catch {
+ print("File operation error in delegate: \(error.localizedDescription)")
+ completionHandler(nil, downloadTask.response, error)
+ }
+ }
+
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+ completionLock.lock()
+ defer { completionLock.unlock() }
+
+ guard !hasCompleted else { return }
+ hasCompleted = true
+
+ if let error = error {
+ switch (error as NSError).code {
+ case NSURLErrorCancelled:
+ return
+ case NSURLErrorTimedOut:
+ completionHandler(nil, task.response, NetworkError.downloadError("下载超时", error))
+ case NSURLErrorNotConnectedToInternet:
+ completionHandler(nil, task.response, NetworkError.noConnection)
+ default:
+ completionHandler(nil, task.response, error)
+ }
+ }
+ }
+
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
+ didWriteData bytesWritten: Int64,
+ totalBytesWritten: Int64,
+ totalBytesExpectedToWrite: Int64) {
+ guard totalBytesExpectedToWrite > 0 else { return }
+ guard bytesWritten > 0 else { return }
+
+ handleProgressUpdate(
+ bytesWritten: bytesWritten,
+ totalBytesWritten: totalBytesWritten,
+ totalBytesExpectedToWrite: totalBytesExpectedToWrite
+ )
+ }
+
+ func cleanup() {
+ completionHandler = { _, _, _ in }
+ progressHandler = nil
+ }
+
+ private func handleProgressUpdate(bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+ let now = Date()
+ let timeDiff = now.timeIntervalSince(lastUpdateTime)
+
+ guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
+
+ Task {
+ progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
+ }
+
+ lastUpdateTime = now
+ lastBytes = totalBytesWritten
+ }
+ }
+
+ func handleDownload(task: NewDownloadTask, productInfo: Product, allowedPlatform: [String]) async throws {
+ if productInfo.id == "APRO" {
+ try await downloadAPRO(task: task, productInfo: productInfo)
+ return
+ }
+
+ var dependenciesToDownload: [DependenciesToDownload] = []
+ let firstPlatform = productInfo.platforms.first
+ let buildGuid = firstPlatform?.languageSet.first?.buildGuid ?? ""
+
+ dependenciesToDownload.append(DependenciesToDownload(sapCode: productInfo.id, version: productInfo.version, buildGuid: buildGuid))
+
+ let dependencies = firstPlatform?.languageSet.first?.dependencies
+
+ if let dependencies = dependencies {
+ for dependency in dependencies {
+ dependenciesToDownload.append(DependenciesToDownload(sapCode: dependency.sapCode, version: dependency.productVersion, buildGuid: dependency.buildGuid))
+ }
+ }
+
+ for dependencyToDownload in dependenciesToDownload {
+ print("\(dependencyToDownload.sapCode), \(dependencyToDownload.version), \(dependencyToDownload.buildGuid)")
+ }
+
+ for dependencyToDownload in dependenciesToDownload {
+ await MainActor.run {
+ task.setStatus(.preparing(DownloadStatus.PrepareInfo(
+ message: String(localized: "正在处理 \(dependencyToDownload.sapCode) 的包信息..."),
+ timestamp: Date(),
+ stage: .fetchingInfo
+ )))
+ }
+
+ let jsonString = try await getApplicationInfo(buildGuid: dependencyToDownload.buildGuid)
+ let productDir = task.directory.appendingPathComponent("\(dependencyToDownload.sapCode)")
+ if !FileManager.default.fileExists(atPath: productDir.path) {
+ try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
+ }
+ let jsonURL = productDir.appendingPathComponent("application.json")
+ try jsonString.write(to: jsonURL, atomically: true, encoding: String.Encoding.utf8)
+
+ guard let jsonData = jsonString.data(using: .utf8),
+ let appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
+ let packages = appInfo["Packages"] as? [String: Any],
+ let packageArray = packages["Package"] as? [[String: Any]] else {
+ throw NetworkError.invalidData("无法解析产品信息")
+ }
+
+ var corePackageCount = 0
+ var nonCorePackageCount = 0
+
+ /*
+ 这里是对包的过滤,一般规则在
+ 1. 如果没有Condition,那么就视为需要下载的包
+ 2. 如果存在Condition,那么按照以下规则下载
+ [OSVersion]>=10.15 : 系统版本大于等于10.15就下载,所以需要一个函数来获取系统版本号
+ [OSArchitecture]==arm64 : 系统架构为arm64的就下载,官方并没有下载另外一个架构的包
+ [OSArchitecture]==x64 : 同上
+ [installLanguage]==zh_CN : 目标安装语言为 zh_CN 的就下载
+
+ PS: 下面是留给看源码的人的
+ 哪怕是官方的ACC下载任何一款App,都是这个逻辑,不信自己去翻,你可能会说,为什么官方能下通用的,你问这个问题之前,可以自己去拿正版的看看他是怎么下载的,他下载的包数量跟我的是不是一致的,他也只是下载了对应架构的包
+
+ 其实要下载通用的也很简单,不是判断架构吗,那下载通用的时候,两个架构同时成立不就好了,但我并没有在官方的下载逻辑中看到,也没尝试过,如果你尝试之后发现可以,请你告诉我
+ */
+
+ for package in packageArray {
+ var shouldDownload = false
+ let packageType = package["Type"] as? String ?? "non-core"
+ let isCore = packageType == "core"
+
+ guard let downloadURL = package["Path"] as? String, !downloadURL.isEmpty else { continue }
+
+ let fullPackageName: String
+ let packageVersion: String
+ if let name = package["fullPackageName"] as? String, !name.isEmpty {
+ fullPackageName = name
+ packageVersion = package["PackageVersion"] as? String ?? ""
+ } else if let name = package["PackageName"] as? String, !name.isEmpty {
+ fullPackageName = "\(name).zip"
+ packageVersion = package["PackageVersion"] as? String ?? ""
+ } else {
+ continue
+ }
+
+ let downloadSize: Int64
+ if let sizeNumber = package["DownloadSize"] as? NSNumber {
+ downloadSize = sizeNumber.int64Value
+ } else if let sizeString = package["DownloadSize"] as? String,
+ let parsedSize = Int64(sizeString) {
+ downloadSize = parsedSize
+ } else if let sizeInt = package["DownloadSize"] as? Int {
+ downloadSize = Int64(sizeInt)
+ } else { continue }
+
+ let installLanguage = "[installLanguage]==\(task.language)"
+ if let condition = package["Condition"] as? String {
+ if condition.isEmpty {
+ shouldDownload = true
+ } else {
+ if condition.contains("[OSVersion]") {
+ let osVersion = ProcessInfo.processInfo.operatingSystemVersion
+ let currentVersion = Double("\(osVersion.majorVersion).\(osVersion.minorVersion)") ?? 0.0
+
+ let versionPattern = #"\[OSVersion\](>=|<=|<|>|==)([\d.]+)"#
+ let regex = try? NSRegularExpression(pattern: versionPattern)
+ let range = NSRange(condition.startIndex.. 0 ? pkg.downloadSize : 0)
+ }
+ }
+
+ await MainActor.run {
+ task.dependenciesToDownload = finalProducts
+ task.totalSize = totalSize
+ }
+
+ await startDownloadProcess(task: task)
+ }
+
+ private func startDownloadProcess(task: NewDownloadTask) async {
+ actor DownloadProgress {
+ var currentPackageIndex: Int = 0
+ func increment() { currentPackageIndex += 1 }
+ func get() -> Int { return currentPackageIndex }
+ }
+
+ let progress = DownloadProgress()
+
+ 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: totalPackages,
+ startTime: Date(),
+ estimatedTimeRemaining: nil
+ )))
+ task.objectWillChange.send()
+ }
+
+ let driverPath = task.directory.appendingPathComponent("driver.xml")
+ 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,
+ displayName: task.displayName
+ )
+ do {
+ try driverXml.write(to: driverPath, atomically: true, encoding: String.Encoding.utf8)
+ } catch {
+ print("Error generating driver.xml:", error.localizedDescription)
+ await MainActor.run {
+ task.setStatus(.failed(DownloadStatus.FailureInfo(
+ message: "生成 driver.xml 失败: \(error.localizedDescription)",
+ error: error,
+ timestamp: Date(),
+ recoverable: false
+ )))
+ }
+ return
+ }
+ }
+ }
+
+ for dependencyToDownload in task.dependenciesToDownload {
+ let productDir = task.directory.appendingPathComponent(dependencyToDownload.sapCode)
+ if !FileManager.default.fileExists(atPath: productDir.path) {
+ do {
+ try FileManager.default.createDirectory(
+ at: productDir,
+ withIntermediateDirectories: true,
+ attributes: nil
+ )
+ } catch {
+ print("Error creating directory for \(dependencyToDownload.sapCode): \(error)")
+ continue
+ }
+ }
+ }
+
+ for dependencyToDownload in task.dependenciesToDownload {
+ for package in dependencyToDownload.packages where !package.downloaded {
+ let currentIndex = await progress.get()
+
+ await MainActor.run {
+ task.currentPackage = package
+ task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+ fileName: package.fullPackageName,
+ currentPackageIndex: currentIndex,
+ totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count },
+ startTime: Date(),
+ estimatedTimeRemaining: nil
+ )))
+ }
+ await globalNetworkManager.saveTask(task)
+
+ await progress.increment()
+
+ guard !package.fullPackageName.isEmpty,
+ !package.downloadURL.isEmpty,
+ package.downloadSize > 0 else {
+ continue
+ }
+
+ let cleanCdn = globalCdn.hasSuffix("/") ? String(globalCdn.dropLast()) : globalCdn
+ let cleanPath = package.downloadURL.hasPrefix("/") ? package.downloadURL : "/\(package.downloadURL)"
+ let downloadURL = cleanCdn + cleanPath
+
+ guard let url = URL(string: downloadURL) else { continue }
+
+ do {
+ if let resumeData = await globalCancelTracker.getResumeData(task.id) {
+ try await downloadPackage(package: package, task: task, product: dependencyToDownload, resumeData: resumeData)
+ } else {
+ try await downloadPackage(package: package, task: task, product: dependencyToDownload, url: url)
+ }
+ } catch {
+ print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)")
+ await handleError(task.id, error)
+ return
+ }
+ }
+ }
+
+ let allPackagesDownloaded = task.dependenciesToDownload.allSatisfy { product in
+ product.packages.allSatisfy { $0.downloaded }
+ }
+
+ if allPackagesDownloaded {
+ await MainActor.run {
+ task.setStatus(.completed(DownloadStatus.CompletionInfo(
+ timestamp: Date(),
+ totalTime: Date().timeIntervalSince(task.createAt),
+ totalSize: task.totalSize
+ )))
+ }
+ await globalNetworkManager.saveTask(task)
+ }
+ }
+
+ func handleError(_ taskId: UUID, _ error: Error) async {
+ let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
+ guard task != nil else { return }
+
+ let (errorMessage, isRecoverable) = classifyError(error)
+
+ if isRecoverable,
+ 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)
+ }
+ }
+
+ if isRecoverable && task.retryCount < NetworkConstants.maxRetryAttempts {
+ task.retryCount += 1
+ let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000))
+ task.setStatus(.retrying(DownloadStatus.RetryInfo(
+ attempt: task.retryCount,
+ maxAttempts: NetworkConstants.maxRetryAttempts,
+ reason: errorMessage,
+ nextRetryDate: nextRetryDate
+ )))
+
+ Task {
+ do {
+ try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
+ if await !(globalCancelTracker?.isCancelled(taskId) ?? false) {
+ await resumeDownloadTask(taskId: taskId)
+ }
+ } catch {
+ print("Retry cancelled for task: \(taskId)")
+ }
+ }
+ } else {
+ task.setStatus(.failed(DownloadStatus.FailureInfo(
+ message: errorMessage,
+ error: error,
+ timestamp: Date(),
+ recoverable: isRecoverable
+ )))
+
+ if let currentPackage = task.currentPackage {
+ let destinationDir = task.directory
+ .appendingPathComponent("\(task.productId)")
+ let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
+ try? FileManager.default.removeItem(at: fileURL)
+ }
+
+ await globalNetworkManager.saveTask(task)
+ await MainActor.run {
+ globalNetworkManager.updateDockBadge()
+ globalNetworkManager.objectWillChange.send()
+ }
+ }
+ }
+
+ func resumeDownloadTask(taskId: UUID) async {
+ let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
+ guard task != nil else { return }
+
+ await MainActor.run {
+ task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+ fileName: task.currentPackage?.fullPackageName ?? "",
+ currentPackageIndex: 0,
+ totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count },
+ startTime: Date(),
+ estimatedTimeRemaining: nil
+ )))
+ }
+
+ await globalNetworkManager.saveTask(task)
+ await MainActor.run {
+ globalNetworkManager.objectWillChange.send()
+ }
+
+ if task.productId == "APRO" {
+ if let resumeData = await globalCancelTracker?.getResumeData(taskId),
+ let currentPackage = task.currentPackage,
+ let product = task.dependenciesToDownload.first {
+ try? await downloadPackage(
+ package: currentPackage,
+ task: task,
+ product: product,
+ resumeData: resumeData
+ )
+ }
+ } else {
+ await startDownloadProcess(task: task)
+ }
+ }
+
+ private func classifyError(_ error: Error) -> (message: String, recoverable: Bool) {
+ switch error {
+ case let networkError as NetworkError:
+ switch networkError {
+ case .noConnection:
+ return (String(localized: "网络连接已断开"), true)
+ case .timeout:
+ return (String(localized: "下载超时"), true)
+ case .serverUnreachable:
+ return (String(localized: "服务器无法访问"), true)
+ case .insufficientStorage:
+ return (String(localized: "存储空间不足"), false)
+ case .filePermissionDenied:
+ return (String(localized: "没有写入权限"), false)
+ default:
+ return (networkError.localizedDescription, false)
+ }
+ case let urlError as URLError:
+ switch urlError.code {
+ case .notConnectedToInternet, .networkConnectionLost, .dataNotAllowed:
+ return (String(localized: "网络连接已断开"), true)
+ case .timedOut:
+ return (String(localized: "连接超时"), true)
+ case .cancelled:
+ return (String(localized: "下载已取消"), false)
+ case .cannotConnectToHost, .dnsLookupFailed:
+ return (String(localized: "无法连接到服务器"), true)
+ default:
+ return (urlError.localizedDescription, true)
+ }
+ default:
+ return (error.localizedDescription, false)
+ }
+ }
+
+
+ private func downloadPackage(package: Package, task: NewDownloadTask, product: DependenciesToDownload, url: URL? = nil, resumeData: Data? = nil) async throws {
+ var lastUpdateTime = Date()
+ var lastBytes: Int64 = 0
+
+ return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ let delegate = DownloadDelegate(
+ destinationDirectory: task.directory.appendingPathComponent(product.sapCode),
+ fileName: package.fullPackageName,
+ completionHandler: { [weak globalNetworkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
+ if let error = error {
+ if (error as NSError).code == NSURLErrorCancelled {
+ continuation.resume()
+ } else {
+ continuation.resume(throwing: error)
+ }
+ return
+ }
+
+ Task {
+ await MainActor.run {
+ package.downloadedSize = package.downloadSize
+ package.progress = 1.0
+ package.status = .completed
+ package.downloaded = true
+
+ var totalDownloaded: Int64 = 0
+ var totalSize: Int64 = 0
+
+ for prod in task.dependenciesToDownload {
+ for pkg in prod.packages {
+ totalSize += pkg.downloadSize
+ if pkg.downloaded {
+ totalDownloaded += pkg.downloadSize
+ }
+ }
+ }
+
+ task.totalSize = totalSize
+ task.totalDownloadedSize = totalDownloaded
+ task.totalProgress = Double(totalDownloaded) / Double(totalSize)
+ task.totalSpeed = 0
+
+ let allCompleted = task.dependenciesToDownload.allSatisfy {
+ product in product.packages.allSatisfy { $0.downloaded }
+ }
+
+ if allCompleted {
+ task.setStatus(.completed(DownloadStatus.CompletionInfo(
+ timestamp: Date(),
+ totalTime: Date().timeIntervalSince(task.createAt),
+ totalSize: totalSize
+ )))
+ }
+
+ product.updateCompletedPackages()
+ }
+ await globalNetworkManager.saveTask(task)
+ await MainActor.run {
+ globalNetworkManager.objectWillChange.send()
+ }
+ continuation.resume()
+ }
+ },
+ progressHandler: { [weak globalNetworkManager] (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) in
+ Task { @MainActor in
+ let now = Date()
+ let timeDiff = now.timeIntervalSince(lastUpdateTime)
+
+ if timeDiff >= 1.0 {
+ let bytesDiff = totalBytesWritten - lastBytes
+ let speed = Double(bytesDiff) / timeDiff
+
+ package.updateProgress(
+ downloadedSize: totalBytesWritten,
+ speed: speed
+ )
+
+ var totalDownloaded: Int64 = 0
+ var totalSize: Int64 = 0
+ var currentSpeed: Double = 0
+
+ for prod in task.dependenciesToDownload {
+ for pkg in prod.packages {
+ totalSize += pkg.downloadSize
+ if pkg.downloaded {
+ totalDownloaded += pkg.downloadSize
+ } else if pkg.id == package.id {
+ totalDownloaded += totalBytesWritten
+ currentSpeed = speed
+ }
+ }
+ }
+
+ task.totalSize = totalSize
+ task.totalDownloadedSize = totalDownloaded
+ task.totalProgress = totalSize > 0 ? Double(totalDownloaded) / Double(totalSize) : 0
+ task.totalSpeed = currentSpeed
+
+ lastUpdateTime = now
+ lastBytes = totalBytesWritten
+
+ globalNetworkManager.objectWillChange.send()
+ }
+ }
+ }
+ )
+
+ let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
+
+ Task {
+ let downloadTask: URLSessionDownloadTask
+ if let resumeData = resumeData {
+ downloadTask = session.downloadTask(withResumeData: resumeData)
+ } else if let url = url {
+ var request = URLRequest(url: url)
+ NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+ downloadTask = session.downloadTask(with: request)
+ } else {
+ continuation.resume(throwing: NetworkError.invalidData("Neither URL nor resume data provided"))
+ return
+ }
+
+ 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 {
+ // 获取匹配的 platform 和 languageSet
+ guard let platform = productInfo.platforms.first(where: { $0.id == "mac" }),
+ let languageSet = platform.languageSet.first else {
+ return ""
+ }
+
+ // 构建依赖列表
+ let dependencies = languageSet.dependencies.map { dependency in
+ """
+
+ \(dependency.sapCode)
+ \(dependency.baseVersion)
+ \(dependency.sapCode)
+
+ """
+ }.joined(separator: "\n")
+
+ return """
+
+
+ Adobe \(displayName)
+ \(sapCode)
+ \(version)
+ mac
+ \(sapCode)
+
+ \(dependencies)
+
+
+
+ /Applications
+ \(language)
+
+
+ """
+ }
+
+ func downloadAPRO(task: NewDownloadTask, productInfo: Product) async throws {
+ let firstPlatform = productInfo.platforms.first
+ let buildGuid = firstPlatform?.languageSet.first?.buildGuid ?? ""
+
+ let manifestURL = globalCdn + buildGuid
+ guard let url = URL(string: manifestURL) else {
+ throw NetworkError.invalidURL(manifestURL)
+ }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+
+ var headers = NetworkConstants.adobeRequestHeaders
+ headers["x-adobe-build-guid"] = buildGuid
+
+ headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+
+ let (manifestData, _) = try await URLSession.shared.data(for: request)
+
+ let manifestDoc = try XMLDocument(data: manifestData)
+
+ guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
+ let assetSizeStr = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_size").first?.stringValue,
+ let assetSize = Int64(assetSizeStr) else {
+ throw NetworkError.invalidData("无法从manifest中获取下载信息")
+ }
+
+ guard let downloadURL = URL(string: downloadPath) else {
+ throw NetworkError.invalidURL(downloadPath)
+ }
+
+ let aproPackage = Package(
+ type: "dmg",
+ fullPackageName: "Adobe Downloader \(task.productId)_\(firstPlatform?.languageSet.first?.productVersion ?? "unknown")_\(firstPlatform?.id ?? "unknown").dmg",
+ downloadSize: assetSize,
+ downloadURL: downloadPath,
+ packageVersion: ""
+ )
+
+ await MainActor.run {
+ let product = DependenciesToDownload(sapCode: task.productId, version: firstPlatform?.languageSet.first?.productVersion ?? "unknown", buildGuid: buildGuid)
+ product.packages = [aproPackage]
+ task.dependenciesToDownload = [product]
+ task.totalSize = assetSize
+ task.currentPackage = aproPackage
+ task.setStatus(.downloading(DownloadStatus.DownloadInfo(
+ fileName: aproPackage.fullPackageName,
+ currentPackageIndex: 0,
+ totalPackages: 1,
+ startTime: Date(),
+ estimatedTimeRemaining: nil
+ )))
+ }
+
+ let tempDownloadDir = task.directory.deletingLastPathComponent()
+ var lastUpdateTime = Date()
+ var lastBytes: Int64 = 0
+
+ return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ let delegate = DownloadDelegate(
+ destinationDirectory: tempDownloadDir,
+ fileName: aproPackage.fullPackageName,
+ completionHandler: { [weak globalNetworkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
+ if let error = error {
+ if (error as NSError).code == NSURLErrorCancelled {
+ continuation.resume()
+ } else {
+ print("Download error:", error)
+ continuation.resume(throwing: error)
+ }
+ return
+ }
+
+ Task {
+ await MainActor.run {
+ aproPackage.downloadedSize = aproPackage.downloadSize
+ aproPackage.progress = 1.0
+ aproPackage.status = .completed
+ aproPackage.downloaded = true
+
+ var totalDownloaded: Int64 = 0
+ var totalSize: Int64 = 0
+
+ totalSize += aproPackage.downloadSize
+ if aproPackage.downloaded {
+ totalDownloaded += aproPackage.downloadSize
+ }
+
+ task.totalSize = totalSize
+ task.totalDownloadedSize = totalDownloaded
+ task.totalProgress = Double(totalDownloaded) / Double(totalSize)
+ task.totalSpeed = 0
+
+ task.setStatus(.completed(DownloadStatus.CompletionInfo(
+ timestamp: Date(),
+ totalTime: Date().timeIntervalSince(task.createAt),
+ totalSize: totalSize
+ )))
+
+ task.objectWillChange.send()
+ }
+
+ await globalNetworkManager.saveTask(task)
+
+ await MainActor.run {
+ globalNetworkManager.updateDockBadge()
+ globalNetworkManager.objectWillChange.send()
+ }
+ continuation.resume()
+ }
+ },
+ progressHandler: { [weak globalNetworkManager] (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) in
+ Task { @MainActor in
+ let now = Date()
+ let timeDiff = now.timeIntervalSince(lastUpdateTime)
+
+ if timeDiff >= 1.0 {
+ let bytesDiff = totalBytesWritten - lastBytes
+ let speed = Double(bytesDiff) / timeDiff
+
+ aproPackage.updateProgress(
+ downloadedSize: totalBytesWritten,
+ speed: speed
+ )
+
+ task.totalDownloadedSize = totalBytesWritten
+ task.totalProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
+ task.totalSpeed = speed
+
+ lastUpdateTime = now
+ lastBytes = totalBytesWritten
+
+ task.objectWillChange.send()
+ globalNetworkManager.objectWillChange.send()
+
+ Task {
+ await globalNetworkManager.saveTask(task)
+ }
+ }
+ }
+ }
+ )
+
+ let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
+
+ var downloadRequest = URLRequest(url: downloadURL)
+ NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
+
+ let downloadTask = session.downloadTask(with: downloadRequest)
+
+ Task {
+ await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session)
+
+ if await (globalCancelTracker?.isCancelled(task.id) ?? false) {
+ continuation.resume(throwing: NetworkError.cancelled)
+ return
+ }
+
+ downloadTask.resume()
+ }
+ }
+ }
+
+ func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
+ let task = await globalCancelTracker?.downloadTasks[taskId]
+ if let downloadTask = task {
+ let data = await withCheckedContinuation { continuation in
+ downloadTask.cancel(byProducingResumeData: { data in
+ continuation.resume(returning: data)
+ })
+ }
+ if let data = data {
+ await globalCancelTracker?.storeResumeData(taskId, data: data)
+ }
+ }
+
+ if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) {
+ task.setStatus(.paused(DownloadStatus.PauseInfo(
+ reason: reason,
+ timestamp: Date(),
+ resumable: true
+ )))
+ await globalNetworkManager.saveTask(task)
+ await MainActor.run {
+ globalNetworkManager.objectWillChange.send()
+ }
+ }
+ }
+
+ func getApplicationInfo(buildGuid: String) async throws -> String {
+ guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
+ throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
+ }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+
+ var headers = NetworkConstants.adobeRequestHeaders
+ headers["x-adobe-build-guid"] = buildGuid
+
+ headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw NetworkError.invalidResponse
+ }
+
+ guard (200...299).contains(httpResponse.statusCode) else {
+ throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
+ }
+
+ guard let jsonString = String(data: data, encoding: .utf8) else {
+ throw NetworkError.invalidData(String(localized: "无法将响应数据转换为json字符串"))
+ }
+
+ return jsonString
+ }
+
+ private func compareVersions(current: Double, required: Double, operator: String) -> Bool {
+ switch `operator` {
+ case ">=":
+ return current >= required
+ case "<=":
+ return current <= required
+ case ">":
+ return current > required
+ case "<":
+ return current < required
+ case "==":
+ return current == required
+ default:
+ return false
+ }
+ }
+}
diff --git a/Adobe Downloader/Utils/NewJSONParser.swift b/Adobe Downloader/Utils/NewJSONParser.swift
index ee4957d..4c385b2 100644
--- a/Adobe Downloader/Utils/NewJSONParser.swift
+++ b/Adobe Downloader/Utils/NewJSONParser.swift
@@ -14,14 +14,21 @@ import Foundation
*/
class NewJSONParser {
- static func parse(jsonString: String) throws -> NewParseResult {
+ static func parse(jsonString: String) throws {
guard let jsonData = jsonString.data(using: .utf8),
let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else {
throw ParserError.invalidJSON
}
let apiVersion = Int(StorageData.shared.apiVersion) ?? 6
- return try parseSti(jsonObject: jsonObject, apiVersion: apiVersion)
-// return try parseCcm(jsonObject: jsonObject, apiVersion: apiVersion)
+ globalStiResult = try parseSti(jsonObject: jsonObject, apiVersion: apiVersion)
+ globalCcmResult = try parseCcm(jsonObject: jsonObject, apiVersion: apiVersion)
+
+ // 更新全局 CDN
+ if !globalCcmResult.cdn.isEmpty {
+ globalCdn = globalCcmResult.cdn
+ } else if !globalStiResult.cdn.isEmpty {
+ globalCdn = globalStiResult.cdn
+ }
}
static func parseStiProducts(jsonString: String) throws {
@@ -32,6 +39,9 @@ class NewJSONParser {
let apiVersion = Int(StorageData.shared.apiVersion) ?? 6
let result = try parseSti(jsonObject: jsonObject, apiVersion: apiVersion)
globalStiResult = result
+
+ // 更新全局 CDN
+ globalCdn = result.cdn
}
static func parseCcmProducts(jsonString: String) throws {
@@ -42,6 +52,9 @@ class NewJSONParser {
let apiVersion = Int(StorageData.shared.apiVersion) ?? 6
let result = try parseCcm(jsonObject: jsonObject, apiVersion: apiVersion)
globalCcmResult = result
+
+ // 更新全局 CDN
+ globalCdn = result.cdn
}
private static func parseSti(jsonObject: [String: Any], apiVersion: Int) throws -> NewParseResult {
@@ -273,8 +286,8 @@ class NewJSONParser {
var productVersion = ""
var buildGuid = ""
- if let stiProducts = globalStiResult?.products {
- let matchingProducts = stiProducts.filter { $0.id == sapCode }
+ if !globalStiResult.products.isEmpty {
+ let matchingProducts = globalStiResult.products.filter { $0.id == sapCode }
if let latestProduct = matchingProducts.sorted(by: {
return AppStatics.compareVersions($0.version, $1.version) < 0
diff --git a/Adobe Downloader/Utils/TaskPersistenceManager.swift b/Adobe Downloader/Utils/TaskPersistenceManager.swift
index 23c46e6..d6cfbe5 100644
--- a/Adobe Downloader/Utils/TaskPersistenceManager.swift
+++ b/Adobe Downloader/Utils/TaskPersistenceManager.swift
@@ -26,8 +26,8 @@ class TaskPersistenceManager {
func saveTask(_ task: NewDownloadTask) async {
let fileName = getTaskFileName(
- sapCode: task.sapCode,
- version: task.version,
+ sapCode: task.productId,
+ version: task.productVersion,
language: task.language,
platform: task.platform
)
@@ -43,12 +43,12 @@ class TaskPersistenceManager {
}
let taskData = TaskData(
- sapCode: task.sapCode,
- version: task.version,
+ sapCode: task.productId,
+ version: task.productVersion,
language: task.language,
displayName: task.displayName,
directory: task.directory,
- productsToDownload: task.productsToDownload.map { product in
+ productsToDownload: task.dependenciesToDownload.map { product in
ProductData(
sapCode: product.sapCode,
version: product.version,
@@ -120,8 +120,8 @@ class TaskPersistenceManager {
let decoder = JSONDecoder()
let taskData = try decoder.decode(TaskData.self, from: data)
- let products = taskData.productsToDownload.map { productData -> ProductsToDownload in
- let product = ProductsToDownload(
+ let products = taskData.productsToDownload.map { productData -> DependenciesToDownload in
+ let product = DependenciesToDownload(
sapCode: productData.sapCode,
version: productData.version,
buildGuid: productData.buildGuid,
@@ -174,12 +174,12 @@ class TaskPersistenceManager {
}
let task = NewDownloadTask(
- sapCode: taskData.sapCode,
- version: taskData.version,
+ productId: taskData.sapCode,
+ productVersion: taskData.version,
language: taskData.language,
displayName: taskData.displayName,
directory: taskData.directory,
- productsToDownload: products,
+ dependenciesToDownload: products,
retryCount: taskData.retryCount,
createAt: taskData.createAt,
totalStatus: initialStatus,
@@ -209,8 +209,8 @@ class TaskPersistenceManager {
func removeTask(_ task: NewDownloadTask) {
let fileName = getTaskFileName(
- sapCode: task.sapCode,
- version: task.version,
+ sapCode: task.productId,
+ version: task.productVersion,
language: task.language,
platform: task.platform
)
@@ -228,7 +228,7 @@ class TaskPersistenceManager {
platform: platform
)
- let product = ProductsToDownload(
+ let product = DependenciesToDownload(
sapCode: sapCode,
version: version,
buildGuid: "",
@@ -249,12 +249,12 @@ class TaskPersistenceManager {
product.packages = [package]
let task = NewDownloadTask(
- sapCode: sapCode,
- version: version,
+ productId: sapCode,
+ productVersion: version,
language: language,
displayName: displayName,
directory: directory,
- productsToDownload: [product],
+ dependenciesToDownload: [product],
retryCount: 0,
createAt: Date(),
totalStatus: .completed(DownloadStatus.CompletionInfo(
diff --git a/Adobe Downloader/Views/AppCardView.swift b/Adobe Downloader/Views/AppCardView.swift
index 5fc88c7..498e05e 100644
--- a/Adobe Downloader/Views/AppCardView.swift
+++ b/Adobe Downloader/Views/AppCardView.swift
@@ -468,38 +468,6 @@ private struct SheetModifier: ViewModifier {
}
}
-#Preview {
- let networkManager = NetworkManager()
- let sap = Sap(
- hidden: false,
- displayName: "Photoshop",
- sapCode: "PHSP",
- versions: [
- "25.0.0": Sap.Versions(
- sapCode: "PHSP",
- baseVersion: "25.0.0",
- productVersion: "25.0.0",
- apPlatform: "macuniversal",
- dependencies: [
- Sap.Versions.Dependencies(sapCode: "ACR", version: "9.6"),
- Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0"),
- Sap.Versions.Dependencies(sapCode: "COSY", version: "2.4.1")
- ],
- buildGuid: ""
- )
- ],
- icons: [
- Sap.ProductIcon(
- size: "192x192",
- url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/25.0.0/192x192.png"
- )
- ]
- )
-
- return AppCardView(sap: sap)
- .environmentObject(networkManager)
-}
-
struct AlertModifier: ViewModifier {
@ObservedObject var viewModel: AppCardViewModel
let confirmRedownload: Bool
diff --git a/Adobe Downloader/Views/DownloadProgressView.swift b/Adobe Downloader/Views/DownloadProgressView.swift
index 87ab6df..8f7c96d 100644
--- a/Adobe Downloader/Views/DownloadProgressView.swift
+++ b/Adobe Downloader/Views/DownloadProgressView.swift
@@ -301,43 +301,45 @@ struct DownloadProgressView: View {
}
private func loadIcon() {
- if let sap = networkManager.saps[task.sapCode],
- let bestIcon = sap.getBestIcon(),
- let iconURL = URL(string: bestIcon.url) {
-
- if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
- self.iconImage = cachedImage
- return
- }
-
- Task {
- do {
- var request = URLRequest(url: iconURL)
- request.timeoutInterval = 10
-
- let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse,
- (200...299).contains(httpResponse.statusCode),
- let image = NSImage(data: data) else {
- throw URLError(.badServerResponse)
- }
-
- IconCache.shared.setIcon(image, for: bestIcon.url)
-
- await MainActor.run {
- self.iconImage = image
- }
- } catch {
- if let localImage = NSImage(named: task.sapCode) {
+ let product = globalCcmResult.products.first { $0.id == task.productId }
+ if product != nil {
+ if let bestIcon = product.getBestIcon(),
+ let iconURL = URL(string: bestIcon.url) {
+
+ if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
+ self.iconImage = cachedImage
+ return
+ }
+
+ Task {
+ do {
+ var request = URLRequest(url: iconURL)
+ request.timeoutInterval = 10
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse,
+ (200...299).contains(httpResponse.statusCode),
+ let image = NSImage(data: data) else {
+ throw URLError(.badServerResponse)
+ }
+
+ IconCache.shared.setIcon(image, for: bestIcon.url)
+
await MainActor.run {
- self.iconImage = localImage
+ self.iconImage = image
+ }
+ } catch {
+ if let localImage = NSImage(named: task.sapCode) {
+ await MainActor.run {
+ self.iconImage = localImage
+ }
}
}
}
+ } else if let localImage = NSImage(named: task.productId) {
+ self.iconImage = localImage
}
- } else if let localImage = NSImage(named: task.sapCode) {
- self.iconImage = localImage
}
}
@@ -377,7 +379,7 @@ struct DownloadProgressView: View {
HStack(spacing: 4) {
Text(task.displayName)
.font(.headline)
- Text(task.version)
+ Text(task.productVersion)
.foregroundColor(.secondary)
}
@@ -430,7 +432,7 @@ struct DownloadProgressView: View {
.progressViewStyle(.linear)
}
- if !task.productsToDownload.isEmpty {
+ if !task.dependenciesToDownload.isEmpty {
Divider()
VStack(alignment: .leading, spacing: 6) {
@@ -547,7 +549,7 @@ struct DownloadProgressView: View {
}
struct ProductRow: View {
- @ObservedObject var product: ProductsToDownload
+ @ObservedObject var dependencies: DependenciesToDownload
let isCurrentProduct: Bool
@Binding var expandedProducts: Set
@@ -688,145 +690,3 @@ struct PackageRow: View {
.cornerRadius(6)
}
}
-
-#Preview("下载中") {
- let product = ProductsToDownload(
- sapCode: "AUDT",
- version: "25.0",
- buildGuid: "123"
- )
- product.packages = [
- Package(
- type: "Application",
- fullPackageName: "AdobeAudition25All",
- downloadSize: 878454797,
- downloadURL: "https://example.com/download",
- packageVersion: "25.0.0.1"
- )
- ]
-
- return DownloadProgressView(
- task: NewDownloadTask(
- sapCode: "AUDT",
- version: "25.0",
- language: "zh_CN",
- displayName: "Adobe Audition",
- directory: URL(fileURLWithPath: "/Users/test/Downloads/Adobe Audition_25.0-zh_CN-macuniversal"),
- productsToDownload: [product],
- createAt: Date(),
- totalStatus: .downloading(DownloadStatus.DownloadInfo(
- fileName: "AdobeAudition25All_stripped.zip",
- currentPackageIndex: 0,
- totalPackages: 2,
- startTime: Date(),
- estimatedTimeRemaining: nil
- )),
- totalProgress: 0.45,
- totalDownloadedSize: 457424883,
- totalSize: 878454797,
- totalSpeed: 1024 * 1024 * 2,
- platform: "macuniversal"
- ),
- onCancel: {},
- onPause: {},
- onResume: {},
- onRetry: {},
- onRemove: {}
- )
- .environmentObject(NetworkManager())
-}
-
-#Preview("已完成") {
- let product = ProductsToDownload(
- sapCode: "AUDT",
- version: "25.0",
- buildGuid: "123"
- )
- let package = Package(
- type: "Application",
- fullPackageName: "AdobeAudition25All",
- downloadSize: 878454797,
- downloadURL: "https://example.com/download",
- packageVersion: "25.0.0.1"
- )
- package.status = .completed
- package.progress = 1.0
- package.downloadedSize = 878454797
- package.downloaded = true
- product.packages = [package]
-
- return DownloadProgressView(
- task: NewDownloadTask(
- sapCode: "AUDT",
- version: "25.0",
- language: "zh_CN",
- displayName: "Adobe Audition",
- directory: URL(fileURLWithPath: "/Users/test/Downloads/Adobe Audition_25.0-zh_CN-macuniversal"),
- productsToDownload: [product],
- createAt: Date(),
- totalStatus: .completed(DownloadStatus.CompletionInfo(
- timestamp: Date(),
- totalTime: 120,
- totalSize: 878454797
- )),
- totalProgress: 1.0,
- totalDownloadedSize: 878454797,
- totalSize: 878454797,
- totalSpeed: 0,
- platform: "macuniversal"
- ),
- onCancel: {},
- onPause: {},
- onResume: {},
- onRetry: {},
- onRemove: {}
- )
- .environmentObject(NetworkManager())
-}
-
-#Preview("暂停") {
- let product = ProductsToDownload(
- sapCode: "AUDT",
- version: "25.0",
- buildGuid: "123"
- )
- let package = Package(
- type: "Application",
- fullPackageName: "AdobeAudition25All",
- downloadSize: 878454797,
- downloadURL: "https://example.com/download",
- packageVersion: "25.0.0.1"
- )
- package.status = .paused
- package.progress = 0.52
- package.downloadedSize = 457424883
- product.packages = [package]
-
- return DownloadProgressView(
- task: NewDownloadTask(
- sapCode: "AUDT",
- version: "25.0",
- language: "zh_CN",
- displayName: "Adobe Audition",
- directory: URL(fileURLWithPath: "/Users/test/Downloads/Adobe Audition_25.0-zh_CN-macuniversal"),
- productsToDownload: [product],
- createAt: Date(),
- totalStatus: .paused(DownloadStatus.PauseInfo(
- reason: .userRequested,
- timestamp: Date(),
- resumable: true
- )),
- totalProgress: 0.52,
- totalDownloadedSize: 457424883,
- totalSize: 878454797,
- totalSpeed: 0,
- platform: "macuniversal"
- ),
- onCancel: {},
- onPause: {},
- onResume: {},
- onRetry: {},
- onRemove: {}
- )
- .environmentObject(NetworkManager())
-}
diff --git a/Adobe Downloader/Views/VersionPickerView.swift b/Adobe Downloader/Views/VersionPickerView.swift
index cedf386..fd56948 100644
--- a/Adobe Downloader/Views/VersionPickerView.swift
+++ b/Adobe Downloader/Views/VersionPickerView.swift
@@ -27,19 +27,19 @@ struct VersionPickerView: View {
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
@State private var expandedVersions: Set = []
- private let sap: Sap
+ private let product: Product
private let onSelect: (String) -> Void
- init(sap: Sap, onSelect: @escaping (String) -> Void) {
- self.sap = sap
+ init(product: Product, onSelect: @escaping (String) -> Void) {
+ self.product = product
self.onSelect = onSelect
}
var body: some View {
VStack(spacing: 0) {
- HeaderView(sap: sap, downloadAppleSilicon: downloadAppleSilicon)
+ HeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon)
VersionListView(
- sap: sap,
+ product: product,
expandedVersions: $expandedVersions,
onSelect: onSelect,
dismiss: dismiss
@@ -50,7 +50,7 @@ struct VersionPickerView: View {
}
private struct HeaderView: View {
- let sap: Sap
+ let product: Product
let downloadAppleSilicon: Bool
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var networkManager: NetworkManager
@@ -58,7 +58,7 @@ private struct HeaderView: View {
var body: some View {
VStack {
HStack {
- Text("\(sap.displayName)")
+ Text("\(product.displayName)")
.font(.headline)
Text("选择版本")
.foregroundColor(.secondary)
@@ -86,7 +86,7 @@ private struct HeaderView: View {
private struct VersionListView: View {
@EnvironmentObject private var networkManager: NetworkManager
- let sap: Sap
+ let product: Product
@Binding var expandedVersions: Set
let onSelect: (String) -> Void
let dismiss: DismissAction
@@ -96,7 +96,7 @@ private struct VersionListView: View {
LazyVStack(spacing: VersionPickerConstants.verticalSpacing) {
ForEach(filteredVersions, id: \.key) { version, info in
VersionRow(
- sap: sap,
+ product: product,
version: version,
info: info,
isExpanded: expandedVersions.contains(version),
@@ -110,10 +110,26 @@ private struct VersionListView: View {
.background(Color(NSColor.windowBackgroundColor))
}
- private var filteredVersions: [(key: String, value: Sap.Versions)] {
- sap.versions
- .filter { StorageData.shared.allowedPlatform.contains($0.value.apPlatform) }
- .sorted { AppStatics.compareVersions($0.key, $1.key) > 0 }
+ private var filteredVersions: [(key: String, value: Product.Platform)] {
+ // 获取支持的平台
+ let platforms = product.platforms.filter { platform in
+ StorageData.shared.allowedPlatform.contains(platform.id) &&
+ platform.languageSet.first != nil
+ }
+
+ // 如果没有支持的平台,返回空数组
+ if platforms.isEmpty {
+ return []
+ }
+
+ // 将平台按版本号降序排序
+ return platforms.map { platform in
+ // 使用第一个语言集的 productVersion 作为版本号
+ (key: platform.languageSet.first?.productVersion ?? "", value: platform)
+ }.sorted { pair1, pair2 in
+ // 按版本号降序排序
+ AppStatics.compareVersions(pair1.key, pair2.key) > 0
+ }
}
private func handleVersionSelect(_ version: String) {
@@ -133,19 +149,18 @@ private struct VersionListView: View {
}
private struct VersionRow: View {
- @EnvironmentObject private var networkManager: NetworkManager
@StorageValue(\.defaultLanguage) private var defaultLanguage
- let sap: Sap
+ let product: Product
let version: String
- let info: Sap.Versions
+ let info: Product.Platform
let isExpanded: Bool
let onSelect: (String) -> Void
let onToggle: (String) -> Void
private var existingPath: URL? {
- networkManager.isVersionDownloaded(
- sap: sap,
+ globalNetworkManager.isVersionDownloaded(
+ product: product,
version: version,
language: defaultLanguage
)
@@ -176,7 +191,8 @@ private struct VersionRow: View {
}
private func handleSelect() {
- if info.dependencies.isEmpty {
+ let dependencies = info.languageSet.first?.dependencies ?? []
+ if dependencies.isEmpty {
onSelect(version)
} else {
onToggle(version)
@@ -186,7 +202,7 @@ private struct VersionRow: View {
private struct VersionHeader: View {
let version: String
- let info: Sap.Versions
+ let info: Product.Platform
let isExpanded: Bool
let hasExistingPath: Bool
let onSelect: () -> Void
@@ -195,12 +211,13 @@ private struct VersionHeader: View {
var body: some View {
Button(action: onSelect) {
HStack {
- VersionInfo(version: version, platform: info.apPlatform)
+ VersionInfo(version: version, platform: info.id)
Spacer()
ExistingPathButton(isVisible: hasExistingPath)
ExpandButton(
isExpanded: isExpanded,
- hasDependencies: !info.dependencies.isEmpty
+ onToggle: onToggle,
+ hasDependencies: !(info.languageSet.first?.dependencies.isEmpty ?? true)
)
}
.padding(.vertical, VersionPickerConstants.buttonPadding)
@@ -243,11 +260,14 @@ private struct ExistingPathButton: View {
private struct ExpandButton: View {
let isExpanded: Bool
+ let onToggle: () -> Void
let hasDependencies: Bool
var body: some View {
- Image(systemName: iconName)
- .foregroundColor(.secondary)
+ Button(action: onToggle) {
+ Image(systemName: iconName)
+ .foregroundColor(.secondary)
+ }
}
private var iconName: String {
@@ -259,7 +279,7 @@ private struct ExpandButton: View {
}
private struct VersionDetails: View {
- let info: Sap.Versions
+ let info: Product.Platform
let version: String
let onSelect: (String) -> Void
@@ -271,7 +291,7 @@ private struct VersionDetails: View {
.padding(.top, 8)
.padding(.leading, 16)
- DependenciesList(dependencies: info.dependencies)
+ DependenciesList(dependencies: info.languageSet.first?.dependencies ?? [])
DownloadButton(version: version, onSelect: onSelect)
}
@@ -281,15 +301,15 @@ private struct VersionDetails: View {
}
private struct DependenciesList: View {
- let dependencies: [Sap.Versions.Dependencies]
-
+ let dependencies: [Product.Platform.LanguageSet.Dependency]
+
var body: some View {
ForEach(dependencies, id: \.sapCode) { dependency in
HStack(spacing: 8) {
Image(systemName: "cube.box")
.foregroundColor(.blue)
.frame(width: 16)
- Text("\(dependency.sapCode) (\(dependency.version))")
+ Text("\(dependency.sapCode) (\(dependency.baseVersion))")
.font(.caption)
Spacer()
}
@@ -311,54 +331,3 @@ private struct DownloadButton: View {
.padding(.leading, 16)
}
}
-
-struct VersionPickerView_Previews: PreviewProvider {
- static var previews: some View {
- let networkManager = NetworkManager()
- networkManager.cdn = "https://example.cdn.adobe.com"
-
- let previewSap = Sap(
- hidden: false,
- displayName: "Photoshop",
- sapCode: "PHSP",
- versions: [
- "26.0.0": Sap.Versions(
- sapCode: "PHSP",
- baseVersion: "26.0.0",
- productVersion: "26.0.0",
- apPlatform: "macuniversal",
- dependencies: [
- Sap.Versions.Dependencies(sapCode: "ACR", version: "9.6"),
- Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0"),
- Sap.Versions.Dependencies(sapCode: "COSY", version: "2.4.1")
- ],
- buildGuid: "b382ef03-c44a-4fd4-a9a1-3119ab0474b4"
- ),
- "25.0.0": Sap.Versions(
- sapCode: "PHSP",
- baseVersion: "25.0.0",
- productVersion: "25.0.0",
- apPlatform: "macuniversal",
- dependencies: [
- Sap.Versions.Dependencies(sapCode: "ACR", version: "9.5"),
- Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0")
- ],
- buildGuid: "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6"
- ),
- "24.0.0": Sap.Versions(
- sapCode: "PHSP",
- baseVersion: "24.0.0",
- productVersion: "24.0.0",
- apPlatform: "macuniversal",
- dependencies: [],
- buildGuid: "q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6"
- )
- ],
- icons: []
- )
-
- return VersionPickerView(sap: previewSap) { _ in }
- .environmentObject(networkManager)
- .previewDisplayName("Version Picker")
- }
-}