mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
Add: Support task record persistence.
This commit is contained in:
@@ -286,7 +286,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 102;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
@@ -300,7 +300,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -318,7 +318,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 102;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
@@ -332,7 +332,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "468"
|
||||
endingLineNumber = "468"
|
||||
landmarkName = "retryPackage(task:package:)"
|
||||
startingLineNumber = "463"
|
||||
endingLineNumber = "463"
|
||||
landmarkName = "startDownloadProcess(task:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
|
||||
@@ -56,6 +56,10 @@ struct Adobe_DownloaderApp: App {
|
||||
.frame(width: 850, height: 800)
|
||||
.tint(.blue)
|
||||
.onAppear {
|
||||
appDelegate.networkManager = networkManager
|
||||
|
||||
networkManager.loadSavedTasks()
|
||||
|
||||
checkCreativeCloudSetup()
|
||||
|
||||
if ModifySetup.checkSetupBackup() {
|
||||
|
||||
@@ -2,7 +2,78 @@ import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var eventMonitor: Any?
|
||||
var networkManager: NetworkManager?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.mainMenu = nil
|
||||
NSApp.mainMenu = nil
|
||||
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
if event.modifierFlags.contains(.command) && event.characters?.lowercased() == "q" {
|
||||
if let mainWindow = NSApp.mainWindow,
|
||||
mainWindow.sheets.isEmpty && !mainWindow.isSheet {
|
||||
self?.handleQuitCommand()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func handleQuitCommand() {
|
||||
guard let manager = networkManager else {
|
||||
NSApplication.shared.terminate(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let hasActiveDownloads = manager.downloadTasks.contains { task in
|
||||
if case .downloading = task.totalStatus {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if hasActiveDownloads {
|
||||
Task {
|
||||
for task in manager.downloadTasks {
|
||||
if case .downloading = task.totalStatus {
|
||||
await manager.downloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: .other(String(localized: "程序即将退出"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = String(localized: "确认退出")
|
||||
alert.informativeText = String(localized:"有正在进行的下载任务,确定要退出吗?\n所有下载任务的进度已保存,下次启动可以继续下载")
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: String(localized:"退出"))
|
||||
alert.addButton(withTitle: String(localized:"取消"))
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertSecondButtonReturn {
|
||||
Task {
|
||||
for task in manager.downloadTasks {
|
||||
if case .paused = task.totalStatus {
|
||||
await manager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSApplication.shared.terminate(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let monitor = eventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
}
|
||||
networkManager = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum PackageStatus: Equatable {
|
||||
enum PackageStatus: Equatable, Codable {
|
||||
case waiting
|
||||
case downloading
|
||||
case paused
|
||||
@@ -165,7 +165,7 @@ enum NetworkError: Error, LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus: Equatable {
|
||||
enum DownloadStatus: Equatable, Codable {
|
||||
case waiting
|
||||
case preparing(PrepareInfo)
|
||||
case downloading(DownloadInfo)
|
||||
@@ -174,12 +174,12 @@ enum DownloadStatus: Equatable {
|
||||
case failed(FailureInfo)
|
||||
case retrying(RetryInfo)
|
||||
|
||||
struct PrepareInfo {
|
||||
struct PrepareInfo: Codable {
|
||||
let message: String
|
||||
let timestamp: Date
|
||||
let stage: PrepareStage
|
||||
|
||||
enum PrepareStage {
|
||||
enum PrepareStage: Codable {
|
||||
case initializing
|
||||
case creatingInstaller
|
||||
case signingApp
|
||||
@@ -188,7 +188,7 @@ enum DownloadStatus: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInfo {
|
||||
struct DownloadInfo: Codable {
|
||||
let fileName: String
|
||||
let currentPackageIndex: Int
|
||||
let totalPackages: Int
|
||||
@@ -196,12 +196,12 @@ enum DownloadStatus: Equatable {
|
||||
let estimatedTimeRemaining: TimeInterval?
|
||||
}
|
||||
|
||||
struct PauseInfo {
|
||||
struct PauseInfo: Codable {
|
||||
let reason: PauseReason
|
||||
let timestamp: Date
|
||||
let resumable: Bool
|
||||
|
||||
enum PauseReason {
|
||||
enum PauseReason: Codable {
|
||||
case userRequested
|
||||
case networkIssue
|
||||
case systemSleep
|
||||
@@ -209,26 +209,124 @@ enum DownloadStatus: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletionInfo {
|
||||
struct CompletionInfo: Codable {
|
||||
let timestamp: Date
|
||||
let totalTime: TimeInterval
|
||||
let totalSize: Int64
|
||||
}
|
||||
|
||||
struct FailureInfo {
|
||||
struct FailureInfo: Codable {
|
||||
let message: String
|
||||
let error: Error?
|
||||
let timestamp: Date
|
||||
let recoverable: Bool
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case message
|
||||
case timestamp
|
||||
case recoverable
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(message, forKey: .message)
|
||||
try container.encode(timestamp, forKey: .timestamp)
|
||||
try container.encode(recoverable, forKey: .recoverable)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
message = try container.decode(String.self, forKey: .message)
|
||||
timestamp = try container.decode(Date.self, forKey: .timestamp)
|
||||
recoverable = try container.decode(Bool.self, forKey: .recoverable)
|
||||
error = nil
|
||||
}
|
||||
|
||||
init(message: String, error: Error?, timestamp: Date, recoverable: Bool) {
|
||||
self.message = message
|
||||
self.error = error
|
||||
self.timestamp = timestamp
|
||||
self.recoverable = recoverable
|
||||
}
|
||||
}
|
||||
|
||||
struct RetryInfo {
|
||||
struct RetryInfo: Codable {
|
||||
let attempt: Int
|
||||
let maxAttempts: Int
|
||||
let reason: String
|
||||
let nextRetryDate: Date
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case info
|
||||
}
|
||||
|
||||
private enum StatusType: String, Codable {
|
||||
case waiting
|
||||
case preparing
|
||||
case downloading
|
||||
case paused
|
||||
case completed
|
||||
case failed
|
||||
case retrying
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .waiting:
|
||||
try container.encode(StatusType.waiting, forKey: .type)
|
||||
case .preparing(let info):
|
||||
try container.encode(StatusType.preparing, forKey: .type)
|
||||
try container.encode(info, forKey: .info)
|
||||
case .downloading(let info):
|
||||
try container.encode(StatusType.downloading, forKey: .type)
|
||||
try container.encode(info, forKey: .info)
|
||||
case .paused(let info):
|
||||
try container.encode(StatusType.paused, forKey: .type)
|
||||
try container.encode(info, forKey: .info)
|
||||
case .completed(let info):
|
||||
try container.encode(StatusType.completed, forKey: .type)
|
||||
try container.encode(info, forKey: .info)
|
||||
case .failed(let info):
|
||||
try container.encode(StatusType.failed, forKey: .type)
|
||||
try container.encode(info, forKey: .info)
|
||||
case .retrying(let info):
|
||||
try container.encode(StatusType.retrying, forKey: .type)
|
||||
try container.encode(info, forKey: .info)
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(StatusType.self, forKey: .type)
|
||||
|
||||
switch type {
|
||||
case .waiting:
|
||||
self = .waiting
|
||||
case .preparing:
|
||||
let info = try container.decode(PrepareInfo.self, forKey: .info)
|
||||
self = .preparing(info)
|
||||
case .downloading:
|
||||
let info = try container.decode(DownloadInfo.self, forKey: .info)
|
||||
self = .downloading(info)
|
||||
case .paused:
|
||||
let info = try container.decode(PauseInfo.self, forKey: .info)
|
||||
self = .paused(info)
|
||||
case .completed:
|
||||
let info = try container.decode(CompletionInfo.self, forKey: .info)
|
||||
self = .completed(info)
|
||||
case .failed:
|
||||
let info = try container.decode(FailureInfo.self, forKey: .info)
|
||||
self = .failed(info)
|
||||
case .retrying:
|
||||
let info = try container.decode(RetryInfo.self, forKey: .info)
|
||||
self = .retrying(info)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .waiting:
|
||||
|
||||
@@ -65,6 +65,7 @@ struct ContentView: View {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.imageScale(.medium)
|
||||
}
|
||||
.disabled(isRefreshing)
|
||||
.buttonStyle(.borderless)
|
||||
.overlay(
|
||||
Group {
|
||||
|
||||
@@ -42,6 +42,7 @@ class NewDownloadTask: Identifiable, ObservableObject, Equatable {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
let platform: String
|
||||
|
||||
var status: DownloadStatus {
|
||||
totalStatus ?? .waiting
|
||||
@@ -88,7 +89,7 @@ class NewDownloadTask: Identifiable, ObservableObject, Equatable {
|
||||
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) {
|
||||
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
|
||||
@@ -104,6 +105,7 @@ class NewDownloadTask: Identifiable, ObservableObject, Equatable {
|
||||
self.totalSpeed = totalSpeed
|
||||
self.currentPackage = currentPackage
|
||||
self.displayInstallButton = sapCode != "APRO"
|
||||
self.platform = platform
|
||||
}
|
||||
|
||||
static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool {
|
||||
|
||||
@@ -40,12 +40,18 @@ class NetworkManager: ObservableObject {
|
||||
case failed(Error)
|
||||
}
|
||||
|
||||
init() {
|
||||
private let networkService: NetworkService
|
||||
|
||||
init(networkService: NetworkService = NetworkService(),
|
||||
downloadUtils: DownloadUtils? = nil) {
|
||||
let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon")
|
||||
self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
||||
|
||||
self.downloadUtils = DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
||||
setupNetworkMonitoring()
|
||||
self.networkService = networkService
|
||||
self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
||||
|
||||
TaskPersistenceManager.shared.setCancelTracker(cancelTracker)
|
||||
configureNetworkMonitor()
|
||||
}
|
||||
|
||||
func fetchProducts() async {
|
||||
@@ -64,15 +70,21 @@ class NetworkManager: ObservableObject {
|
||||
directory: destinationURL,
|
||||
productsToDownload: [],
|
||||
createAt: Date(),
|
||||
totalStatus: .preparing(DownloadStatus.PrepareInfo(message: "正在准备下载...", timestamp: Date(), stage: .initializing)),
|
||||
totalStatus: .preparing(DownloadStatus.PrepareInfo(
|
||||
message: "正在准备下载...",
|
||||
timestamp: Date(),
|
||||
stage: .initializing
|
||||
)),
|
||||
totalProgress: 0,
|
||||
totalDownloadedSize: 0,
|
||||
totalSize: 0,
|
||||
totalSpeed: 0
|
||||
totalSpeed: 0,
|
||||
platform: productInfo.apPlatform
|
||||
)
|
||||
|
||||
downloadTasks.append(task)
|
||||
updateDockBadge()
|
||||
saveTask(task)
|
||||
|
||||
do {
|
||||
try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: allowedPlatform, saps: saps)
|
||||
@@ -84,6 +96,7 @@ class NetworkManager: ObservableObject {
|
||||
timestamp: Date(),
|
||||
recoverable: true
|
||||
)))
|
||||
saveTask(task)
|
||||
objectWillChange.send()
|
||||
}
|
||||
throw error
|
||||
@@ -101,10 +114,22 @@ class NetworkManager: ObservableObject {
|
||||
await cancelTracker.cancel(taskId)
|
||||
|
||||
if let task = downloadTasks.first(where: { $0.id == taskId }) {
|
||||
if task.status.isActive {
|
||||
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
||||
message: "下载已取消",
|
||||
error: NetworkError.downloadCancelled,
|
||||
timestamp: Date(),
|
||||
recoverable: false
|
||||
)))
|
||||
saveTask(task)
|
||||
}
|
||||
|
||||
if removeFiles {
|
||||
try? FileManager.default.removeItem(at: task.directory)
|
||||
}
|
||||
|
||||
TaskPersistenceManager.shared.removeTask(task)
|
||||
|
||||
await MainActor.run {
|
||||
downloadTasks.removeAll { $0.id == taskId }
|
||||
updateDockBadge()
|
||||
@@ -125,7 +150,7 @@ class NetworkManager: ObservableObject {
|
||||
|
||||
while retryCount < maxRetries {
|
||||
do {
|
||||
let (saps, cdn, sapCodes) = try await fetchProductsData()
|
||||
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
|
||||
|
||||
await MainActor.run {
|
||||
self.saps = saps
|
||||
@@ -273,97 +298,17 @@ class NetworkManager: ObservableObject {
|
||||
}
|
||||
|
||||
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["Cookie"] = generateCookie()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func fetchProductsData() async throws -> ([String: Sap], String, [SapCodes]) {
|
||||
var components = URLComponents(string: NetworkConstants.productsXmlURL)
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "_type", value: "xml"),
|
||||
URLQueryItem(name: "channel", value: "ccm"),
|
||||
URLQueryItem(name: "channel", value: "sti"),
|
||||
URLQueryItem(name: "platform", value: "osx10-64,osx10,macarm64,macuniversal"),
|
||||
URLQueryItem(name: "productType", value: "Desktop")
|
||||
]
|
||||
|
||||
guard let url = components?.url else {
|
||||
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
NetworkConstants.adobeRequestHeaders.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, nil)
|
||||
}
|
||||
|
||||
guard let xmlString = String(data: data, encoding: .utf8) else {
|
||||
throw NetworkError.invalidData("无法解码XML数据")
|
||||
}
|
||||
|
||||
let result: ([String: Sap], String, [SapCodes]) = try await Task.detached(priority: .userInitiated) {
|
||||
let parseResult = try XHXMLParser.parse(xmlString: xmlString)
|
||||
let products = parseResult.products, cdn = parseResult.cdn
|
||||
var sapCodes: [SapCodes] = []
|
||||
let allowedPlatforms = ["macuniversal", "macarm64", "osx10-64", "osx10"]
|
||||
for product in products.values {
|
||||
if product.isValid {
|
||||
var lastVersion: String? = nil
|
||||
for version in product.versions.values.reversed() {
|
||||
if !version.buildGuid.isEmpty && allowedPlatforms.contains(version.apPlatform) {
|
||||
lastVersion = version.productVersion
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastVersion != nil {
|
||||
sapCodes.append(SapCodes(
|
||||
sapCode: product.sapCode,
|
||||
displayName: product.displayName
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
return (products, cdn, sapCodes)
|
||||
}.value
|
||||
|
||||
return result
|
||||
return try await networkService.getApplicationInfo(buildGuid: buildGuid)
|
||||
}
|
||||
|
||||
func isVersionDownloaded(sap: Sap, version: String, language: String) -> URL? {
|
||||
if let task = downloadTasks.first(where: {
|
||||
$0.sapCode == sap.sapCode &&
|
||||
$0.version == version &&
|
||||
$0.language == language &&
|
||||
!$0.status.isCompleted
|
||||
}) { return task.directory }
|
||||
|
||||
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
||||
let fileName = sap.sapCode == "APRO"
|
||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
||||
@@ -377,16 +322,6 @@ class NetworkManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
if let task = downloadTasks.first(where: {
|
||||
$0.sapCode == sap.sapCode &&
|
||||
$0.version == version &&
|
||||
$0.language == language
|
||||
}) {
|
||||
if FileManager.default.fileExists(atPath: task.directory.path) {
|
||||
return task.directory
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -405,10 +340,6 @@ class NetworkManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNetworkMonitoring() {
|
||||
configureNetworkMonitor()
|
||||
}
|
||||
|
||||
func retryFetchData() {
|
||||
Task {
|
||||
isFetchingProducts = false
|
||||
@@ -420,4 +351,19 @@ class NetworkManager: ObservableObject {
|
||||
func updateAllowedPlatform(useAppleSilicon: Bool) {
|
||||
allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
||||
}
|
||||
|
||||
func saveTask(_ task: NewDownloadTask) {
|
||||
TaskPersistenceManager.shared.saveTask(task)
|
||||
}
|
||||
|
||||
func loadSavedTasks() {
|
||||
let savedTasks = TaskPersistenceManager.shared.loadTasks()
|
||||
for task in savedTasks {
|
||||
for product in task.productsToDownload {
|
||||
product.updateCompletedPackages()
|
||||
}
|
||||
}
|
||||
downloadTasks.append(contentsOf: savedTasks)
|
||||
updateDockBadge()
|
||||
}
|
||||
}
|
||||
|
||||
100
Adobe Downloader/Services/NetworkService.swift
Normal file
100
Adobe Downloader/Services/NetworkService.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import Foundation
|
||||
|
||||
class NetworkService {
|
||||
func fetchProductsData() async throws -> ([String: Sap], String, [SapCodes]) {
|
||||
var components = URLComponents(string: NetworkConstants.productsXmlURL)
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "_type", value: "xml"),
|
||||
URLQueryItem(name: "channel", value: "ccm"),
|
||||
URLQueryItem(name: "channel", value: "sti"),
|
||||
URLQueryItem(name: "platform", value: "osx10-64,osx10,macarm64,macuniversal"),
|
||||
URLQueryItem(name: "productType", value: "Desktop")
|
||||
]
|
||||
|
||||
guard let url = components?.url else {
|
||||
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
NetworkConstants.adobeRequestHeaders.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, nil)
|
||||
}
|
||||
|
||||
guard let xmlString = String(data: data, encoding: .utf8) else {
|
||||
throw NetworkError.invalidData("无法解码XML数据")
|
||||
}
|
||||
|
||||
let result: ([String: Sap], String, [SapCodes]) = try await Task.detached(priority: .userInitiated) {
|
||||
let parseResult = try XHXMLParser.parse(xmlString: xmlString)
|
||||
let products = parseResult.products, cdn = parseResult.cdn
|
||||
var sapCodes: [SapCodes] = []
|
||||
let allowedPlatforms = ["macuniversal", "macarm64", "osx10-64", "osx10"]
|
||||
for product in products.values {
|
||||
if product.isValid {
|
||||
var lastVersion: String? = nil
|
||||
for version in product.versions.values.reversed() {
|
||||
if !version.buildGuid.isEmpty && allowedPlatforms.contains(version.apPlatform) {
|
||||
lastVersion = version.productVersion
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastVersion != nil {
|
||||
sapCodes.append(SapCodes(
|
||||
sapCode: product.sapCode,
|
||||
displayName: product.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["Cookie"] = generateCookie()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private func generateCookie() -> String {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let random = Int.random(in: 100000...999999)
|
||||
return "s_cc=true; s_sq=; AMCV_9E1005A551ED61CA0A490D45%40AdobeOrg=1075005958%7CMCIDTS%7C\(timestamp)%7CMCMID%7C\(random)%7CMCAAMLH-1683925272%7C11%7CMCAAMB-1683925272%7CRKhpRz8krg2tLO6pguXWp5olkAcUniQYPHaMWWgdJ3xzPWQmdj0y%7CMCOPTOUT-1683327672s%7CNONE%7CvVersion%7C4.4.1; gpv=cc-search-desktop; s_ppn=cc-search-desktop"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import Foundation
|
||||
actor CancelTracker {
|
||||
private var cancelledIds: Set<UUID> = []
|
||||
private var pausedIds: Set<UUID> = []
|
||||
private var downloadTasks: [UUID: URLSessionDownloadTask] = [:]
|
||||
var downloadTasks: [UUID: URLSessionDownloadTask] = [:]
|
||||
private var sessions: [UUID: URLSession] = [:]
|
||||
private var resumeData: [UUID: Data] = [:]
|
||||
|
||||
@@ -64,4 +64,8 @@ actor CancelTracker {
|
||||
func isPaused(_ id: UUID) -> Bool {
|
||||
return pausedIds.contains(id)
|
||||
}
|
||||
|
||||
func storeResumeData(_ id: UUID, data: Data) {
|
||||
resumeData[id] = data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,18 @@ class DownloadUtils {
|
||||
}
|
||||
|
||||
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
|
||||
let task = await cancelTracker.downloadTasks[taskId]
|
||||
if let downloadTask = task {
|
||||
let data = await withCheckedContinuation { continuation in
|
||||
downloadTask.cancel(byProducingResumeData: { data in
|
||||
continuation.resume(returning: data)
|
||||
})
|
||||
}
|
||||
if let data = data {
|
||||
await cancelTracker.storeResumeData(taskId, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
||||
task.setStatus(.paused(DownloadStatus.PauseInfo(
|
||||
@@ -110,24 +122,12 @@ class DownloadUtils {
|
||||
timestamp: Date(),
|
||||
resumable: true
|
||||
)))
|
||||
networkManager?.saveTask(task)
|
||||
}
|
||||
}
|
||||
await cancelTracker.pause(taskId)
|
||||
}
|
||||
|
||||
func resumeDownloadTask(taskId: UUID) async {
|
||||
await MainActor.run {
|
||||
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
||||
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
|
||||
fileName: task.currentPackage?.fullPackageName ?? "",
|
||||
currentPackageIndex: 0,
|
||||
totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count },
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
||||
await startDownloadProcess(task: task)
|
||||
}
|
||||
@@ -223,7 +223,133 @@ class DownloadUtils {
|
||||
}
|
||||
}
|
||||
|
||||
internal func startDownloadProcess(task: NewDownloadTask) async {
|
||||
private func downloadPackage(package: Package, task: NewDownloadTask, product: ProductsToDownload, url: URL? = nil, resumeData: Data? = nil) async throws {
|
||||
var lastUpdateTime = Date()
|
||||
var lastBytes: Int64 = 0
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let delegate = DownloadDelegate(
|
||||
destinationDirectory: task.directory.appendingPathComponent(product.sapCode),
|
||||
fileName: package.fullPackageName,
|
||||
completionHandler: { [weak networkManager] localURL, response, error in
|
||||
if let error = error {
|
||||
if (error as NSError).code == NSURLErrorCancelled {
|
||||
continuation.resume()
|
||||
} else {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
package.downloadedSize = package.downloadSize
|
||||
package.progress = 1.0
|
||||
package.status = .completed
|
||||
package.downloaded = true
|
||||
|
||||
var totalDownloaded: Int64 = 0
|
||||
var totalSize: Int64 = 0
|
||||
|
||||
for prod in task.productsToDownload {
|
||||
for pkg in prod.packages {
|
||||
totalSize += pkg.downloadSize
|
||||
if pkg.downloaded {
|
||||
totalDownloaded += pkg.downloadSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.totalSize = totalSize
|
||||
task.totalDownloadedSize = totalDownloaded
|
||||
task.totalProgress = Double(totalDownloaded) / Double(totalSize)
|
||||
task.totalSpeed = 0
|
||||
|
||||
let allCompleted = task.productsToDownload.allSatisfy {
|
||||
product in product.packages.allSatisfy { $0.downloaded }
|
||||
}
|
||||
|
||||
if allCompleted {
|
||||
task.setStatus(.completed(DownloadStatus.CompletionInfo(
|
||||
timestamp: Date(),
|
||||
totalTime: Date().timeIntervalSince(task.createAt),
|
||||
totalSize: totalSize
|
||||
)))
|
||||
}
|
||||
|
||||
product.updateCompletedPackages()
|
||||
networkManager?.saveTask(task)
|
||||
networkManager?.objectWillChange.send()
|
||||
}
|
||||
|
||||
continuation.resume()
|
||||
},
|
||||
progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
|
||||
Task { @MainActor in
|
||||
let now = Date()
|
||||
let timeDiff = now.timeIntervalSince(lastUpdateTime)
|
||||
|
||||
if timeDiff >= 1.0 {
|
||||
let bytesDiff = totalBytesWritten - lastBytes
|
||||
let speed = Double(bytesDiff) / timeDiff
|
||||
|
||||
package.updateProgress(
|
||||
downloadedSize: totalBytesWritten,
|
||||
speed: speed
|
||||
)
|
||||
|
||||
var totalDownloaded: Int64 = 0
|
||||
var totalSize: Int64 = 0
|
||||
var currentSpeed: Double = 0
|
||||
|
||||
for prod in task.productsToDownload {
|
||||
for pkg in prod.packages {
|
||||
totalSize += pkg.downloadSize
|
||||
if pkg.downloaded {
|
||||
totalDownloaded += pkg.downloadSize
|
||||
} else if pkg.id == package.id {
|
||||
totalDownloaded += totalBytesWritten
|
||||
currentSpeed = speed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.totalSize = totalSize
|
||||
task.totalDownloadedSize = totalDownloaded
|
||||
task.totalProgress = totalSize > 0 ? Double(totalDownloaded) / Double(totalSize) : 0
|
||||
task.totalSpeed = currentSpeed
|
||||
|
||||
lastUpdateTime = now
|
||||
lastBytes = totalBytesWritten
|
||||
|
||||
networkManager?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
||||
|
||||
Task {
|
||||
let downloadTask: URLSessionDownloadTask
|
||||
if let resumeData = resumeData {
|
||||
downloadTask = session.downloadTask(withResumeData: resumeData)
|
||||
} else if let url = url {
|
||||
var request = URLRequest(url: url)
|
||||
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
downloadTask = session.downloadTask(with: request)
|
||||
} else {
|
||||
continuation.resume(throwing: NetworkError.invalidData("Neither URL nor resume data provided"))
|
||||
return
|
||||
}
|
||||
|
||||
await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||
await cancelTracker.clearResumeData(task.id)
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startDownloadProcess(task: NewDownloadTask) async {
|
||||
actor DownloadProgress {
|
||||
var currentPackageIndex: Int = 0
|
||||
func increment() { currentPackageIndex += 1 }
|
||||
@@ -300,6 +426,7 @@ class DownloadUtils {
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
)))
|
||||
networkManager?.saveTask(task)
|
||||
}
|
||||
|
||||
await progress.increment()
|
||||
@@ -318,10 +445,14 @@ class DownloadUtils {
|
||||
guard let url = URL(string: downloadURL) else { continue }
|
||||
|
||||
do {
|
||||
try await downloadPackage(package: package, task: task, product: product, url: url)
|
||||
if let resumeData = await cancelTracker.getResumeData(task.id) {
|
||||
try await downloadPackage(package: package, task: task, product: product, resumeData: resumeData)
|
||||
} else {
|
||||
try await downloadPackage(package: package, task: task, product: product, url: url)
|
||||
}
|
||||
} catch {
|
||||
print("Error downloading \(package.fullPackageName): \(error.localizedDescription)")
|
||||
await self.handleError(task.id, error)
|
||||
print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)")
|
||||
await handleError(task.id, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -338,128 +469,7 @@ class DownloadUtils {
|
||||
totalTime: Date().timeIntervalSince(task.createAt),
|
||||
totalSize: task.totalSize
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadPackage(package: Package, task: NewDownloadTask, product: ProductsToDownload, url: URL) async throws {
|
||||
var lastUpdateTime = Date()
|
||||
var lastBytes: Int64 = 0
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let delegate = DownloadDelegate(
|
||||
destinationDirectory: task.directory.appendingPathComponent(product.sapCode),
|
||||
fileName: package.fullPackageName,
|
||||
completionHandler: { [weak networkManager] localURL, response, error in
|
||||
if let error = error {
|
||||
if (error as NSError).code == NSURLErrorCancelled {
|
||||
continuation.resume()
|
||||
} else {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
package.downloadedSize = package.downloadSize
|
||||
package.progress = 1.0
|
||||
package.status = .completed
|
||||
package.downloaded = true
|
||||
|
||||
var totalDownloaded: Int64 = 0
|
||||
var totalSize: Int64 = 0
|
||||
|
||||
for prod in task.productsToDownload {
|
||||
for pkg in prod.packages {
|
||||
totalSize += pkg.downloadSize
|
||||
if pkg.downloaded {
|
||||
totalDownloaded += pkg.downloadSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.totalSize = totalSize
|
||||
task.totalDownloadedSize = totalDownloaded
|
||||
task.totalProgress = Double(totalDownloaded) / Double(totalSize)
|
||||
task.totalSpeed = 0
|
||||
|
||||
let allCompleted = task.productsToDownload.allSatisfy {
|
||||
product in product.packages.allSatisfy { $0.downloaded }
|
||||
}
|
||||
|
||||
if allCompleted {
|
||||
task.setStatus(.completed(DownloadStatus.CompletionInfo(
|
||||
timestamp: Date(),
|
||||
totalTime: Date().timeIntervalSince(task.createAt),
|
||||
totalSize: totalSize
|
||||
)))
|
||||
}
|
||||
|
||||
product.updateCompletedPackages()
|
||||
|
||||
networkManager?.objectWillChange.send()
|
||||
}
|
||||
|
||||
continuation.resume()
|
||||
},
|
||||
progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
|
||||
Task { @MainActor in
|
||||
let now = Date()
|
||||
let timeDiff = now.timeIntervalSince(lastUpdateTime)
|
||||
|
||||
if timeDiff >= 1.0 {
|
||||
let bytesDiff = totalBytesWritten - lastBytes
|
||||
let speed = Double(bytesDiff) / timeDiff
|
||||
|
||||
package.updateProgress(
|
||||
downloadedSize: totalBytesWritten,
|
||||
speed: speed
|
||||
)
|
||||
|
||||
var completedSize: Int64 = 0
|
||||
var totalSize: Int64 = 0
|
||||
|
||||
for prod in task.productsToDownload {
|
||||
for pkg in prod.packages {
|
||||
totalSize += pkg.downloadSize
|
||||
if pkg.downloaded {
|
||||
completedSize += pkg.downloadSize
|
||||
} else if pkg.id == package.id {
|
||||
completedSize += totalBytesWritten
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.totalSize = totalSize
|
||||
task.totalDownloadedSize = completedSize
|
||||
task.totalProgress = Double(completedSize) / Double(totalSize)
|
||||
task.totalSpeed = speed
|
||||
|
||||
lastUpdateTime = now
|
||||
lastBytes = totalBytesWritten
|
||||
|
||||
networkManager?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
|
||||
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
||||
|
||||
Task {
|
||||
if let resumeData = await cancelTracker.getResumeData(task.id) {
|
||||
let downloadTask = session.downloadTask(withResumeData: resumeData)
|
||||
await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||
await cancelTracker.clearResumeData(task.id)
|
||||
downloadTask.resume()
|
||||
} else {
|
||||
let downloadTask = session.downloadTask(with: request)
|
||||
await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||
downloadTask.resume()
|
||||
}
|
||||
networkManager?.saveTask(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,7 +794,7 @@ class DownloadUtils {
|
||||
}
|
||||
|
||||
guard let jsonString = String(data: data, encoding: .utf8) else {
|
||||
throw NetworkError.invalidData("无法将响应数据转换为json字符串")
|
||||
throw NetworkError.invalidData(String(localized: "无法将响应数据转换为json字符串"))
|
||||
}
|
||||
|
||||
return jsonString
|
||||
@@ -831,6 +841,7 @@ class DownloadUtils {
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
networkManager?.saveTask(task)
|
||||
networkManager?.updateDockBadge()
|
||||
networkManager?.objectWillChange.send()
|
||||
}
|
||||
@@ -842,26 +853,26 @@ class DownloadUtils {
|
||||
case let networkError as NetworkError:
|
||||
switch networkError {
|
||||
case .noConnection:
|
||||
return ("网络连接已断开", true)
|
||||
return (String(localized: "网络连接已断开"), true)
|
||||
case .timeout:
|
||||
return ("下载超时", true)
|
||||
return (String(localized: "下载超时"), true)
|
||||
case .serverUnreachable:
|
||||
return ("服务器无法访问", true)
|
||||
return (String(localized: "服务器无法访问"), true)
|
||||
case .insufficientStorage:
|
||||
return ("存储空间不足", false)
|
||||
return (String(localized: "存储空间不足"), false)
|
||||
case .filePermissionDenied:
|
||||
return ("没有入权限", false)
|
||||
return (String(localized: "没有写入权限"), false)
|
||||
default:
|
||||
return (networkError.localizedDescription, false)
|
||||
}
|
||||
case let urlError as URLError:
|
||||
switch urlError.code {
|
||||
case .notConnectedToInternet:
|
||||
return ("网络连接已开", true)
|
||||
return (String(localized: "网络连接已断开"), true)
|
||||
case .timedOut:
|
||||
return ("连接超时", true)
|
||||
return (String(localized: "连接超时"), true)
|
||||
case .cancelled:
|
||||
return ("下载已取消", false)
|
||||
return (String(localized: "下载已取消"), false)
|
||||
default:
|
||||
return (urlError.localizedDescription, true)
|
||||
}
|
||||
|
||||
255
Adobe Downloader/Utils/TaskPersistenceManager.swift
Normal file
255
Adobe Downloader/Utils/TaskPersistenceManager.swift
Normal file
@@ -0,0 +1,255 @@
|
||||
import Foundation
|
||||
|
||||
class TaskPersistenceManager {
|
||||
static let shared = TaskPersistenceManager()
|
||||
|
||||
private let fileManager = FileManager.default
|
||||
private var tasksDirectory: URL
|
||||
private weak var cancelTracker: CancelTracker?
|
||||
|
||||
private init() {
|
||||
let containerURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
|
||||
print(tasksDirectory)
|
||||
try? fileManager.createDirectory(at: tasksDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
func setCancelTracker(_ tracker: CancelTracker) {
|
||||
self.cancelTracker = tracker
|
||||
}
|
||||
|
||||
private func getTaskFileName(sapCode: String, version: String, language: String, platform: String) -> String {
|
||||
return sapCode == "APRO"
|
||||
? "Adobe Downloader \(sapCode)_\(version)_\(platform)-task.json"
|
||||
: "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json"
|
||||
}
|
||||
|
||||
func saveTask(_ task: NewDownloadTask) {
|
||||
let fileName = getTaskFileName(
|
||||
sapCode: task.sapCode,
|
||||
version: task.version,
|
||||
language: task.language,
|
||||
platform: task.platform
|
||||
)
|
||||
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
||||
|
||||
var resumeDataDict: [String: Data]? = nil
|
||||
|
||||
Task {
|
||||
if let currentPackage = task.currentPackage,
|
||||
let cancelTracker = self.cancelTracker,
|
||||
let resumeData = await cancelTracker.getResumeData(task.id) {
|
||||
resumeDataDict = [currentPackage.id.uuidString: resumeData]
|
||||
}
|
||||
}
|
||||
|
||||
let taskData = TaskData(
|
||||
sapCode: task.sapCode,
|
||||
version: task.version,
|
||||
language: task.language,
|
||||
displayName: task.displayName,
|
||||
directory: task.directory,
|
||||
productsToDownload: task.productsToDownload.map { product in
|
||||
ProductData(
|
||||
sapCode: product.sapCode,
|
||||
version: product.version,
|
||||
buildGuid: product.buildGuid,
|
||||
applicationJson: product.applicationJson,
|
||||
packages: product.packages.map { package in
|
||||
PackageData(
|
||||
type: package.type,
|
||||
fullPackageName: package.fullPackageName,
|
||||
downloadSize: package.downloadSize,
|
||||
downloadURL: package.downloadURL,
|
||||
downloadedSize: package.downloadedSize,
|
||||
progress: package.progress,
|
||||
speed: package.speed,
|
||||
status: package.status,
|
||||
downloaded: package.downloaded
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
retryCount: task.retryCount,
|
||||
createAt: task.createAt,
|
||||
totalStatus: task.totalStatus ?? .waiting,
|
||||
totalProgress: task.totalProgress,
|
||||
totalDownloadedSize: task.totalDownloadedSize,
|
||||
totalSize: task.totalSize,
|
||||
totalSpeed: task.totalSpeed,
|
||||
displayInstallButton: task.displayInstallButton,
|
||||
platform: task.platform,
|
||||
resumeData: resumeDataDict
|
||||
)
|
||||
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try encoder.encode(taskData)
|
||||
// print("保存数据")
|
||||
try data.write(to: fileURL)
|
||||
} catch {
|
||||
print("Error saving task: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func loadTasks() -> [NewDownloadTask] {
|
||||
var tasks: [NewDownloadTask] = []
|
||||
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(at: tasksDirectory, includingPropertiesForKeys: nil)
|
||||
for file in files where file.pathExtension == "json" {
|
||||
if let task = loadTask(from: file) {
|
||||
tasks.append(task)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error loading tasks: \(error)")
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
private func loadTask(from url: URL) -> NewDownloadTask? {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let decoder = JSONDecoder()
|
||||
let taskData = try decoder.decode(TaskData.self, from: data)
|
||||
|
||||
let products = taskData.productsToDownload.map { productData -> ProductsToDownload in
|
||||
let product = ProductsToDownload(
|
||||
sapCode: productData.sapCode,
|
||||
version: productData.version,
|
||||
buildGuid: productData.buildGuid,
|
||||
applicationJson: productData.applicationJson ?? ""
|
||||
)
|
||||
|
||||
product.packages = productData.packages.map { packageData -> Package in
|
||||
let package = Package(
|
||||
type: packageData.type,
|
||||
fullPackageName: packageData.fullPackageName,
|
||||
downloadSize: packageData.downloadSize,
|
||||
downloadURL: packageData.downloadURL
|
||||
)
|
||||
package.downloadedSize = packageData.downloadedSize
|
||||
package.progress = packageData.progress
|
||||
package.speed = packageData.speed
|
||||
package.status = packageData.status
|
||||
package.downloaded = packageData.downloaded
|
||||
return package
|
||||
}
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
for product in products {
|
||||
for package in product.packages {
|
||||
package.speed = 0
|
||||
}
|
||||
}
|
||||
|
||||
let initialStatus: DownloadStatus
|
||||
switch taskData.totalStatus {
|
||||
case .completed:
|
||||
initialStatus = taskData.totalStatus
|
||||
case .failed:
|
||||
initialStatus = taskData.totalStatus
|
||||
case .downloading:
|
||||
initialStatus = .paused(DownloadStatus.PauseInfo(
|
||||
reason: .other(String(localized: "程序意外退出")),
|
||||
timestamp: Date(),
|
||||
resumable: true
|
||||
))
|
||||
default:
|
||||
initialStatus = .paused(DownloadStatus.PauseInfo(
|
||||
reason: .other(String(localized: "程序重启后自动暂停")),
|
||||
timestamp: Date(),
|
||||
resumable: true
|
||||
))
|
||||
}
|
||||
|
||||
let task = NewDownloadTask(
|
||||
sapCode: taskData.sapCode,
|
||||
version: taskData.version,
|
||||
language: taskData.language,
|
||||
displayName: taskData.displayName,
|
||||
directory: taskData.directory,
|
||||
productsToDownload: products,
|
||||
retryCount: taskData.retryCount,
|
||||
createAt: taskData.createAt,
|
||||
totalStatus: initialStatus,
|
||||
totalProgress: taskData.totalProgress,
|
||||
totalDownloadedSize: taskData.totalDownloadedSize,
|
||||
totalSize: taskData.totalSize,
|
||||
totalSpeed: 0,
|
||||
currentPackage: products.first?.packages.first,
|
||||
platform: taskData.platform
|
||||
)
|
||||
task.displayInstallButton = taskData.displayInstallButton
|
||||
|
||||
if let resumeData = taskData.resumeData?.values.first {
|
||||
Task {
|
||||
if let cancelTracker = self.cancelTracker {
|
||||
await cancelTracker.storeResumeData(task.id, data: resumeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return task
|
||||
} catch {
|
||||
print("Error loading task from \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeTask(_ task: NewDownloadTask) {
|
||||
let fileName = getTaskFileName(
|
||||
sapCode: task.sapCode,
|
||||
version: task.version,
|
||||
language: task.language,
|
||||
platform: task.platform
|
||||
)
|
||||
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
||||
|
||||
try? fileManager.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TaskData: Codable {
|
||||
let sapCode: String
|
||||
let version: String
|
||||
let language: String
|
||||
let displayName: String
|
||||
let directory: URL
|
||||
let productsToDownload: [ProductData]
|
||||
let retryCount: Int
|
||||
let createAt: Date
|
||||
let totalStatus: DownloadStatus
|
||||
let totalProgress: Double
|
||||
let totalDownloadedSize: Int64
|
||||
let totalSize: Int64
|
||||
let totalSpeed: Double
|
||||
let displayInstallButton: Bool
|
||||
let platform: String
|
||||
let resumeData: [String: Data]?
|
||||
}
|
||||
|
||||
private struct ProductData: Codable {
|
||||
let sapCode: String
|
||||
let version: String
|
||||
let buildGuid: String
|
||||
let applicationJson: String?
|
||||
let packages: [PackageData]
|
||||
}
|
||||
|
||||
private struct PackageData: Codable {
|
||||
let type: String
|
||||
let fullPackageName: String
|
||||
let downloadSize: Int64
|
||||
let downloadURL: String
|
||||
let downloadedSize: Int64
|
||||
let progress: Double
|
||||
let speed: Double
|
||||
let status: PackageStatus
|
||||
let downloaded: Bool
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class IconCache {
|
||||
static let shared = IconCache()
|
||||
@@ -47,22 +48,50 @@ class AppCardViewModel: ObservableObject {
|
||||
get { userDefaults.string(forKey: "defaultDirectory") ?? "" }
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(sap: Sap, networkManager: NetworkManager?) {
|
||||
self.sap = sap
|
||||
self.networkManager = networkManager
|
||||
loadIcon()
|
||||
updateDownloadingStatus()
|
||||
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
networkManager?.objectWillChange
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateDownloadingStatus()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func updateDownloadingStatus() {
|
||||
guard let networkManager = networkManager else {
|
||||
Task { @MainActor in
|
||||
self.isDownloading = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
isDownloading = networkManager?.downloadTasks.contains { task in
|
||||
return task.sapCode == sap.sapCode && task.status.isActive
|
||||
} ?? false
|
||||
let isActive = networkManager.downloadTasks.contains { task in
|
||||
task.sapCode == sap.sapCode && isTaskActive(task.status)
|
||||
}
|
||||
self.isDownloading = isActive
|
||||
}
|
||||
}
|
||||
|
||||
private func isTaskActive(_ status: DownloadStatus) -> Bool {
|
||||
switch status {
|
||||
case .downloading, .preparing, .paused, .waiting, .retrying(_):
|
||||
return true
|
||||
case .completed, .failed:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getDestinationURL(version: String, language: String, useDefaultDirectory: Bool, defaultDirectory: String) async throws -> URL {
|
||||
func getDestinationURL(version: String, language: String) async throws -> URL {
|
||||
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
||||
let installerName = sap.sapCode == "APRO"
|
||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
||||
@@ -164,29 +193,17 @@ class AppCardViewModel: ObservableObject {
|
||||
showExistingFileAlert = true
|
||||
}
|
||||
} else {
|
||||
startDownload(version, language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startDownload(_ version: String, _ language: String) {
|
||||
Task {
|
||||
do {
|
||||
let destinationURL = try await getDestinationURL(
|
||||
version: version,
|
||||
language: language,
|
||||
useDefaultDirectory: useDefaultDirectory,
|
||||
defaultDirectory: defaultDirectory
|
||||
)
|
||||
|
||||
try await networkManager?.startDownload(
|
||||
sap: sap,
|
||||
selectedVersion: version,
|
||||
language: language,
|
||||
destinationURL: destinationURL
|
||||
)
|
||||
} catch {
|
||||
handleError(error)
|
||||
do {
|
||||
let destinationURL = try await getDestinationURL(version: version, language: language)
|
||||
try await networkManager.startDownload(
|
||||
sap: sap,
|
||||
selectedVersion: version,
|
||||
language: language,
|
||||
destinationURL: destinationURL
|
||||
)
|
||||
} catch {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,7 +273,8 @@ class AppCardViewModel: ObservableObject {
|
||||
totalProgress: 1.0,
|
||||
totalDownloadedSize: 0,
|
||||
totalSize: 0,
|
||||
totalSpeed: 0
|
||||
totalSpeed: 0,
|
||||
platform: ""
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
@@ -319,6 +337,9 @@ struct AppCardView: View {
|
||||
viewModel.networkManager = networkManager
|
||||
viewModel.updateDownloadingStatus()
|
||||
}
|
||||
.onChange(of: networkManager.downloadTasks) { _ in
|
||||
viewModel.updateDownloadingStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +491,12 @@ struct AlertModifier: ViewModifier {
|
||||
if confirmRedownload {
|
||||
viewModel.showRedownloadConfirm = true
|
||||
} else {
|
||||
viewModel.startDownload(viewModel.pendingVersion, viewModel.pendingLanguage)
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(
|
||||
version: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -487,7 +513,12 @@ struct AlertModifier: ViewModifier {
|
||||
Button("取消", role: .cancel) { }
|
||||
Button("确认") {
|
||||
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
||||
viewModel.startDownload(viewModel.pendingVersion, viewModel.pendingLanguage)
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(
|
||||
version: viewModel.pendingVersion,
|
||||
language: viewModel.pendingLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
@@ -497,7 +528,12 @@ struct AlertModifier: ViewModifier {
|
||||
Button("确定", role: .cancel) { }
|
||||
Button("重试") {
|
||||
if !viewModel.selectedVersion.isEmpty {
|
||||
viewModel.startDownload(viewModel.selectedVersion, viewModel.selectedLanguage)
|
||||
Task {
|
||||
await viewModel.checkAndStartDownload(
|
||||
version: viewModel.selectedVersion,
|
||||
language: viewModel.selectedLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
|
||||
@@ -607,7 +607,8 @@ struct PackageRow: View {
|
||||
totalProgress: 0.45,
|
||||
totalDownloadedSize: 457424883,
|
||||
totalSize: 878454797,
|
||||
totalSpeed: 1024 * 1024 * 2
|
||||
totalSpeed: 1024 * 1024 * 2,
|
||||
platform: ""
|
||||
),
|
||||
onCancel: {},
|
||||
onPause: {},
|
||||
@@ -642,7 +643,8 @@ struct PackageRow: View {
|
||||
totalProgress: 1.0,
|
||||
totalDownloadedSize: 878454797,
|
||||
totalSize: 878454797,
|
||||
totalSpeed: 0
|
||||
totalSpeed: 0,
|
||||
platform: ""
|
||||
),
|
||||
onCancel: {},
|
||||
onPause: {},
|
||||
@@ -677,7 +679,8 @@ struct PackageRow: View {
|
||||
totalProgress: 0.52,
|
||||
totalDownloadedSize: 457424883,
|
||||
totalSize: 878454797,
|
||||
totalSpeed: 0
|
||||
totalSpeed: 0,
|
||||
platform: ""
|
||||
),
|
||||
onCancel: {},
|
||||
onPause: {},
|
||||
|
||||
@@ -218,6 +218,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"下载超时" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Download timeout"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"下载错误" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -418,6 +428,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"存储空间不足" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Insufficient storage space"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"安装" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -619,6 +639,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"无法将响应数据转换为json字符串" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Unable to convert response data to json string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"是否确认重新下载?这将覆盖现有的安装程序。" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -659,6 +689,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"有正在进行的下载任务,确定要退出吗?\n所有下载任务的进度已保存,下次启动可以继续下载" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "There are download tasks in progress. Are you sure you want to quit?\nThe progress of all download tasks has been saved and you can continue downloading next time you start the program."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"服务器响应无效" : {
|
||||
"comment" : "Invalid response",
|
||||
"localizations" : {
|
||||
@@ -670,6 +710,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"服务器无法访问" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Server Unreachable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"未安装 Adobe Creative Cloud" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -818,6 +868,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"没有写入权限" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No write permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"没有找到产品" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -879,6 +939,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"确认退出" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Confirm Exit"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"确认重新下载" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -899,6 +969,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"程序即将退出" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Program is about to exit"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"程序意外退出" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Program quit unexpectedly"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"程序重启后自动暂停" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Automatically pause after program restart"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"等待中" : {
|
||||
"comment" : "Download status waiting",
|
||||
"localizations" : {
|
||||
@@ -942,6 +1042,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"网络连接已断开" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Network connection lost"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"联系 @X1a0He" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -1003,6 +1113,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"连接超时" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Connection timeout"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"退出" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Exit"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"选择" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
34
appcast.xml
34
appcast.xml
@@ -2,6 +2,36 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>Adobe Downloader</title>
|
||||
<item>
|
||||
<title>1.1.0</title>
|
||||
<pubDate>Sat, 09 Nov 2024 23:08:48 +0800</pubDate>
|
||||
<sparkle:version>110</sparkle:version>
|
||||
<sparkle:shortVersionString>1.1.0</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
|
||||
<enclosure url="https://github.com/X1a0He/Adobe-Downloader/releases/download/1.1.0/Adobe.Downloader.dmg"
|
||||
length="2725357" type="application/octet-stream"
|
||||
sparkle:edSignature="kL76crIDDNBgf0fgr30VbQdY28eheD6t0Xz+5K9Gk8N2mc1pOT37ww9SszZSzKwoPyMbnX+KgG5/AWT2NC0FBA=="/>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<style>ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}</style>
|
||||
<h4>Adobe Downloader 更新日志: </h4>
|
||||
<ul>
|
||||
<li>修复了初次启动程序时,默认下载目录为 "Downloads" 导致提示 你不能存储文件“ILST”,因为该宗卷是只读宗卷 的问题</li>
|
||||
<li>新的实现取代了 windowResizability 以适应 macOS 12.0+(可能)</li>
|
||||
<li>新增下载记录持久化功能(M1 Max macOS 15上测试正常,未测试其他机型)</li>
|
||||
</ul>
|
||||
<h4>PS: 此版本改动略大,如有bugs,请及时提出</h4>
|
||||
<hr>
|
||||
<h4>Adobe Downloader Changes: </h4>
|
||||
<ul>
|
||||
<li>Fixed the issue that when launching the program for the first time, the default directory is "Downloads", which causes a download error message</li>
|
||||
<li>New implementation replaces windowResizability to adapt to macOS 12.0+ (Maybe)</li>
|
||||
<li>Added task record persistence(Tested normally on M1 Max macOS 15, other models not tested)</li>
|
||||
</ul>
|
||||
<h4>PS: This version has been slightly changed. If there are any bugs, please report them in time.</h4>
|
||||
]]>
|
||||
</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>1.0.1</title>
|
||||
<pubDate>Thu, 07 Nov 2024 21:29:26 +0800</pubDate>
|
||||
@@ -14,7 +44,7 @@
|
||||
sparkle:edSignature="/paRKw18fjGopMIkrNSPJV1k0NloLccfjeyBLjjbNus7IyjFyGmdTH5ccxcbcXnYuFqozFrtKuBizpTCmNJfBw=="/>
|
||||
<description><
|
||||
|
||||
- 2024-11-07 21:10 Update Log
|
||||
- 2024-11-09 23:00 Update Log
|
||||
|
||||
```markdown
|
||||
1. Support macOS 13.0 and above
|
||||
2. Added Sparkle for checking update
|
||||
3. When the default directory is not selected, the Downloads folder will be used as the default directory
|
||||
4. When installing via Adobe Downloader and encountering permission issues, provide terminal commands to allow users to
|
||||
install by themselves
|
||||
5. Adjusted the UI display of existing files
|
||||
6. Fixed the issue where the number of downloaded packages and total packages was not updated during task download
|
||||
1. Fixed the issue that when launching the program for the first time, the default directory is "Downloads", which
|
||||
causes a download error message
|
||||
2. New implementation replaces windowResizability to adapt to macOS 12.0+ (Maybe)
|
||||
3. Added task record persistence(Tested normally on M1 Max macOS 15, other models not tested)
|
||||
|
||||
PS: This version has been slightly changed. If there are any bugs, please report them in time.
|
||||
```
|
||||
|
||||
### Language friendly
|
||||
@@ -59,6 +58,7 @@ via Telegram.**
|
||||
- [x] Support installation of non-Acrobat products
|
||||
- [x] Support multiple products download at the same time
|
||||
- [x] Supports using default language and default directory
|
||||
- [x] Support task record persistence
|
||||
|
||||
## 👀 Preview
|
||||
|
||||
|
||||
16
readme.md
16
readme.md
@@ -6,7 +6,7 @@
|
||||
|
||||
## 使用须知
|
||||
|
||||
**🍎仅支持 macOS 13.0+**
|
||||
**🍎仅支持 macOS 12.0+**
|
||||
|
||||
> **如果你也喜欢 Adobe Downloader, 或者对你有帮助, 请 Star 仓库吧 🌟, 你的支持是我更新的动力**
|
||||
>
|
||||
@@ -26,15 +26,14 @@
|
||||
|
||||
- 更多关于 App 的更新日志,请查看 [Update Log](update-log.md)
|
||||
|
||||
- 2024-11-07 21:10 更新日志
|
||||
- 2024-11-09 23:00 更新日志
|
||||
|
||||
```markdown
|
||||
1. 修复了当系统版本低于 macOS 14.6 时无法打开程序的问题,现已支持 macOS 13.0 以上
|
||||
2. 增加 Sparkle 用于检测更新
|
||||
3. 当默认目录为 未选择 时,将 下载 文件夹作为默认目录
|
||||
4. 当通过 Adobe Downloader 安装遇到权限问题时,提供终端命令让用户自行安装
|
||||
5. 调整了文件已存在的 UI 显示
|
||||
6. 修复了在任务下载中,已下载包与总包数量不更新的问题
|
||||
1. 修复了初次启动程序时,默认下载目录为 "Downloads" 导致提示 你不能存储文件“ILST”,因为该宗卷是只读宗卷 的问题
|
||||
2. 新的实现取代了 windowResizability 以适应 macOS 12.0+(可能)
|
||||
3. 新增下载记录持久化功能(M1 Max macOS 15上测试正常,未测试其他机型)
|
||||
|
||||
PS: 此版本改动略大,如有bugs,请及时提出
|
||||
```
|
||||
|
||||
### 语言支持
|
||||
@@ -56,6 +55,7 @@
|
||||
- [x] 支持安装非 Acrobat 产品
|
||||
- [x] 支持多个产品同时下载
|
||||
- [x] 支持使用默认语言和默认目录
|
||||
- [x] 支持任务记录持久化
|
||||
|
||||
## 👀 预览
|
||||
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
# Change Log
|
||||
|
||||
- 2024-11-09 23:00 更新日志
|
||||
|
||||
[//]: # (1.1.0)
|
||||
|
||||
```markdown
|
||||
1. 修复了初次启动程序时,默认下载目录为 "Downloads" 导致提示 你不能存储文件“ILST”,因为该宗卷是只读宗卷 的问题
|
||||
2. 新的实现取代了 windowResizability 以适应 macOS 12.0+(可能)
|
||||
3. 新增下载记录持久化功能(M1 Max macOS 15上测试正常,未测试其他机型)
|
||||
|
||||
PS: 此版本改动略大,如有bugs,请及时提出
|
||||
====================
|
||||
|
||||
1. Fixed the issue that when launching the program for the first time, the default directory is "Downloads", which
|
||||
causes a download error message
|
||||
2. New implementation replaces windowResizability to adapt to macOS 12.0+ (Maybe)
|
||||
3. Added task record persistence(Tested normally on M1 Max macOS 15, other models not tested)
|
||||
|
||||
PS: This version has been slightly changed. If there are any bugs, please report them in time.
|
||||
```
|
||||
|
||||
- 2024-11-07 21:10 更新日志
|
||||
|
||||
[//]: # (1.0.1)
|
||||
|
||||
```markdown
|
||||
1. 修复了当系统版本低于 macOS 14.6 时无法打开程序的问题,现已支持 macOS 13.0 以上
|
||||
2. 增加 Sparkle 用于检测更新
|
||||
@@ -21,6 +43,14 @@
|
||||
6. Fixed the issue where the number of downloaded packages and total packages was not updated during task download
|
||||
```
|
||||
|
||||
<img width="1064" alt="image" src="https://github.com/user-attachments/assets/84f3f1de-a429-45ca-9b29-948234b4fcdb">
|
||||
|
||||
<img width="530" alt="image" src="https://github.com/user-attachments/assets/7a22ea27-449b-42cf-8142-fce1215c5d12">
|
||||
|
||||
<img width="427" alt="image" src="https://github.com/user-attachments/assets/403b20db-4014-4645-8833-3616390b17fb">
|
||||
|
||||
<img width="880" alt="image" src="https://github.com/user-attachments/assets/b6b04cd9-bfdf-4cdd-b14c-6dcd48b376a7">
|
||||
|
||||
- 2024-11-06 15:50 更新日志
|
||||
|
||||
```markdown
|
||||
|
||||
Reference in New Issue
Block a user