Add: Support task record persistence.

This commit is contained in:
X1a0He
2024-11-09 23:15:50 +08:00
parent 889755493b
commit 6de8104084
19 changed files with 1052 additions and 331 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,6 +65,7 @@ struct ContentView: View {
Image(systemName: "arrow.down.circle")
.imageScale(.medium)
}
.disabled(isRefreshing)
.buttonStyle(.borderless)
.overlay(
Group {

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

@@ -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: {},

View File

@@ -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" : {

View File

@@ -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><![CDATA[
<style>ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}</style>
<h5>Adobe Downloader 更新日志: </h1>
<h5>Adobe Downloader 更新日志: </h5>
<ul>
<li>修复了当系统版本低于 macOS 14.6 时无法打开程序的问题,现已支持 macOS 13.0 以上</li>
<li>增加 Sparkle 用于检测更新</li>
@@ -24,7 +54,7 @@
<li>修复了在任务下载中,已下载包与总包数量不更新的问题</li>
</ul>
<hr>
<h5>Adobe Downloader Changes: </h1>
<h5>Adobe Downloader Changes: </h5>
<ul>
<li>Support macOS 13.0 and above</li>
<li>Added Sparkle for checking update</li>

View File

@@ -6,7 +6,7 @@
## Before Use
**🍎Only for macOS 13.0+.**
**🍎Only for macOS 12.0+.**
> **If you like Adobe Downloader, or it helps you, please Star🌟 it.**
>
@@ -27,16 +27,15 @@
- For historical update logs, please go to [Update Log](update-log.md)
- 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

View File

@@ -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] 支持任务记录持久化
## 👀 预览

View File

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