refactor: save works

This commit is contained in:
X1a0He
2025-02-27 23:02:40 +08:00
parent 01e88cec69
commit b816dcf159
21 changed files with 3545 additions and 2445 deletions

View File

@@ -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>

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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 {
// sapCodeAdobeidsapCode
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"
]
}

View File

@@ -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
//}

View File

@@ -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)
}

View File

@@ -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
// }
//}

View 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
}
}

View File

@@ -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
// }
//}

View 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

View File

@@ -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)
// }
//}

View File

@@ -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 &&

View 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:
ACCApp
*/
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
}
}
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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")
}
}