mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
refactor: save works
This commit is contained in:
@@ -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">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
@@ -36,19 +36,5 @@
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "FF32C0C6-C193-4B43-831E-E6F00D1EE00D"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Adobe Downloader/Commons/Extensions.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "22"
|
||||
endingLineNumber = "22">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
</Bucket>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
//}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
//}
|
||||
|
||||
99
Adobe Downloader/Models/NewDownloadTask.swift
Normal file
99
Adobe Downloader/Models/NewDownloadTask.swift
Normal file
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
//}
|
||||
|
||||
107
Adobe Downloader/Services/NewNetworkService.swift
Normal file
107
Adobe Downloader/Services/NewNetworkService.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -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 &&
|
||||
947
Adobe Downloader/Utils/NewDownloadUtils.swift
Normal file
947
Adobe Downloader/Utils/NewDownloadUtils.swift
Normal file
@@ -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..<condition.endIndex, in: condition)
|
||||
|
||||
if let matches = regex?.matches(in: condition, range: range) {
|
||||
var meetsAllConditions = true
|
||||
|
||||
for match in matches {
|
||||
guard let operatorRange = Range(match.range(at: 1), in: condition),
|
||||
let versionRange = Range(match.range(at: 2), in: condition),
|
||||
let requiredVersion = Double(condition[versionRange]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let operatorSymbol = String(condition[operatorRange])
|
||||
let meets = compareVersions(current: currentVersion, required: requiredVersion, operator: operatorSymbol)
|
||||
|
||||
if !meets {
|
||||
meetsAllConditions = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if meetsAllConditions {
|
||||
shouldDownload = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if condition.contains("[OSArchitecture]==\(AppStatics.architectureSymbol)") {
|
||||
shouldDownload = true
|
||||
}
|
||||
if condition.contains(installLanguage) || task.language == "ALL" {
|
||||
shouldDownload = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
shouldDownload = true
|
||||
}
|
||||
|
||||
if isCore {
|
||||
corePackageCount += 1
|
||||
} else {
|
||||
nonCorePackageCount += 1
|
||||
}
|
||||
|
||||
if shouldDownload {
|
||||
let newPackage = Package(
|
||||
type: packageType,
|
||||
fullPackageName: fullPackageName,
|
||||
downloadSize: downloadSize,
|
||||
downloadURL: downloadURL,
|
||||
packageVersion: packageVersion
|
||||
)
|
||||
dependencyToDownload.packages.append(newPackage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let finalProducts = dependenciesToDownload
|
||||
let totalSize = finalProducts.reduce(0) { productSum, product in
|
||||
productSum + product.packages.reduce(0) { packageSum, pkg in
|
||||
packageSum + (pkg.downloadSize > 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<Void, Error>) 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.sapCode)</SAPCode>
|
||||
<BaseVersion>\(dependency.baseVersion)</BaseVersion>
|
||||
<EsdDirectory>\(dependency.sapCode)</EsdDirectory>
|
||||
</Dependency>
|
||||
"""
|
||||
}.joined(separator: "\n")
|
||||
|
||||
return """
|
||||
<DriverInfo>
|
||||
<ProductInfo>
|
||||
<n>Adobe \(displayName)</n>
|
||||
<SAPCode>\(sapCode)</SAPCode>
|
||||
<CodexVersion>\(version)</CodexVersion>
|
||||
<Platform>mac</Platform>
|
||||
<EsdDirectory>\(sapCode)</EsdDirectory>
|
||||
<Dependencies>
|
||||
\(dependencies)
|
||||
</Dependencies>
|
||||
</ProductInfo>
|
||||
<RequestInfo>
|
||||
<InstallDir>/Applications</InstallDir>
|
||||
<InstallLanguage>\(language)</InstallLanguage>
|
||||
</RequestInfo>
|
||||
</DriverInfo>
|
||||
"""
|
||||
}
|
||||
|
||||
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<Void, Error>) 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -27,19 +27,19 @@ struct VersionPickerView: View {
|
||||
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
||||
@State private var expandedVersions: Set<String> = []
|
||||
|
||||
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<String>
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user