refactor: complete initial reconstruction

This commit is contained in:
X1a0He
2025-03-05 21:09:14 +08:00
parent b816dcf159
commit 4b9dc3d417
17 changed files with 1016 additions and 652 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.DS_Store .DS_Store
*.DS_Store *.DS_Store
.idea/ .idea/
.fleet
## User settings ## User settings
xcuserdata/ xcuserdata/

View File

@@ -12,7 +12,7 @@
<key>Adobe-Downloader.xcscheme_^#shared#^_</key> <key>Adobe-Downloader.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>1</integer>
</dict> </dict>
<key>AdobeDownloaderHelperTool.xcscheme_^#shared#^_</key> <key>AdobeDownloaderHelperTool.xcscheme_^#shared#^_</key>
<dict> <dict>

View File

@@ -105,7 +105,6 @@ struct Adobe_DownloaderApp: App {
Settings { Settings {
AboutView(updater: updaterController.updater) AboutView(updater: updaterController.updater)
.environmentObject(globalNetworkManager)
} }
} }

View File

@@ -55,6 +55,7 @@ enum NetworkError: Error, LocalizedError {
case unsupportedPlatform(String) case unsupportedPlatform(String)
case incompatibleVersion(String, String) case incompatibleVersion(String, String)
case installError(String) case installError(String)
case productNotFound
private var errorGroup: Int { private var errorGroup: Int {
switch self { switch self {
@@ -64,7 +65,7 @@ enum NetworkError: Error, LocalizedError {
case .httpError, .serverError, .clientError: return 4000 case .httpError, .serverError, .clientError: return 4000
case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000 case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000
case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000 case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000
case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError: return 7000 case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError, .productNotFound: return 7000
} }
} }
@@ -94,6 +95,7 @@ enum NetworkError: Error, LocalizedError {
case .unsupportedPlatform: return 2 case .unsupportedPlatform: return 2
case .incompatibleVersion: return 3 case .incompatibleVersion: return 3
case .installError: return 4 case .installError: return 4
case .productNotFound: return 5
} }
} }
@@ -160,6 +162,8 @@ enum NetworkError: Error, LocalizedError {
return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled") return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
case .installError(let message): case .installError(let message):
return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message) return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message)
case .productNotFound:
return NSLocalizedString("产品未找到", value: "产品未找到", comment: "Product not found")
} }
} }

View File

@@ -8,6 +8,8 @@
// //
private var _globalStiResult: NewParseResult? private var _globalStiResult: NewParseResult?
private var _globalCcmResult: NewParseResult? private var _globalCcmResult: NewParseResult?
private var _globalProducts: [Product]?
private var _globalUniqueProducts: [UniqueProduct]?
private var _globalCdn: String = "" private var _globalCdn: String = ""
private var _globalNetworkService: NewNetworkService? private var _globalNetworkService: NewNetworkService?
private var _globalNetworkManager: NetworkManager? private var _globalNetworkManager: NetworkManager?
@@ -96,6 +98,30 @@ var globalCancelTracker: CancelTracker {
} }
} }
var globalProducts: [Product] {
get {
if _globalProducts == nil {
_globalProducts = []
}
return _globalProducts!
}
set {
_globalProducts = newValue
}
}
var globalUniqueProducts: [UniqueProduct] {
get {
if _globalUniqueProducts == nil {
_globalUniqueProducts = []
}
return _globalUniqueProducts!
}
set {
_globalUniqueProducts = newValue
}
}
func getAllProducts() -> [Product] { func getAllProducts() -> [Product] {
var allProducts = [Product]() var allProducts = [Product]()
let stiProducts = globalStiResult.products let stiProducts = globalStiResult.products
@@ -108,3 +134,93 @@ func getAllProducts() -> [Product] {
} }
return allProducts return allProducts
} }
/// ID
/// - Parameters:
/// - id: ID
/// - version:
/// - Returns:
func findProduct(id: String, version: String? = nil) -> Product? {
// ID
guard let product = globalProducts.first(where: { $0.id == id }) else {
return nil
}
//
guard let version = version else {
return product
}
//
for platform in product.platforms {
for languageSet in platform.languageSet {
if languageSet.productVersion == version {
return product
}
}
}
return nil
}
///
/// - Parameter id: ID
/// - Returns:
func getProductVersions(id: String) -> [String] {
guard let product = globalProducts.first(where: { $0.id == id }) else {
return []
}
//
var versions = Set<String>()
for platform in product.platforms {
for languageSet in platform.languageSet {
versions.insert(languageSet.productVersion)
}
}
//
return Array(versions).sorted { version1, version2 in
version1.compare(version2, options: .numeric) == .orderedDescending
}
}
///
/// - Parameters:
/// - id: ID
/// - version:
/// - Returns:
func getProductLanguages(id: String, version: String) -> [String] {
guard let product = globalProducts.first(where: { $0.id == id }) else {
return []
}
var languages = Set<String>()
for platform in product.platforms {
for languageSet in platform.languageSet {
if languageSet.productVersion == version {
languages.insert(languageSet.name)
}
}
}
return Array(languages).sorted()
}
/// ID
/// - Parameter id: ID
/// - Returns:
func findProducts(id: String) -> [Product] {
var matchedProducts = [Product]()
// globalProducts
matchedProducts.append(contentsOf: globalProducts.filter { $0.id == id })
// globalCcmResult
matchedProducts.append(contentsOf: globalCcmResult.products.filter { $0.id == id })
// globalStiResult
matchedProducts.append(contentsOf: globalStiResult.products.filter { $0.id == id })
return matchedProducts
}

View File

@@ -6,7 +6,6 @@ struct ContentView: View {
@State private var showDownloadManager = false @State private var showDownloadManager = false
@State private var searchText = "" @State private var searchText = ""
@State private var currentApiVersion = StorageData.shared.apiVersion @State private var currentApiVersion = StorageData.shared.apiVersion
@State private var cachedProducts: [UniqueProduct] = []
private var apiVersion: String { private var apiVersion: String {
get { StorageData.shared.apiVersion } get { StorageData.shared.apiVersion }
@@ -17,13 +16,10 @@ struct ContentView: View {
} }
private var filteredProducts: [UniqueProduct] { private var filteredProducts: [UniqueProduct] {
if searchText.isEmpty { if searchText.isEmpty { return globalUniqueProducts }
return cachedProducts
}
return cachedProducts.filter { return globalUniqueProducts.filter {
$0.displayName.localizedCaseInsensitiveContains(searchText) || $0.displayName.localizedCaseInsensitiveContains(searchText) || $0.id.localizedCaseInsensitiveContains(searchText)
$0.id.localizedCaseInsensitiveContains(searchText)
} }
} }
@@ -31,22 +27,14 @@ struct ContentView: View {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} }
private func updateProductsCache() { private func refreshData() {
// isRefreshing = true
let validProducts = globalCcmResult.products errorMessage = nil
.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
Task {
// 使ID await globalNetworkManager.fetchProducts()
var uniqueProductsDict = [String: UniqueProduct]() await MainActor.run { isRefreshing = false }
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 = uniqueProducts
} }
var body: some View { var body: some View {
@@ -60,9 +48,7 @@ struct ContentView: View {
await globalNetworkManager.fetchProducts() await globalNetworkManager.fetchProducts()
} }
} }
)) { )) { Text("Apple Silicon") }
Text("Apple Silicon")
}
.toggleStyle(.switch) .toggleStyle(.switch)
.tint(.green) .tint(.green)
.disabled(isRefreshing) .disabled(isRefreshing)
@@ -137,7 +123,7 @@ struct ContentView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill") Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange) .foregroundColor(.orange)
Text("Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader") Text("Adobe Downloader 完全免费: https://github.com/X1a0He/Adobe-Downloader")
} }
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal) .padding(.horizontal)
@@ -204,8 +190,8 @@ struct ContentView: View {
], ],
spacing: 20 spacing: 20
) { ) {
ForEach(filteredProducts, id: \.sapCode) { sap in ForEach(filteredProducts, id: \.id) { uniqueProduct in
AppCardView(sap: sap) AppCardView(uniqueProduct: uniqueProduct)
} }
} }
.padding() .padding()
@@ -224,63 +210,30 @@ struct ContentView: View {
} }
} }
} }
.sheet(isPresented: $showDownloadManager) { .sheet(isPresented: $showDownloadManager) { DownloadManagerView() }
DownloadManagerView() .onAppear { if globalCcmResult.products.isEmpty { refreshData() } }
.environmentObject(globalNetworkManager)
}
.onAppear {
if globalCcmResult.products.isEmpty {
refreshData()
} else {
updateProductsCache()
}
}
.onChange(of: globalNetworkManager.saps) { _ in
updateProductsCache()
}
} }
private func refreshData() { struct SearchField: View {
isRefreshing = true @Binding var text: String
errorMessage = nil
Task { var body: some View {
await globalNetworkManager.fetchProducts() HStack {
await MainActor.run { Image(systemName: "magnifyingglass")
updateProductsCache() .foregroundColor(.secondary)
isRefreshing = false TextField("搜索应用", text: $text)
} .textFieldStyle(PlainTextFieldStyle())
} if !text.isEmpty {
} Button(action: { text = "" }) {
} Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
struct SearchField: View { }
@Binding var text: String .buttonStyle(PlainButtonStyle())
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.secondary)
TextField("搜索应用", text: $text)
.textFieldStyle(PlainTextFieldStyle())
if !text.isEmpty {
Button(action: { text = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
} }
.buttonStyle(PlainButtonStyle())
} }
.padding(8)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
} }
.padding(8)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
} }
} }
#Preview {
let networkManager = NetworkManager()
return ContentView()
.environmentObject(networkManager)
.frame(width: 792, height: 600)
}

View File

@@ -55,13 +55,9 @@ class NewNetworkService {
let products = globalCcmResult.products let products = globalCcmResult.products
if products.isEmpty { if products.isEmpty { return ([], []) }
return ([], [])
}
let validProducts = products.filter { let validProducts = products.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
$0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform)
}
var uniqueProductsDict = [String: UniqueProduct]() var uniqueProductsDict = [String: UniqueProduct]()
for product in validProducts { for product in validProducts {

View File

@@ -49,16 +49,10 @@ class NetworkManager: ObservableObject {
func fetchProducts() async { func fetchProducts() async {
loadingState = .loading loadingState = .loading
do { do {
let (saps, sapCodes) = try await globalNetworkService.fetchProductsData() let (products, uniqueProducts) = 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 { await MainActor.run {
globalProducts = products
globalUniqueProducts = uniqueProducts.sorted { $0.displayName < $1.displayName }
self.loadingState = .success self.loadingState = .success
} }
} catch { } catch {
@@ -153,8 +147,10 @@ class NetworkManager: ObservableObject {
while retryCount < maxRetries { while retryCount < maxRetries {
do { do {
let (saps, sapCodes) = try await globalNetworkService.fetchProductsData() let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
await MainActor.run { await MainActor.run {
globalProducts = products
globalUniqueProducts = uniqueProducts
self.loadingState = .success self.loadingState = .success
self.isFetchingProducts = false self.isFetchingProducts = false
} }
@@ -290,18 +286,18 @@ class NetworkManager: ObservableObject {
return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid) return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid)
} }
func isVersionDownloaded(product: Product, version: String, language: String) -> URL? { func isVersionDownloaded(productId: String, version: String, language: String) -> URL? {
if let task = downloadTasks.first(where: { if let task = downloadTasks.first(where: {
$0.sapCode == sap.sapCode && $0.productId == productId &&
$0.version == version && $0.productVersion == version &&
$0.language == language && $0.language == language &&
!$0.status.isCompleted !$0.status.isCompleted
}) { return task.directory } }) { return task.directory }
let platform = sap.versions[version]?.apPlatform ?? "unknown" let platform = globalProducts.first(where: { $0.id == productId })?.platforms.first?.id ?? "unknown"
let fileName = sap.sapCode == "APRO" let fileName = productId == "APRO"
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg" ? "Adobe Downloader \(productId)_\(version)_\(platform).dmg"
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)" : "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)"
if useDefaultDirectory && !defaultDirectory.isEmpty { if useDefaultDirectory && !defaultDirectory.isEmpty {
let defaultPath = URL(fileURLWithPath: defaultDirectory) let defaultPath = URL(fileURLWithPath: defaultDirectory)
@@ -344,7 +340,7 @@ class NetworkManager: ObservableObject {
let savedTasks = await TaskPersistenceManager.shared.loadTasks() let savedTasks = await TaskPersistenceManager.shared.loadTasks()
await MainActor.run { await MainActor.run {
for task in savedTasks { for task in savedTasks {
for product in task.productsToDownload { for product in task.dependenciesToDownload {
product.updateCompletedPackages() product.updateCompletedPackages()
} }
} }
@@ -383,7 +379,7 @@ class NetworkManager: ObservableObject {
for task in downloadTasks { for task in downloadTasks {
if case .paused(let info) = task.status, if case .paused(let info) = task.status,
info.reason == .networkIssue { info.reason == .networkIssue {
await downloadUtils.resumeDownloadTask(taskId: task.id) await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
} }
} }
} }
@@ -391,7 +387,7 @@ class NetworkManager: ObservableObject {
private func pauseActiveTasks() async { private func pauseActiveTasks() async {
for task in downloadTasks { for task in downloadTasks {
if case .downloading = task.status { if case .downloading = task.status {
await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue) await globalNewDownloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
} }
} }
} }

View File

@@ -244,6 +244,9 @@ class NewDownloadUtils {
if condition.contains("[OSArchitecture]==\(AppStatics.architectureSymbol)") { if condition.contains("[OSArchitecture]==\(AppStatics.architectureSymbol)") {
shouldDownload = true shouldDownload = true
} }
if condition.contains("[OSArchitecture]==x64") {
shouldDownload = true
}
if condition.contains(installLanguage) || task.language == "ALL" { if condition.contains(installLanguage) || task.language == "ALL" {
shouldDownload = true shouldDownload = true
} }
@@ -311,7 +314,6 @@ class NewDownloadUtils {
if !FileManager.default.fileExists(atPath: driverPath.path) { if !FileManager.default.fileExists(atPath: driverPath.path) {
if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) { if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) {
let driverXml = generateDriverXML( let driverXml = generateDriverXML(
sapCode: task.productId,
version: task.productVersion, version: task.productVersion,
language: task.language, language: task.language,
productInfo: productInfo, productInfo: productInfo,
@@ -412,19 +414,19 @@ class NewDownloadUtils {
func handleError(_ taskId: UUID, _ error: Error) async { func handleError(_ taskId: UUID, _ error: Error) async {
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
guard task != nil else { return } guard let task = task else { return }
let (errorMessage, isRecoverable) = classifyError(error) let (errorMessage, isRecoverable) = classifyError(error)
if isRecoverable, if isRecoverable,
let downloadTask = await globalCancelTracker?.downloadTasks[taskId] { let downloadTask = await globalCancelTracker.downloadTasks[taskId] {
let resumeData = await withCheckedContinuation { continuation in let resumeData = await withCheckedContinuation { continuation in
downloadTask.cancel(byProducingResumeData: { data in downloadTask.cancel(byProducingResumeData: { data in
continuation.resume(returning: data) continuation.resume(returning: data)
}) })
} }
if let resumeData = resumeData { if let resumeData = resumeData {
await globalCancelTracker?.storeResumeData(taskId, data: resumeData) await globalCancelTracker.storeResumeData(taskId, data: resumeData)
} }
} }
@@ -441,7 +443,7 @@ class NewDownloadUtils {
Task { Task {
do { do {
try await Task.sleep(nanoseconds: NetworkConstants.retryDelay) try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
if await !(globalCancelTracker?.isCancelled(taskId) ?? false) { if await globalCancelTracker.isCancelled(taskId) == false {
await resumeDownloadTask(taskId: taskId) await resumeDownloadTask(taskId: taskId)
} }
} catch { } catch {
@@ -458,7 +460,7 @@ class NewDownloadUtils {
if let currentPackage = task.currentPackage { if let currentPackage = task.currentPackage {
let destinationDir = task.directory let destinationDir = task.directory
.appendingPathComponent("\(task.productId)") .appendingPathComponent("\(task.productId ?? "")")
let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName) let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
try? FileManager.default.removeItem(at: fileURL) try? FileManager.default.removeItem(at: fileURL)
} }
@@ -473,16 +475,18 @@ class NewDownloadUtils {
func resumeDownloadTask(taskId: UUID) async { func resumeDownloadTask(taskId: UUID) async {
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
guard task != nil else { return } guard let task = task else { return }
await MainActor.run { await MainActor.run {
let totalPackages = task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }
task.setStatus(.downloading(DownloadStatus.DownloadInfo( task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: task.currentPackage?.fullPackageName ?? "", fileName: task.currentPackage?.fullPackageName ?? "",
currentPackageIndex: 0, currentPackageIndex: 0,
totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }, totalPackages: totalPackages,
startTime: Date(), startTime: Date(),
estimatedTimeRemaining: nil estimatedTimeRemaining: nil
))) )))
task.objectWillChange.send()
} }
await globalNetworkManager.saveTask(task) await globalNetworkManager.saveTask(task)
@@ -491,7 +495,7 @@ class NewDownloadUtils {
} }
if task.productId == "APRO" { if task.productId == "APRO" {
if let resumeData = await globalCancelTracker?.getResumeData(taskId), if let resumeData = await globalCancelTracker.getResumeData(taskId),
let currentPackage = task.currentPackage, let currentPackage = task.currentPackage,
let product = task.dependenciesToDownload.first { let product = task.dependenciesToDownload.first {
try? await downloadPackage( try? await downloadPackage(
@@ -598,9 +602,9 @@ class NewDownloadUtils {
product.updateCompletedPackages() product.updateCompletedPackages()
} }
await globalNetworkManager.saveTask(task) await globalNetworkManager?.saveTask(task)
await MainActor.run { await MainActor.run {
globalNetworkManager.objectWillChange.send() globalNetworkManager?.objectWillChange.send()
} }
continuation.resume() continuation.resume()
} }
@@ -643,7 +647,7 @@ class NewDownloadUtils {
lastUpdateTime = now lastUpdateTime = now
lastBytes = totalBytesWritten lastBytes = totalBytesWritten
globalNetworkManager.objectWillChange.send() globalNetworkManager?.objectWillChange.send()
} }
} }
} }
@@ -664,27 +668,27 @@ class NewDownloadUtils {
return return
} }
await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session) await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
await globalCancelTracker?.clearResumeData(task.id) await globalCancelTracker.clearResumeData(task.id)
downloadTask.resume() downloadTask.resume()
} }
} }
} }
func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Product, displayName: String) -> String { func generateDriverXML(version: String, language: String, productInfo: Product, displayName: String) -> String {
// platform languageSet // platform languageSet
guard let platform = productInfo.platforms.first(where: { $0.id == "mac" }), guard let platform = globalProducts.first(where: { $0.id == productInfo.id })?.platforms.first?.id,
let languageSet = platform.languageSet.first else { let languageSet = globalProducts.first(where: { $0.id == productInfo.id })?.platforms.first?.languageSet else {
return "" return ""
} }
// //
let dependencies = languageSet.dependencies.map { dependency in let dependencies = languageSet.first?.dependencies.map { dependency in
""" """
<Dependency> <Dependency>
<SAPCode>\(dependency.sapCode)</SAPCode> <SAPCode>\(productInfo.id)</SAPCode>
<BaseVersion>\(dependency.baseVersion)</BaseVersion> <BaseVersion>\(languageSet.first?.baseVersion)</BaseVersion>
<EsdDirectory>\(dependency.sapCode)</EsdDirectory> <EsdDirectory>\(productInfo.id)</EsdDirectory>
</Dependency> </Dependency>
""" """
}.joined(separator: "\n") }.joined(separator: "\n")
@@ -693,10 +697,10 @@ class NewDownloadUtils {
<DriverInfo> <DriverInfo>
<ProductInfo> <ProductInfo>
<n>Adobe \(displayName)</n> <n>Adobe \(displayName)</n>
<SAPCode>\(sapCode)</SAPCode> <SAPCode>\(productInfo.id)</SAPCode>
<CodexVersion>\(version)</CodexVersion> <CodexVersion>\(version)</CodexVersion>
<Platform>mac</Platform> <Platform>mac</Platform>
<EsdDirectory>\(sapCode)</EsdDirectory> <EsdDirectory>\(productInfo.id)</EsdDirectory>
<Dependencies> <Dependencies>
\(dependencies) \(dependencies)
</Dependencies> </Dependencies>
@@ -811,11 +815,11 @@ class NewDownloadUtils {
task.objectWillChange.send() task.objectWillChange.send()
} }
await globalNetworkManager.saveTask(task) await globalNetworkManager?.saveTask(task)
await MainActor.run { await MainActor.run {
globalNetworkManager.updateDockBadge() globalNetworkManager?.updateDockBadge()
globalNetworkManager.objectWillChange.send() globalNetworkManager?.objectWillChange.send()
} }
continuation.resume() continuation.resume()
} }
@@ -842,10 +846,10 @@ class NewDownloadUtils {
lastBytes = totalBytesWritten lastBytes = totalBytesWritten
task.objectWillChange.send() task.objectWillChange.send()
globalNetworkManager.objectWillChange.send() globalNetworkManager?.objectWillChange.send()
Task { Task {
await globalNetworkManager.saveTask(task) await globalNetworkManager?.saveTask(task)
} }
} }
} }
@@ -860,20 +864,19 @@ class NewDownloadUtils {
let downloadTask = session.downloadTask(with: downloadRequest) let downloadTask = session.downloadTask(with: downloadRequest)
Task { Task {
await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session) await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
if await (globalCancelTracker?.isCancelled(task.id) ?? false) { if await globalCancelTracker.isCancelled(task.id) {
continuation.resume(throwing: NetworkError.cancelled) continuation.resume(throwing: NetworkError.cancelled)
return return
} }
downloadTask.resume() downloadTask.resume()
} }
} }
} }
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async { func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
let task = await globalCancelTracker?.downloadTasks[taskId] let task = await globalCancelTracker.downloadTasks[taskId]
if let downloadTask = task { if let downloadTask = task {
let data = await withCheckedContinuation { continuation in let data = await withCheckedContinuation { continuation in
downloadTask.cancel(byProducingResumeData: { data in downloadTask.cancel(byProducingResumeData: { data in
@@ -881,7 +884,7 @@ class NewDownloadUtils {
}) })
} }
if let data = data { if let data = data {
await globalCancelTracker?.storeResumeData(taskId, data: data) await globalCancelTracker.storeResumeData(taskId, data: data)
} }
} }
@@ -944,4 +947,223 @@ class NewDownloadUtils {
return false return false
} }
} }
private func executePrivilegedCommand(_ command: String) async -> String {
return await withCheckedContinuation { continuation in
PrivilegedHelperManager.shared.executeCommand(command) { result in
if result.starts(with: "Error:") {
print("命令执行失败: \(command)")
print("错误信息: \(result)")
}
continuation.resume(returning: result)
}
}
}
func downloadX1a0HeCCPackages(
progressHandler: @escaping (Double, String) -> Void,
cancellationHandler: @escaping () -> Bool,
shouldProcess: Bool = true
) async throws {
let baseUrl = "https://cdn-ffc.oobesaas.adobe.com/core/v1/applications?name=CreativeCloud&platform=\(AppStatics.isAppleSilicon ? "macarm64" : "osx10")"
let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 300
configuration.httpAdditionalHeaders = NetworkConstants.downloadHeaders
let session = URLSession(configuration: configuration)
do {
var request = URLRequest(url: URL(string: baseUrl)!)
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
let xmlDoc = try XMLDocument(data: data)
let packageSets = try xmlDoc.nodes(forXPath: "//packageSet[name='ADC']")
guard let adcPackageSet = packageSets.first else {
throw NetworkError.invalidData("找不到ADC包集")
}
let targetPackages = ["HDBox", "IPCBox"]
var packagesToDownload: [(name: String, url: URL, size: Int64)] = []
for packageName in targetPackages {
let packageNodes = try adcPackageSet.nodes(forXPath: ".//package[name='\(packageName)']")
guard let package = packageNodes.first else {
print("未找到包: \(packageName)")
continue
}
guard let manifestUrl = try package.nodes(forXPath: ".//manifestUrl").first?.stringValue,
let cdnBase = try xmlDoc.nodes(forXPath: "//cdn/secure").first?.stringValue else {
print("无法获取manifest URL或CDN基础URL")
continue
}
let manifestFullUrl = cdnBase + manifestUrl
var manifestRequest = URLRequest(url: URL(string: manifestFullUrl)!)
NetworkConstants.downloadHeaders.forEach { manifestRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
let (manifestData, manifestResponse) = try await session.data(for: manifestRequest)
guard let manifestHttpResponse = manifestResponse as? HTTPURLResponse,
(200...299).contains(manifestHttpResponse.statusCode) else {
print("获取manifest失败: HTTP \(String(describing: (manifestResponse as? HTTPURLResponse)?.statusCode))")
continue
}
let manifestDoc = try XMLDocument(data: manifestData)
let assetPathNodes = try manifestDoc.nodes(forXPath: "//asset_path")
let sizeNodes = try manifestDoc.nodes(forXPath: "//asset_size")
guard let assetPath = assetPathNodes.first?.stringValue,
let sizeStr = sizeNodes.first?.stringValue,
let size = Int64(sizeStr),
let downloadUrl = URL(string: assetPath) else {
continue
}
packagesToDownload.append((packageName, downloadUrl, size))
}
guard !packagesToDownload.isEmpty else {
throw NetworkError.invalidData("没有找到可下载的包")
}
let totalCount = packagesToDownload.count
for (index, package) in packagesToDownload.enumerated() {
if cancellationHandler() {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.cancelled
}
await MainActor.run {
progressHandler(Double(index) / Double(totalCount), "正在下载 \(package.name)...")
}
let destinationURL = tempDirectory.appendingPathComponent("\(package.name).zip")
var downloadRequest = URLRequest(url: package.url)
print(downloadRequest)
NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
let (downloadURL, downloadResponse) = try await session.download(for: downloadRequest)
guard let httpResponse = downloadResponse as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("下载失败: HTTP \(String(describing: (downloadResponse as? HTTPURLResponse)?.statusCode))")
continue
}
try FileManager.default.moveItem(at: downloadURL, to: destinationURL)
}
await MainActor.run {
progressHandler(0.9, shouldProcess ? "正在安装组件..." : "正在完成下载...")
}
let targetDirectory = "/Library/Application\\ Support/Adobe/Adobe\\ Desktop\\ Common"
let rawTargetDirectory = "/Library/Application Support/Adobe/Adobe Desktop Common"
if !FileManager.default.fileExists(atPath: rawTargetDirectory) {
let createDirResult = await executePrivilegedCommand("/bin/mkdir -p \(targetDirectory)")
if createDirResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("创建目录失败: \(createDirResult)")
}
let chmodResult = await executePrivilegedCommand("/bin/chmod 755 \(targetDirectory)")
if chmodResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("设置权限失败: \(chmodResult)")
}
}
for package in packagesToDownload {
let packageDir = "\(targetDirectory)/\(package.name)"
let removeResult = await executePrivilegedCommand("/bin/rm -rf \(packageDir)")
if removeResult.starts(with: "Error:") {
print("移除旧目录失败: \(removeResult)")
}
let mkdirResult = await executePrivilegedCommand("/bin/mkdir -p \(packageDir)")
if mkdirResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("创建 \(package.name) 目录失败")
}
let unzipResult = await executePrivilegedCommand("cd \(packageDir) && /usr/bin/unzip -o '\(tempDirectory.path)/\(package.name).zip'")
if unzipResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("解压 \(package.name) 失败: \(unzipResult)")
}
let chmodResult = await executePrivilegedCommand("/bin/chmod -R 755 \(packageDir)")
if chmodResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("设置 \(package.name) 权限失败: \(chmodResult)")
}
let chownResult = await executePrivilegedCommand("/usr/sbin/chown -R root:wheel \(packageDir)")
if chownResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("设置 \(package.name) 所有者失败: \(chownResult)")
}
}
try await Task.sleep(nanoseconds: 1_000_000_000)
if shouldProcess {
try await withCheckedThrowingContinuation { continuation in
ModifySetup.backupAndModifySetupFile { success, message in
if success {
continuation.resume()
} else {
continuation.resume(throwing: NetworkError.installError(message))
}
}
}
ModifySetup.clearVersionCache()
}
try? FileManager.default.removeItem(at: tempDirectory)
await MainActor.run {
progressHandler(1.0, shouldProcess ? "安装完成" : "下载完成")
}
} catch {
print("发生错误: \(error.localizedDescription)")
throw error
}
}
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
await globalCancelTracker.cancel(taskId)
if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) {
if removeFiles {
try? FileManager.default.removeItem(at: task.directory)
}
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: String(localized: "下载已取消"),
error: NetworkError.downloadCancelled,
timestamp: Date(),
recoverable: false
)))
await globalNetworkManager.saveTask(task)
await MainActor.run {
globalNetworkManager.updateDockBadge()
globalNetworkManager.objectWillChange.send()
}
}
}
} }

View File

@@ -18,15 +18,15 @@ class TaskPersistenceManager {
self.cancelTracker = tracker self.cancelTracker = tracker
} }
private func getTaskFileName(sapCode: String, version: String, language: String, platform: String) -> String { private func getTaskFileName(productId: String, version: String, language: String, platform: String) -> String {
return sapCode == "APRO" return productId == "APRO"
? "Adobe Downloader \(sapCode)_\(version)_\(platform)-task.json" ? "Adobe Downloader \(productId)_\(version)_\(platform)-task.json"
: "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json" : "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)-task.json"
} }
func saveTask(_ task: NewDownloadTask) async { func saveTask(_ task: NewDownloadTask) async {
let fileName = getTaskFileName( let fileName = getTaskFileName(
sapCode: task.productId, productId: task.productId,
version: task.productVersion, version: task.productVersion,
language: task.language, language: task.language,
platform: task.platform platform: task.platform
@@ -209,7 +209,7 @@ class TaskPersistenceManager {
func removeTask(_ task: NewDownloadTask) { func removeTask(_ task: NewDownloadTask) {
let fileName = getTaskFileName( let fileName = getTaskFileName(
sapCode: task.productId, productId: task.productId,
version: task.productVersion, version: task.productVersion,
language: task.language, language: task.language,
platform: task.platform platform: task.platform
@@ -220,16 +220,16 @@ class TaskPersistenceManager {
try? fileManager.removeItem(at: fileURL) try? fileManager.removeItem(at: fileURL)
} }
func createExistingProgramTask(sapCode: String, version: String, language: String, displayName: String, platform: String, directory: URL) async { func createExistingProgramTask(productId: String, version: String, language: String, displayName: String, platform: String, directory: URL) async {
let fileName = getTaskFileName( let fileName = getTaskFileName(
sapCode: sapCode, productId: productId,
version: version, version: version,
language: language, language: language,
platform: platform platform: platform
) )
let product = DependenciesToDownload( let product = DependenciesToDownload(
sapCode: sapCode, sapCode: productId,
version: version, version: version,
buildGuid: "", buildGuid: "",
applicationJson: "" applicationJson: ""
@@ -249,7 +249,7 @@ class TaskPersistenceManager {
product.packages = [package] product.packages = [package]
let task = NewDownloadTask( let task = NewDownloadTask(
productId: sapCode, productId: productId,
productVersion: version, productVersion: version,
language: language, language: language,
displayName: displayName, displayName: displayName,

View File

@@ -277,17 +277,19 @@ struct GeneralSettingsView: View {
} }
var body: some View { var body: some View {
Form { ScrollView {
DownloadSettingsView(viewModel: viewModel) VStack(spacing: 16) {
HelperSettingsView(viewModel: viewModel, DownloadSettingsView(viewModel: viewModel)
showHelperAlert: $showHelperAlert, HelperSettingsView(viewModel: viewModel,
helperAlertMessage: $helperAlertMessage, showHelperAlert: $showHelperAlert,
helperAlertSuccess: $helperAlertSuccess) helperAlertMessage: $helperAlertMessage,
CCSettingsView(viewModel: viewModel) helperAlertSuccess: $helperAlertSuccess)
UpdateSettingsView(viewModel: viewModel) CCSettingsView(viewModel: viewModel)
CleanConfigView() UpdateSettingsView(viewModel: viewModel)
CleanConfigView()
}
.padding()
} }
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.alert(helperAlertSuccess ? "操作成功" : "操作失败", isPresented: $showHelperAlert) { .alert(helperAlertSuccess ? "操作成功" : "操作失败", isPresented: $showHelperAlert) {
Button("确定") { } Button("确定") { }
@@ -298,28 +300,7 @@ struct GeneralSettingsView: View {
Button("取消", role: .cancel) { } Button("取消", role: .cancel) { }
Button("下载") { Button("下载") {
Task { Task {
viewModel.isDownloadingSetup = true await downloadSetup(shouldProcess: false)
viewModel.isCancelled = false
do {
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
progressHandler: { progress, status in
viewModel.setupDownloadProgress = progress
viewModel.setupDownloadStatus = status
},
cancellationHandler: { viewModel.isCancelled }
)
viewModel.setupVersion = ModifySetup.checkComponentVersion()
viewModel.isSuccess = true
viewModel.alertMessage = String(localized: "Setup 组件安装成功")
} catch NetworkError.cancelled {
viewModel.isSuccess = false
viewModel.alertMessage = String(localized: "下载已取消")
} catch {
viewModel.isSuccess = false
viewModel.alertMessage = error.localizedDescription
}
viewModel.showAlert = true
viewModel.isDownloadingSetup = false
} }
} }
} message: { } message: {
@@ -329,29 +310,7 @@ struct GeneralSettingsView: View {
Button("取消", role: .cancel) { } Button("取消", role: .cancel) { }
Button("确定") { Button("确定") {
Task { Task {
viewModel.isDownloadingSetup = true await downloadSetup(shouldProcess: true)
viewModel.isCancelled = false
do {
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
progressHandler: { progress, status in
viewModel.setupDownloadProgress = progress
viewModel.setupDownloadStatus = status
},
cancellationHandler: { viewModel.isCancelled },
shouldProcess: true
)
viewModel.setupVersion = ModifySetup.checkComponentVersion()
viewModel.isSuccess = true
viewModel.alertMessage = String(localized: "X1a0He CC 下载并处理成功")
} catch NetworkError.cancelled {
viewModel.isSuccess = false
viewModel.alertMessage = String(localized: "下载已取消")
} catch {
viewModel.isSuccess = false
viewModel.alertMessage = error.localizedDescription
}
viewModel.showAlert = true
viewModel.isDownloadingSetup = false
} }
} }
} message: { } message: {
@@ -361,29 +320,7 @@ struct GeneralSettingsView: View {
Button("取消", role: .cancel) { } Button("取消", role: .cancel) { }
Button("确定") { Button("确定") {
Task { Task {
viewModel.isDownloadingSetup = true await downloadSetup(shouldProcess: false)
viewModel.isCancelled = false
do {
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
progressHandler: { progress, status in
viewModel.setupDownloadProgress = progress
viewModel.setupDownloadStatus = status
},
cancellationHandler: { viewModel.isCancelled },
shouldProcess: false
)
viewModel.setupVersion = ModifySetup.checkComponentVersion()
viewModel.isSuccess = true
viewModel.alertMessage = String(localized: "X1a0He CC 下载成功")
} catch NetworkError.cancelled {
viewModel.isSuccess = false
viewModel.alertMessage = String(localized: "下载已取消")
} catch {
viewModel.isSuccess = false
viewModel.alertMessage = error.localizedDescription
}
viewModel.showAlert = true
viewModel.isDownloadingSetup = false
} }
} }
} message: { } message: {
@@ -401,6 +338,34 @@ struct GeneralSettingsView: View {
viewModel.objectWillChange.send() viewModel.objectWillChange.send()
} }
} }
private func downloadSetup(shouldProcess: Bool) async {
viewModel.isDownloadingSetup = true
viewModel.isCancelled = false
do {
try await globalNewDownloadUtils.downloadX1a0HeCCPackages(
progressHandler: { progress, status in
viewModel.setupDownloadProgress = progress
viewModel.setupDownloadStatus = status
},
cancellationHandler: { viewModel.isCancelled },
shouldProcess: shouldProcess
)
viewModel.setupVersion = ModifySetup.checkComponentVersion()
viewModel.isSuccess = true
viewModel.alertMessage = shouldProcess ?
String(localized: "X1a0He CC 下载并处理成功") :
String(localized: "X1a0He CC 下载成功")
} catch NetworkError.cancelled {
viewModel.isSuccess = false
viewModel.alertMessage = String(localized: "下载已取消")
} catch {
viewModel.isSuccess = false
viewModel.alertMessage = error.localizedDescription
}
viewModel.showAlert = true
viewModel.isDownloadingSetup = false
}
} }
struct DownloadSettingsView: View { struct DownloadSettingsView: View {

View File

@@ -51,8 +51,7 @@ final class AppCardViewModel: ObservableObject {
@Published var pendingLanguage = "" @Published var pendingLanguage = ""
@Published var showRedownloadConfirm = false @Published var showRedownloadConfirm = false
let sap: Sap let uniqueProduct: UniqueProduct
weak var networkManager: NetworkManager?
@Published var isDownloading = false @Published var isDownloading = false
private let userDefaults = UserDefaults.standard private let userDefaults = UserDefaults.standard
@@ -67,10 +66,9 @@ final class AppCardViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(sap: Sap, networkManager: NetworkManager?) { init(uniqueProduct: UniqueProduct) {
self.sap = sap self.uniqueProduct = uniqueProduct
self.networkManager = networkManager
Task { @MainActor in Task { @MainActor in
setupObservers() setupObservers()
} }
@@ -78,12 +76,12 @@ final class AppCardViewModel: ObservableObject {
@MainActor @MainActor
private func setupObservers() { private func setupObservers() {
networkManager?.$downloadTasks globalNetworkManager.$downloadTasks
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.sink { [weak self] tasks in .sink { [weak self] tasks in
guard let self = self else { return } guard let self = self else { return }
let hasActiveTask = tasks.contains { let hasActiveTask = tasks.contains {
$0.sapCode == self.sap.sapCode && self.isTaskActive($0.status) $0.productId == self.uniqueProduct.id && self.isTaskActive($0.status)
} }
if hasActiveTask != self.isDownloading { if hasActiveTask != self.isDownloading {
@@ -93,7 +91,7 @@ final class AppCardViewModel: ObservableObject {
} }
.store(in: &cancellables) .store(in: &cancellables)
networkManager?.objectWillChange globalNetworkManager.objectWillChange
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.sink { [weak self] _ in .sink { [weak self] _ in
self?.updateDownloadingStatus() self?.updateDownloadingStatus()
@@ -114,13 +112,8 @@ final class AppCardViewModel: ObservableObject {
@MainActor @MainActor
func updateDownloadingStatus() { func updateDownloadingStatus() {
guard let networkManager = networkManager else { let hasActiveTask = globalNetworkManager.downloadTasks.contains {
self.isDownloading = false $0.productId == uniqueProduct.id && isTaskActive($0.status)
return
}
let hasActiveTask = networkManager.downloadTasks.contains {
$0.sapCode == sap.sapCode && isTaskActive($0.status)
} }
if hasActiveTask != self.isDownloading { if hasActiveTask != self.isDownloading {
@@ -130,10 +123,10 @@ final class AppCardViewModel: ObservableObject {
} }
func getDestinationURL(version: String, language: String) async throws -> URL { func getDestinationURL(version: String, language: String) async throws -> URL {
let platform = sap.versions[version]?.apPlatform ?? "unknown" let platform = globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.id
let installerName = sap.sapCode == "APRO" let installerName = uniqueProduct.id == "APRO"
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg" ? "Adobe Downloader \(uniqueProduct.id)_\(version)_\(platform).dmg"
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)" : "Adobe Downloader \(uniqueProduct.id)_\(version)-\(language)-\(platform)"
if useDefaultDirectory && !defaultDirectory.isEmpty { if useDefaultDirectory && !defaultDirectory.isEmpty {
return URL(fileURLWithPath: defaultDirectory) return URL(fileURLWithPath: defaultDirectory)
@@ -167,10 +160,9 @@ final class AppCardViewModel: ObservableObject {
} }
func loadIcon() { func loadIcon() {
if let bestIcon = sap.getBestIcon(), if let bestIcon = globalProducts.first(where: { $0.id == uniqueProduct.id })?.getBestIcon(),
let iconURL = URL(string: bestIcon.url) { let iconURL = URL(string: bestIcon.value) {
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) {
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
self.iconImage = cachedImage self.iconImage = cachedImage
return return
} }
@@ -189,20 +181,20 @@ final class AppCardViewModel: ObservableObject {
await MainActor.run { await MainActor.run {
if let image = NSImage(data: data) { if let image = NSImage(data: data) {
IconCache.shared.setIcon(image, for: bestIcon.url) IconCache.shared.setIcon(image, for: bestIcon.value)
self.iconImage = image self.iconImage = image
} }
} }
} catch { } catch {
await MainActor.run { await MainActor.run {
if let localImage = NSImage(named: sap.sapCode) { if let localImage = NSImage(named: uniqueProduct.id) {
self.iconImage = localImage self.iconImage = localImage
} }
} }
} }
} }
} else { } else {
if let localImage = NSImage(named: sap.sapCode) { if let localImage = NSImage(named: uniqueProduct.id) {
self.iconImage = localImage self.iconImage = localImage
} }
} }
@@ -222,37 +214,32 @@ final class AppCardViewModel: ObservableObject {
} }
func checkAndStartDownload(version: String, language: String) async { func checkAndStartDownload(version: String, language: String) async {
if let networkManager = networkManager { if let existingPath = globalNetworkManager.isVersionDownloaded(productId: uniqueProduct.id, version: version, language: language) {
if let existingPath = networkManager.isVersionDownloaded(sap: sap, version: version, language: language) { await MainActor.run {
await MainActor.run { existingFilePath = existingPath
existingFilePath = existingPath pendingVersion = version
pendingVersion = version pendingLanguage = language
pendingLanguage = language showExistingFileAlert = true
showExistingFileAlert = true }
} } else {
} else { do {
do { let destinationURL = try await getDestinationURL(version: version, language: language)
let destinationURL = try await getDestinationURL(version: version, language: language) try await globalNetworkManager.startDownload(
try await networkManager.startDownload( productId: uniqueProduct.id,
sap: sap, selectedVersion: version,
selectedVersion: version, language: language,
language: language, destinationURL: destinationURL
destinationURL: destinationURL )
) } catch {
} catch { handleError(error)
handleError(error)
}
} }
} }
} }
func createCompletedTask(_ path: URL) async { func createCompletedTask(_ path: URL) async {
guard let networkManager = networkManager, let existingTask = globalNetworkManager.downloadTasks.first { task in
let productInfo = sap.versions[pendingVersion] else { return } return task.productId == uniqueProduct.id &&
task.productVersion == pendingVersion &&
let existingTask = networkManager.downloadTasks.first { task in
return task.sapCode == sap.sapCode &&
task.version == pendingVersion &&
task.language == pendingLanguage && task.language == pendingLanguage &&
task.directory == path task.directory == path
} }
@@ -262,27 +249,24 @@ final class AppCardViewModel: ObservableObject {
} }
await TaskPersistenceManager.shared.createExistingProgramTask( await TaskPersistenceManager.shared.createExistingProgramTask(
sapCode: sap.sapCode, productId: uniqueProduct.id,
version: pendingVersion, version: pendingVersion,
language: pendingLanguage, language: pendingLanguage,
displayName: sap.displayName, displayName: uniqueProduct.displayName,
platform: productInfo.apPlatform, platform: globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.id ?? "unknown",
directory: path directory: path
) )
let savedTasks = await TaskPersistenceManager.shared.loadTasks() let savedTasks = await TaskPersistenceManager.shared.loadTasks()
await MainActor.run { await MainActor.run {
networkManager.downloadTasks = savedTasks globalNetworkManager.downloadTasks = savedTasks
networkManager.updateDockBadge() globalNetworkManager.updateDockBadge()
networkManager.objectWillChange.send() globalNetworkManager.objectWillChange.send()
} }
} }
var dependenciesCount: Int { var dependenciesCount: Int {
if let firstVersion = sap.versions.first?.value { return globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.languageSet.first?.dependencies.count ?? 0
return firstVersion.dependencies.count
}
return 0
} }
var hasValidIcon: Bool { var hasValidIcon: Bool {
@@ -304,12 +288,11 @@ final class AppCardViewModel: ObservableObject {
struct AppCardView: View { struct AppCardView: View {
@StateObject private var viewModel: AppCardViewModel @StateObject private var viewModel: AppCardViewModel
@EnvironmentObject private var networkManager: NetworkManager
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
@StorageValue(\.defaultLanguage) private var defaultLanguage @StorageValue(\.defaultLanguage) private var defaultLanguage
init(sap: Sap) { init(uniqueProduct: UniqueProduct) {
_viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil)) _viewModel = StateObject(wrappedValue: AppCardViewModel(uniqueProduct: uniqueProduct))
} }
var body: some View { var body: some View {
@@ -322,18 +305,19 @@ struct AppCardView: View {
} }
} }
.modifier(CardModifier()) .modifier(CardModifier())
.modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager)) .modifier(SheetModifier(viewModel: viewModel))
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true)) .modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
.onAppear(perform: setupViewModel) .onAppear(perform: setupViewModel)
.onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus) .onChange(of: globalNetworkManager.downloadTasks.count) { _ in
updateDownloadStatus()
}
} }
private func setupViewModel() { private func setupViewModel() {
viewModel.networkManager = networkManager
viewModel.updateDownloadingStatus() viewModel.updateDownloadingStatus()
} }
private func updateDownloadStatus(_ _: [NewDownloadTask]) { private func updateDownloadStatus() {
viewModel.updateDownloadingStatus() viewModel.updateDownloadingStatus()
} }
} }
@@ -379,16 +363,19 @@ private struct ProductInfoView: View {
var body: some View { var body: some View {
VStack { VStack {
Text(viewModel.sap.displayName) Text(viewModel.uniqueProduct.displayName)
.font(.system(size: AppCardConstants.titleFontSize)) .font(.system(size: AppCardConstants.titleFontSize))
.fontWeight(.bold) .fontWeight(.bold)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
HStack(spacing: 4) { HStack(spacing: 4) {
Text("可用版本: \(viewModel.sap.versions.count)") let product = findProduct(id: viewModel.uniqueProduct.id)
let versions = Set(product?.platforms.first?.languageSet.map { $0.productVersion } ?? [])
let dependenciesCount = product?.platforms.first?.languageSet.first?.dependencies.count ?? 0
Text("可用版本: \(versions.count)")
Text("|") Text("|")
Text("依赖包: \(viewModel.dependenciesCount)") Text("依赖包: \(dependenciesCount)")
} }
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -437,23 +424,23 @@ private struct CardModifier: ViewModifier {
private struct SheetModifier: ViewModifier { private struct SheetModifier: ViewModifier {
@ObservedObject var viewModel: AppCardViewModel @ObservedObject var viewModel: AppCardViewModel
let networkManager: NetworkManager
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage @StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
@StorageValue(\.defaultLanguage) private var defaultLanguage @StorageValue(\.defaultLanguage) private var defaultLanguage
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.sheet(isPresented: $viewModel.showVersionPicker) { .sheet(isPresented: $viewModel.showVersionPicker) {
VersionPickerView(sap: viewModel.sap) { version in if let product = findProduct(id: viewModel.uniqueProduct.id) {
Task { VersionPickerView(product: product) { version in
await viewModel.handleDownloadRequest( Task {
version, await viewModel.handleDownloadRequest(
useDefaultLanguage: useDefaultLanguage, version,
defaultLanguage: defaultLanguage useDefaultLanguage: useDefaultLanguage,
) defaultLanguage: defaultLanguage
)
}
} }
} }
.environmentObject(networkManager)
} }
.sheet(isPresented: $viewModel.showLanguagePicker) { .sheet(isPresented: $viewModel.showLanguagePicker) {
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
@@ -482,10 +469,9 @@ struct AlertModifier: ViewModifier {
viewModel.showExistingFileAlert = false viewModel.showExistingFileAlert = false
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty { if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
Task { Task {
if let networkManager = viewModel.networkManager, if !globalNetworkManager.downloadTasks.contains(where: { task in
!networkManager.downloadTasks.contains(where: { task in task.productId == viewModel.uniqueProduct.id &&
task.sapCode == viewModel.sap.sapCode && task.productVersion == viewModel.pendingVersion &&
task.version == viewModel.pendingVersion &&
task.language == viewModel.pendingLanguage task.language == viewModel.pendingLanguage
}) { }) {
await viewModel.createCompletedTask(path) await viewModel.createCompletedTask(path)
@@ -540,12 +526,10 @@ struct AlertModifier: ViewModifier {
} }
private func startRedownload() async { private func startRedownload() async {
guard let networkManager = viewModel.networkManager else { return }
do { do {
networkManager.downloadTasks.removeAll { task in globalNetworkManager.downloadTasks.removeAll { task in
task.sapCode == viewModel.sap.sapCode && task.productId == viewModel.uniqueProduct.id &&
task.version == viewModel.pendingVersion && task.productVersion == viewModel.pendingVersion &&
task.language == viewModel.pendingLanguage task.language == viewModel.pendingLanguage
} }
@@ -558,8 +542,8 @@ struct AlertModifier: ViewModifier {
language: viewModel.pendingLanguage language: viewModel.pendingLanguage
) )
try await networkManager.startDownload( try await globalNetworkManager.startDownload(
sap: viewModel.sap, productId: viewModel.uniqueProduct.id,
selectedVersion: viewModel.pendingVersion, selectedVersion: viewModel.pendingVersion,
language: viewModel.pendingLanguage, language: viewModel.pendingLanguage,
destinationURL: destinationURL destinationURL: destinationURL

View File

@@ -7,7 +7,6 @@
import SwiftUI import SwiftUI
struct DownloadManagerView: View { struct DownloadManagerView: View {
@EnvironmentObject private var networkManager: NetworkManager
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var sortOrder: SortOrder = .addTime @State private var sortOrder: SortOrder = .addTime
@@ -27,7 +26,7 @@ struct DownloadManagerView: View {
} }
private func removeTask(_ task: NewDownloadTask) { private func removeTask(_ task: NewDownloadTask) {
networkManager.removeTask(taskId: task.id) globalNetworkManager.removeTask(taskId: task.id)
} }
private func sortTasks(_ tasks: [NewDownloadTask]) -> [NewDownloadTask] { private func sortTasks(_ tasks: [NewDownloadTask]) -> [NewDownloadTask] {
@@ -47,123 +46,163 @@ struct DownloadManagerView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { DownloadManagerToolbar(
Text("下载管理") sortOrder: $sortOrder,
.font(.headline) dismiss: dismiss
Spacer() )
HStack(){ DownloadTaskList(
Menu { tasks: sortTasks(globalNetworkManager.downloadTasks),
ForEach([SortOrder.addTime, .name, .status], id: \.self) { order in removeTask: removeTask
Button(action: { )
sortOrder = order
}) {
HStack {
Text(order.description)
if sortOrder == order {
Image(systemName: "checkmark")
}
}
}
}
} label: {
HStack {
Image(systemName: "arrow.up.arrow.down")
Text(sortOrder.description)
.font(.caption)
}
}
}
.frame(minWidth: 120)
.fixedSize()
Button("全部暂停") {
Task {
for task in networkManager.downloadTasks {
if case .downloading = task.status {
await networkManager.downloadUtils.pauseDownloadTask(
taskId: task.id,
reason: .userRequested
)
}
}
}
}
Button("全部继续") {
Task {
for task in networkManager.downloadTasks {
if case .paused = task.status {
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
}
}
}
}
Button("清理已完成") {
networkManager.downloadTasks.removeAll { task in
if case .completed = task.status {
return true
}
return false
}
networkManager.updateDockBadge()
}
Button("关闭") {
dismiss()
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
.padding(.horizontal)
.padding(.vertical, 8)
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 8) {
ForEach(sortTasks(networkManager.downloadTasks)) { task in
DownloadProgressView(
task: task,
onCancel: {
Task {
await networkManager.downloadUtils.cancelDownloadTask(taskId: task.id)
}
},
onPause: {
Task {
await networkManager.downloadUtils.pauseDownloadTask(
taskId: task.id,
reason: .userRequested
)
}
},
onResume: {
Task {
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
}
},
onRetry: {
Task {
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
}
},
onRemove: {
removeTask(task)
}
)
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
.background(Color(NSColor.windowBackgroundColor))
} }
.frame(width:800, height: 600) .frame(width:800, height: 600)
} }
} }
private struct DownloadManagerToolbar: View {
@Binding var sortOrder: DownloadManagerView.SortOrder
let dismiss: DismissAction
var body: some View {
HStack {
Text("下载管理")
.font(.headline)
Spacer()
SortMenuView(sortOrder: $sortOrder)
.frame(minWidth: 120)
.fixedSize()
ToolbarButtons(dismiss: dismiss)
}
.padding(.horizontal)
.padding(.vertical, 8)
}
}
private struct ToolbarButtons: View {
let dismiss: DismissAction
var body: some View {
Group {
Button("全部暂停") {
Task {
for task in globalNetworkManager.downloadTasks {
if case .downloading = task.status {
await globalNewDownloadUtils.pauseDownloadTask(
taskId: task.id,
reason: .userRequested
)
}
}
}
}
Button("全部继续") {
Task {
for task in globalNetworkManager.downloadTasks {
if case .paused = task.status {
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
}
}
}
}
Button("清理已完成") {
globalNetworkManager.downloadTasks.removeAll { task in
if case .completed = task.status {
return true
}
return false
}
globalNetworkManager.updateDockBadge()
}
Button("关闭") {
dismiss()
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
}
}
private struct DownloadTaskList: View {
let tasks: [NewDownloadTask]
let removeTask: (NewDownloadTask) -> Void
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 8) {
ForEach(tasks) { task in
DownloadProgressView(
task: task,
onCancel: { TaskOperations.cancelTask(task) },
onPause: { TaskOperations.pauseTask(task) },
onResume: { TaskOperations.resumeTask(task) },
onRetry: { TaskOperations.resumeTask(task) },
onRemove: { removeTask(task) }
)
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
.background(Color(NSColor.windowBackgroundColor))
}
}
private enum TaskOperations {
static func cancelTask(_ task: NewDownloadTask) {
Task {
await globalNewDownloadUtils.cancelDownloadTask(taskId: task.id)
}
}
static func pauseTask(_ task: NewDownloadTask) {
Task {
await globalNewDownloadUtils.pauseDownloadTask(
taskId: task.id,
reason: .userRequested
)
}
}
static func resumeTask(_ task: NewDownloadTask) {
Task {
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
}
}
}
extension DownloadManagerView.SortOrder: Hashable {} extension DownloadManagerView.SortOrder: Hashable {}
struct SortMenuView: View {
@Binding var sortOrder: DownloadManagerView.SortOrder
var body: some View {
Menu {
ForEach([DownloadManagerView.SortOrder.addTime, .name, .status], id: \.self) { order in
Button(action: {
sortOrder = order
}) {
HStack {
Text(order.description)
if sortOrder == order {
Image(systemName: "checkmark")
}
}
}
}
} label: {
HStack {
Image(systemName: "arrow.up.arrow.down")
Text(sortOrder.description)
.font(.caption)
}
}
}
}
#Preview { #Preview {
DownloadManagerView() DownloadManagerView()
.environmentObject(NetworkManager())
} }

View File

@@ -6,7 +6,6 @@
import SwiftUI import SwiftUI
struct DownloadProgressView: View { struct DownloadProgressView: View {
@EnvironmentObject private var networkManager: NetworkManager
@ObservedObject var task: NewDownloadTask @ObservedObject var task: NewDownloadTask
let onCancel: () -> Void let onCancel: () -> Void
let onPause: () -> Void let onPause: () -> Void
@@ -26,7 +25,7 @@ struct DownloadProgressView: View {
private var statusLabel: some View { private var statusLabel: some View {
Text(task.status.description) Text(task.status.description)
.font(.caption) .font(.caption)
.foregroundColor(statusColor) .foregroundColor(.white)
.padding(.vertical, 2) .padding(.vertical, 2)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.background(statusBackgroundColor) .background(statusBackgroundColor)
@@ -131,7 +130,7 @@ struct DownloadProgressView: View {
showInstallPrompt = false showInstallPrompt = false
isInstalling = true isInstalling = true
Task { Task {
await networkManager.installProduct(at: task.directory) await globalNetworkManager.installProduct(at: task.directory)
} }
} catch { } catch {
showSetupProcessAlert = true showSetupProcessAlert = true
@@ -145,7 +144,7 @@ struct DownloadProgressView: View {
showInstallPrompt = false showInstallPrompt = false
isInstalling = true isInstalling = true
Task { Task {
await networkManager.installProduct(at: task.directory) await globalNetworkManager.installProduct(at: task.directory)
} }
} catch { } catch {
showSetupProcessAlert = true showSetupProcessAlert = true
@@ -205,7 +204,7 @@ struct DownloadProgressView: View {
showInstallPrompt = false showInstallPrompt = false
isInstalling = true isInstalling = true
Task { Task {
await networkManager.installProduct(at: task.directory) await globalNetworkManager.installProduct(at: task.directory)
} }
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
@@ -217,18 +216,18 @@ struct DownloadProgressView: View {
} }
.sheet(isPresented: $isInstalling) { .sheet(isPresented: $isInstalling) {
Group { Group {
if case .installing(let progress, let status) = networkManager.installationState { if case .installing(let progress, let status) = globalNetworkManager.installationState {
InstallProgressView( InstallProgressView(
productName: task.displayName, productName: task.displayName,
progress: progress, progress: progress,
status: status, status: status,
onCancel: { onCancel: {
networkManager.cancelInstallation() globalNetworkManager.cancelInstallation()
isInstalling = false isInstalling = false
}, },
onRetry: nil onRetry: nil
) )
} else if case .completed = networkManager.installationState { } else if case .completed = globalNetworkManager.installationState {
InstallProgressView( InstallProgressView(
productName: task.displayName, productName: task.displayName,
progress: 1.0, progress: 1.0,
@@ -238,7 +237,7 @@ struct DownloadProgressView: View {
}, },
onRetry: nil onRetry: nil
) )
} else if case .failed(let error) = networkManager.installationState { } else if case .failed(let error) = globalNetworkManager.installationState {
InstallProgressView( InstallProgressView(
productName: task.displayName, productName: task.displayName,
progress: 0, progress: 0,
@@ -248,7 +247,7 @@ struct DownloadProgressView: View {
}, },
onRetry: { onRetry: {
Task { Task {
await networkManager.retryInstallation(at: task.directory) await globalNetworkManager.retryInstallation(at: task.directory)
} }
} }
) )
@@ -258,7 +257,7 @@ struct DownloadProgressView: View {
progress: 0, progress: 0,
status: String(localized: "准备安装..."), status: String(localized: "准备安装..."),
onCancel: { onCancel: {
networkManager.cancelInstallation() globalNetworkManager.cancelInstallation()
isInstalling = false isInstalling = false
}, },
onRetry: nil onRetry: nil
@@ -301,12 +300,12 @@ struct DownloadProgressView: View {
} }
private func loadIcon() { private func loadIcon() {
let product = globalCcmResult.products.first { $0.id == task.productId } let product = findProduct(id: task.productId)
if product != nil { if product != nil {
if let bestIcon = product.getBestIcon(), if let bestIcon = product?.getBestIcon(),
let iconURL = URL(string: bestIcon.url) { let iconURL = URL(string: bestIcon.value) {
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) { if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) {
self.iconImage = cachedImage self.iconImage = cachedImage
return return
} }
@@ -324,13 +323,13 @@ struct DownloadProgressView: View {
throw URLError(.badServerResponse) throw URLError(.badServerResponse)
} }
IconCache.shared.setIcon(image, for: bestIcon.url) IconCache.shared.setIcon(image, for: bestIcon.value)
await MainActor.run { await MainActor.run {
self.iconImage = image self.iconImage = image
} }
} catch { } catch {
if let localImage = NSImage(named: task.sapCode) { if let localImage = NSImage(named: task.productId) {
await MainActor.run { await MainActor.run {
self.iconImage = localImage self.iconImage = localImage
} }
@@ -357,184 +356,20 @@ struct DownloadProgressView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 12) { TaskHeaderView(iconImage: iconImage, task: task, loadIcon: loadIcon, formatPath: formatPath, openInFinder: openInFinder)
Group {
if let iconImage = iconImage {
Image(nsImage: iconImage)
.resizable()
.interpolation(.high)
.aspectRatio(contentMode: .fit)
} else {
Image(systemName: "app.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.secondary)
}
}
.frame(width: 32, height: 32)
.onAppear(perform: loadIcon)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
HStack(spacing: 4) {
Text(task.displayName)
.font(.headline)
Text(task.productVersion)
.foregroundColor(.secondary)
}
statusLabel
Spacer()
}
Text(formatPath(task.directory.path))
.font(.caption)
.foregroundColor(.blue)
.lineLimit(1)
.truncationMode(.middle)
.onTapGesture {
openInFinder(task.directory.path)
}
.help(task.directory.path)
}
}
VStack(alignment: .leading, spacing: 4) { TaskProgressView(task: task, formatRemainingTime: formatRemainingTime, formatSpeed: formatSpeed)
HStack {
HStack(spacing: 4) {
Text(task.formattedDownloadedSize)
Text("/")
Text(task.formattedTotalSize)
}
Spacer()
if task.totalSpeed > 0 {
Text(formatRemainingTime(
totalSize: task.totalSize,
downloadedSize: task.totalDownloadedSize,
speed: task.totalSpeed
))
.foregroundColor(.secondary)
}
Text("\(Int(task.totalProgress * 100))%")
if task.totalSpeed > 0 {
Text(formatSpeed(task.totalSpeed))
.foregroundColor(.secondary)
}
}
.font(.caption)
ProgressView(value: task.totalProgress)
.progressViewStyle(.linear)
}
if !task.dependenciesToDownload.isEmpty { if !task.dependenciesToDownload.isEmpty {
Divider() Divider()
PackageListView(
VStack(alignment: .leading, spacing: 6) { task: task,
HStack { isPackageListExpanded: $isPackageListExpanded,
Button(action: { showCommandLineInstall: $showCommandLineInstall,
withAnimation { showCopiedAlert: $showCopiedAlert,
isPackageListExpanded.toggle() expandedProducts: $expandedProducts,
} actionButtons: AnyView(actionButtons)
}) { )
HStack {
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
.foregroundColor(.secondary)
Text("产品和包列表")
.font(.caption)
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Spacer()
#if DEBUG
Button(action: {
let containerURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
let fileName = "\(task.sapCode == "APRO" ? "Adobe Downloader \(task.sapCode)_\(task.version)_\(task.platform)" : "Adobe Downloader \(task.sapCode)_\(task.version)-\(task.language)-\(task.platform)")-task.json"
let fileURL = tasksDirectory.appendingPathComponent(fileName)
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: tasksDirectory.path)
}) {
Label("查看持久化文件", systemImage: "doc.text.magnifyingglass")
}
.buttonStyle(.borderedProminent)
.tint(.blue)
.controlSize(.regular)
#endif
if case .completed = task.status, task.sapCode != "APRO" {
Button(action: {
showCommandLineInstall.toggle()
}) {
Label("命令行安装", systemImage: "terminal")
}
.buttonStyle(.borderedProminent)
.tint(.purple)
.controlSize(.regular)
.popover(isPresented: $showCommandLineInstall, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 8) {
Button("复制命令") {
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
let driverPath = "\(task.directory.path)/driver.xml"
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(command, forType: .string)
showCopiedAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
showCopiedAlert = false
}
}
if showCopiedAlert {
Text("已复制")
.font(.caption)
.foregroundColor(.green)
}
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
let driverPath = "\(task.directory.path)/driver.xml"
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
Text(command)
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)
.textSelection(.enabled)
.padding(8)
.background(Color.secondary.opacity(0.1))
.cornerRadius(6)
}
.padding()
.frame(width: 400)
}
}
actionButtons
}
if isPackageListExpanded {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 8) {
ForEach(task.productsToDownload, id: \.sapCode) { product in
ProductRow(
product: product,
isCurrentProduct: task.currentPackage?.id == product.packages.first?.id,
expandedProducts: $expandedProducts
)
}
}
}
.frame(maxHeight: 200)
}
}
} }
} }
.padding() .padding()
@@ -548,8 +383,256 @@ struct DownloadProgressView: View {
} }
} }
private struct TaskHeaderView: View {
let iconImage: NSImage?
let task: NewDownloadTask
let loadIcon: () -> Void
let formatPath: (String) -> String
let openInFinder: (String) -> Void
var body: some View {
HStack(spacing: 12) {
Group {
if let iconImage = iconImage {
Image(nsImage: iconImage)
.resizable()
.interpolation(.high)
.aspectRatio(contentMode: .fit)
} else {
Image(systemName: "app.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.secondary)
}
}
.frame(width: 32, height: 32)
.onAppear(perform: loadIcon)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
HStack(spacing: 4) {
Text(task.displayName)
.font(.headline)
Text(task.productVersion)
.foregroundColor(.secondary)
}
statusLabel
Spacer()
}
Text(formatPath(task.directory.path))
.font(.caption)
.foregroundColor(.blue)
.lineLimit(1)
.truncationMode(.middle)
.onTapGesture {
openInFinder(task.directory.path)
}
.help(task.directory.path)
}
}
}
private var statusLabel: some View {
Text(task.status.description)
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 2)
.padding(.horizontal, 6)
.background(statusBackgroundColor)
.cornerRadius(4)
}
private var statusBackgroundColor: Color {
switch task.status {
case .downloading:
return Color.blue
case .preparing:
return Color.purple.opacity(0.8)
case .completed:
return Color.green.opacity(0.8)
case .failed:
return Color.red.opacity(0.8)
case .paused:
return Color.orange.opacity(0.8)
case .waiting:
return Color.gray.opacity(0.8)
case .retrying:
return Color.yellow.opacity(0.8)
}
}
}
private struct TaskProgressView: View {
let task: NewDownloadTask
let formatRemainingTime: (Int64, Int64, Double) -> String
let formatSpeed: (Double) -> String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
HStack(spacing: 4) {
Text(task.formattedDownloadedSize)
Text("/")
Text(task.formattedTotalSize)
}
Spacer()
if task.totalSpeed > 0 {
Text(formatRemainingTime(
task.totalSize,
task.totalDownloadedSize,
task.totalSpeed
))
.foregroundColor(.secondary)
}
Text("\(Int(task.totalProgress * 100))%")
if task.totalSpeed > 0 {
Text(formatSpeed(task.totalSpeed))
.foregroundColor(.secondary)
}
}
.font(.caption)
ProgressView(value: task.totalProgress)
.progressViewStyle(.linear)
}
}
}
private struct PackageListView: View {
let task: NewDownloadTask
@Binding var isPackageListExpanded: Bool
@Binding var showCommandLineInstall: Bool
@Binding var showCopiedAlert: Bool
@Binding var expandedProducts: Set<String>
let actionButtons: AnyView
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Button(action: {
withAnimation {
isPackageListExpanded.toggle()
}
}) {
HStack {
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
.foregroundColor(.secondary)
Text("产品和包列表")
.font(.caption)
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Spacer()
#if DEBUG
Button(action: {
let containerURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
let fileName = "\(task.productId == "APRO" ? "Adobe Downloader \(task.productId)_\(task.productVersion)_\(task.platform)" : "Adobe Downloader \(task.productId)_\(task.productVersion)-\(task.language)-\(task.platform)")-task.json"
let fileURL = tasksDirectory.appendingPathComponent(fileName)
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: tasksDirectory.path)
}) {
Label("查看持久化文件", systemImage: "doc.text.magnifyingglass")
}
.buttonStyle(.borderedProminent)
.tint(.blue)
.controlSize(.regular)
#endif
if case .completed = task.status, task.productId != "APRO" {
CommandLineInstallButton(
task: task,
showCommandLineInstall: $showCommandLineInstall,
showCopiedAlert: $showCopiedAlert
)
}
actionButtons
}
if isPackageListExpanded {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 8) {
ForEach(task.dependenciesToDownload, id: \.sapCode) { product in
ProductRow(
product: product,
isCurrentProduct: task.currentPackage?.id == product.packages.first?.id,
expandedProducts: $expandedProducts
)
}
}
}
.frame(maxHeight: 200)
}
}
}
}
private struct CommandLineInstallButton: View {
let task: NewDownloadTask
@Binding var showCommandLineInstall: Bool
@Binding var showCopiedAlert: Bool
var body: some View {
Button(action: {
showCommandLineInstall.toggle()
}) {
Label("命令行安装", systemImage: "terminal")
}
.buttonStyle(.borderedProminent)
.tint(.purple)
.controlSize(.regular)
.popover(isPresented: $showCommandLineInstall, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 8) {
Button("复制命令") {
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
let driverPath = "\(task.directory.path)/driver.xml"
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(command, forType: .string)
showCopiedAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
showCopiedAlert = false
}
}
if showCopiedAlert {
Text("已复制")
.font(.caption)
.foregroundColor(.green)
}
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
let driverPath = "\(task.directory.path)/driver.xml"
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
Text(command)
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)
.textSelection(.enabled)
.padding(8)
.background(Color.secondary.opacity(0.1))
.cornerRadius(6)
}
.padding()
.frame(width: 400)
}
}
}
struct ProductRow: View { struct ProductRow: View {
@ObservedObject var dependencies: DependenciesToDownload @ObservedObject var product: DependenciesToDownload
let isCurrentProduct: Bool let isCurrentProduct: Bool
@Binding var expandedProducts: Set<String> @Binding var expandedProducts: Set<String>

View File

@@ -20,7 +20,7 @@ struct ShouldExistsSetUpView: View {
var body: some View { var body: some View {
VStack(spacing: 20) { VStack(spacing: 20) {
HeaderView() SetupAlertHeaderView()
MessageView() MessageView()
ButtonsView( ButtonsView(
isDownloading: $isDownloading, isDownloading: $isDownloading,
@@ -47,17 +47,19 @@ struct ShouldExistsSetUpView: View {
} }
} }
private struct HeaderView: View { private struct SetupAlertHeaderView: View {
var body: some View { var body: some View {
Image(systemName: "exclamationmark.triangle.fill") VStack {
.font(.system(size: 64)) Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange) .font(.system(size: 64))
.padding(.bottom, 5) .foregroundColor(.orange)
.frame(alignment: .bottomTrailing) .padding(.bottom, 5)
.frame(alignment: .bottomTrailing)
Text("未检测到 Adobe CC 组件") Text("未检测到 Adobe CC 组件")
.font(.system(size: 24)) .font(.system(size: 24))
.bold() .bold()
}
} }
} }
@@ -176,7 +178,7 @@ private struct ButtonsView: View {
isCancelled = false isCancelled = false
Task { Task {
do { do {
try await networkManager.downloadUtils.downloadX1a0HeCCPackages( try await globalNewDownloadUtils.downloadX1a0HeCCPackages(
progressHandler: { progress, status in progressHandler: { progress, status in
Task { @MainActor in Task { @MainActor in
downloadProgress = progress downloadProgress = progress

View File

@@ -21,7 +21,6 @@ private enum VersionPickerConstants {
} }
struct VersionPickerView: View { struct VersionPickerView: View {
@EnvironmentObject private var networkManager: NetworkManager
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@StorageValue(\.defaultLanguage) private var defaultLanguage @StorageValue(\.defaultLanguage) private var defaultLanguage
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon @StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
@@ -37,7 +36,7 @@ struct VersionPickerView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon) VersionPickerHeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon)
VersionListView( VersionListView(
product: product, product: product,
expandedVersions: $expandedVersions, expandedVersions: $expandedVersions,
@@ -49,7 +48,7 @@ struct VersionPickerView: View {
} }
} }
private struct HeaderView: View { private struct VersionPickerHeaderView: View {
let product: Product let product: Product
let downloadAppleSilicon: Bool let downloadAppleSilicon: Bool
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@@ -160,7 +159,7 @@ private struct VersionRow: View {
private var existingPath: URL? { private var existingPath: URL? {
globalNetworkManager.isVersionDownloaded( globalNetworkManager.isVersionDownloaded(
product: product, productId: product.id,
version: version, version: version,
language: defaultLanguage language: defaultLanguage
) )

View File

@@ -11,7 +11,6 @@
}, },
"(可能导致处理 Setup 组件失败)" : { "(可能导致处理 Setup 组件失败)" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -22,7 +21,6 @@
} }
}, },
"(将导致无法使用安装功能)" : { "(将导致无法使用安装功能)" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -157,8 +155,12 @@
}, },
"Adobe Downloader %@" : { "Adobe Downloader %@" : {
},
"Adobe Downloader 完全免费: https://github.com/X1a0He/Adobe-Downloader" : {
}, },
"Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader" : { "Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -288,6 +290,7 @@
}, },
"Debug 模式" : { "Debug 模式" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -422,6 +425,7 @@
}, },
"Setup 组件安装成功" : { "Setup 组件安装成功" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -769,6 +773,9 @@
} }
} }
}, },
"产品未找到" : {
"comment" : "Product not found"
},
"仅下载" : { "仅下载" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -1385,6 +1392,7 @@
} }
}, },
"将执行的命令:" : { "将执行的命令:" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1425,6 +1433,7 @@
} }
}, },
"展开全部" : { "展开全部" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1445,7 +1454,6 @@
} }
}, },
"已处理" : { "已处理" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1456,7 +1464,6 @@
} }
}, },
"已备份" : { "已备份" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1662,6 +1669,7 @@
} }
}, },
"折叠全部" : { "折叠全部" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -2099,9 +2107,6 @@
} }
} }
} }
},
"查看持久化文件" : {
}, },
"检查中" : { "检查中" : {
"localizations" : { "localizations" : {