mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 19:27:35 +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_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 102;
|
CURRENT_PROJECT_VERSION = 110;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 102;
|
CURRENT_PROJECT_VERSION = 110;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
|
filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
|
||||||
startingColumnNumber = "9223372036854775807"
|
startingColumnNumber = "9223372036854775807"
|
||||||
endingColumnNumber = "9223372036854775807"
|
endingColumnNumber = "9223372036854775807"
|
||||||
startingLineNumber = "468"
|
startingLineNumber = "463"
|
||||||
endingLineNumber = "468"
|
endingLineNumber = "463"
|
||||||
landmarkName = "retryPackage(task:package:)"
|
landmarkName = "startDownloadProcess(task:)"
|
||||||
landmarkType = "7">
|
landmarkType = "7">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ struct Adobe_DownloaderApp: App {
|
|||||||
.frame(width: 850, height: 800)
|
.frame(width: 850, height: 800)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
appDelegate.networkManager = networkManager
|
||||||
|
|
||||||
|
networkManager.loadSavedTasks()
|
||||||
|
|
||||||
checkCreativeCloudSetup()
|
checkCreativeCloudSetup()
|
||||||
|
|
||||||
if ModifySetup.checkSetupBackup() {
|
if ModifySetup.checkSetupBackup() {
|
||||||
|
|||||||
@@ -2,7 +2,78 @@ import Cocoa
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
private var eventMonitor: Any?
|
||||||
|
var networkManager: NetworkManager?
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
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 Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum PackageStatus: Equatable {
|
enum PackageStatus: Equatable, Codable {
|
||||||
case waiting
|
case waiting
|
||||||
case downloading
|
case downloading
|
||||||
case paused
|
case paused
|
||||||
@@ -165,7 +165,7 @@ enum NetworkError: Error, LocalizedError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DownloadStatus: Equatable {
|
enum DownloadStatus: Equatable, Codable {
|
||||||
case waiting
|
case waiting
|
||||||
case preparing(PrepareInfo)
|
case preparing(PrepareInfo)
|
||||||
case downloading(DownloadInfo)
|
case downloading(DownloadInfo)
|
||||||
@@ -174,12 +174,12 @@ enum DownloadStatus: Equatable {
|
|||||||
case failed(FailureInfo)
|
case failed(FailureInfo)
|
||||||
case retrying(RetryInfo)
|
case retrying(RetryInfo)
|
||||||
|
|
||||||
struct PrepareInfo {
|
struct PrepareInfo: Codable {
|
||||||
let message: String
|
let message: String
|
||||||
let timestamp: Date
|
let timestamp: Date
|
||||||
let stage: PrepareStage
|
let stage: PrepareStage
|
||||||
|
|
||||||
enum PrepareStage {
|
enum PrepareStage: Codable {
|
||||||
case initializing
|
case initializing
|
||||||
case creatingInstaller
|
case creatingInstaller
|
||||||
case signingApp
|
case signingApp
|
||||||
@@ -188,7 +188,7 @@ enum DownloadStatus: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DownloadInfo {
|
struct DownloadInfo: Codable {
|
||||||
let fileName: String
|
let fileName: String
|
||||||
let currentPackageIndex: Int
|
let currentPackageIndex: Int
|
||||||
let totalPackages: Int
|
let totalPackages: Int
|
||||||
@@ -196,12 +196,12 @@ enum DownloadStatus: Equatable {
|
|||||||
let estimatedTimeRemaining: TimeInterval?
|
let estimatedTimeRemaining: TimeInterval?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PauseInfo {
|
struct PauseInfo: Codable {
|
||||||
let reason: PauseReason
|
let reason: PauseReason
|
||||||
let timestamp: Date
|
let timestamp: Date
|
||||||
let resumable: Bool
|
let resumable: Bool
|
||||||
|
|
||||||
enum PauseReason {
|
enum PauseReason: Codable {
|
||||||
case userRequested
|
case userRequested
|
||||||
case networkIssue
|
case networkIssue
|
||||||
case systemSleep
|
case systemSleep
|
||||||
@@ -209,26 +209,124 @@ enum DownloadStatus: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CompletionInfo {
|
struct CompletionInfo: Codable {
|
||||||
let timestamp: Date
|
let timestamp: Date
|
||||||
let totalTime: TimeInterval
|
let totalTime: TimeInterval
|
||||||
let totalSize: Int64
|
let totalSize: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FailureInfo {
|
struct FailureInfo: Codable {
|
||||||
let message: String
|
let message: String
|
||||||
let error: Error?
|
let error: Error?
|
||||||
let timestamp: Date
|
let timestamp: Date
|
||||||
let recoverable: Bool
|
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 attempt: Int
|
||||||
let maxAttempts: Int
|
let maxAttempts: Int
|
||||||
let reason: String
|
let reason: String
|
||||||
let nextRetryDate: Date
|
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 {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .waiting:
|
case .waiting:
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ struct ContentView: View {
|
|||||||
Image(systemName: "arrow.down.circle")
|
Image(systemName: "arrow.down.circle")
|
||||||
.imageScale(.medium)
|
.imageScale(.medium)
|
||||||
}
|
}
|
||||||
|
.disabled(isRefreshing)
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.overlay(
|
.overlay(
|
||||||
Group {
|
Group {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class NewDownloadTask: Identifiable, ObservableObject, Equatable {
|
|||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let platform: String
|
||||||
|
|
||||||
var status: DownloadStatus {
|
var status: DownloadStatus {
|
||||||
totalStatus ?? .waiting
|
totalStatus ?? .waiting
|
||||||
@@ -88,7 +89,7 @@ class NewDownloadTask: Identifiable, ObservableObject, Equatable {
|
|||||||
objectWillChange.send()
|
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.sapCode = sapCode
|
||||||
self.version = version
|
self.version = version
|
||||||
self.language = language
|
self.language = language
|
||||||
@@ -104,6 +105,7 @@ class NewDownloadTask: Identifiable, ObservableObject, Equatable {
|
|||||||
self.totalSpeed = totalSpeed
|
self.totalSpeed = totalSpeed
|
||||||
self.currentPackage = currentPackage
|
self.currentPackage = currentPackage
|
||||||
self.displayInstallButton = sapCode != "APRO"
|
self.displayInstallButton = sapCode != "APRO"
|
||||||
|
self.platform = platform
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool {
|
static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool {
|
||||||
|
|||||||
@@ -40,12 +40,18 @@ class NetworkManager: ObservableObject {
|
|||||||
case failed(Error)
|
case failed(Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
private let networkService: NetworkService
|
||||||
|
|
||||||
|
init(networkService: NetworkService = NetworkService(),
|
||||||
|
downloadUtils: DownloadUtils? = nil) {
|
||||||
let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon")
|
let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon")
|
||||||
self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
||||||
|
|
||||||
self.downloadUtils = DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
self.networkService = networkService
|
||||||
setupNetworkMonitoring()
|
self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
||||||
|
|
||||||
|
TaskPersistenceManager.shared.setCancelTracker(cancelTracker)
|
||||||
|
configureNetworkMonitor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchProducts() async {
|
func fetchProducts() async {
|
||||||
@@ -64,15 +70,21 @@ class NetworkManager: ObservableObject {
|
|||||||
directory: destinationURL,
|
directory: destinationURL,
|
||||||
productsToDownload: [],
|
productsToDownload: [],
|
||||||
createAt: Date(),
|
createAt: Date(),
|
||||||
totalStatus: .preparing(DownloadStatus.PrepareInfo(message: "正在准备下载...", timestamp: Date(), stage: .initializing)),
|
totalStatus: .preparing(DownloadStatus.PrepareInfo(
|
||||||
|
message: "正在准备下载...",
|
||||||
|
timestamp: Date(),
|
||||||
|
stage: .initializing
|
||||||
|
)),
|
||||||
totalProgress: 0,
|
totalProgress: 0,
|
||||||
totalDownloadedSize: 0,
|
totalDownloadedSize: 0,
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
totalSpeed: 0
|
totalSpeed: 0,
|
||||||
|
platform: productInfo.apPlatform
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadTasks.append(task)
|
downloadTasks.append(task)
|
||||||
updateDockBadge()
|
updateDockBadge()
|
||||||
|
saveTask(task)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: allowedPlatform, saps: saps)
|
try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: allowedPlatform, saps: saps)
|
||||||
@@ -84,6 +96,7 @@ class NetworkManager: ObservableObject {
|
|||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
recoverable: true
|
recoverable: true
|
||||||
)))
|
)))
|
||||||
|
saveTask(task)
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
@@ -101,10 +114,22 @@ class NetworkManager: ObservableObject {
|
|||||||
await cancelTracker.cancel(taskId)
|
await cancelTracker.cancel(taskId)
|
||||||
|
|
||||||
if let task = downloadTasks.first(where: { $0.id == 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 {
|
if removeFiles {
|
||||||
try? FileManager.default.removeItem(at: task.directory)
|
try? FileManager.default.removeItem(at: task.directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskPersistenceManager.shared.removeTask(task)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
downloadTasks.removeAll { $0.id == taskId }
|
downloadTasks.removeAll { $0.id == taskId }
|
||||||
updateDockBadge()
|
updateDockBadge()
|
||||||
@@ -125,7 +150,7 @@ class NetworkManager: ObservableObject {
|
|||||||
|
|
||||||
while retryCount < maxRetries {
|
while retryCount < maxRetries {
|
||||||
do {
|
do {
|
||||||
let (saps, cdn, sapCodes) = try await fetchProductsData()
|
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.saps = saps
|
self.saps = saps
|
||||||
@@ -273,97 +298,17 @@ class NetworkManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getApplicationInfo(buildGuid: String) async throws -> String {
|
func getApplicationInfo(buildGuid: String) async throws -> String {
|
||||||
guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
|
return try await networkService.getApplicationInfo(buildGuid: buildGuid)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isVersionDownloaded(sap: Sap, version: String, language: String) -> URL? {
|
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 platform = sap.versions[version]?.apPlatform ?? "unknown"
|
||||||
let fileName = sap.sapCode == "APRO"
|
let fileName = sap.sapCode == "APRO"
|
||||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
? "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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,10 +340,6 @@ class NetworkManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupNetworkMonitoring() {
|
|
||||||
configureNetworkMonitor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func retryFetchData() {
|
func retryFetchData() {
|
||||||
Task {
|
Task {
|
||||||
isFetchingProducts = false
|
isFetchingProducts = false
|
||||||
@@ -420,4 +351,19 @@ class NetworkManager: ObservableObject {
|
|||||||
func updateAllowedPlatform(useAppleSilicon: Bool) {
|
func updateAllowedPlatform(useAppleSilicon: Bool) {
|
||||||
allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
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 {
|
actor CancelTracker {
|
||||||
private var cancelledIds: Set<UUID> = []
|
private var cancelledIds: Set<UUID> = []
|
||||||
private var pausedIds: Set<UUID> = []
|
private var pausedIds: Set<UUID> = []
|
||||||
private var downloadTasks: [UUID: URLSessionDownloadTask] = [:]
|
var downloadTasks: [UUID: URLSessionDownloadTask] = [:]
|
||||||
private var sessions: [UUID: URLSession] = [:]
|
private var sessions: [UUID: URLSession] = [:]
|
||||||
private var resumeData: [UUID: Data] = [:]
|
private var resumeData: [UUID: Data] = [:]
|
||||||
|
|
||||||
@@ -64,4 +64,8 @@ actor CancelTracker {
|
|||||||
func isPaused(_ id: UUID) -> Bool {
|
func isPaused(_ id: UUID) -> Bool {
|
||||||
return pausedIds.contains(id)
|
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 {
|
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 {
|
await MainActor.run {
|
||||||
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
||||||
task.setStatus(.paused(DownloadStatus.PauseInfo(
|
task.setStatus(.paused(DownloadStatus.PauseInfo(
|
||||||
@@ -110,24 +122,12 @@ class DownloadUtils {
|
|||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
resumable: true
|
resumable: true
|
||||||
)))
|
)))
|
||||||
|
networkManager?.saveTask(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await cancelTracker.pause(taskId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resumeDownloadTask(taskId: UUID) async {
|
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 }) {
|
if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
||||||
await startDownloadProcess(task: task)
|
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 {
|
actor DownloadProgress {
|
||||||
var currentPackageIndex: Int = 0
|
var currentPackageIndex: Int = 0
|
||||||
func increment() { currentPackageIndex += 1 }
|
func increment() { currentPackageIndex += 1 }
|
||||||
@@ -300,6 +426,7 @@ class DownloadUtils {
|
|||||||
startTime: Date(),
|
startTime: Date(),
|
||||||
estimatedTimeRemaining: nil
|
estimatedTimeRemaining: nil
|
||||||
)))
|
)))
|
||||||
|
networkManager?.saveTask(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
await progress.increment()
|
await progress.increment()
|
||||||
@@ -318,10 +445,14 @@ class DownloadUtils {
|
|||||||
guard let url = URL(string: downloadURL) else { continue }
|
guard let url = URL(string: downloadURL) else { continue }
|
||||||
|
|
||||||
do {
|
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 {
|
} catch {
|
||||||
print("Error downloading \(package.fullPackageName): \(error.localizedDescription)")
|
print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)")
|
||||||
await self.handleError(task.id, error)
|
await handleError(task.id, error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,128 +469,7 @@ class DownloadUtils {
|
|||||||
totalTime: Date().timeIntervalSince(task.createAt),
|
totalTime: Date().timeIntervalSince(task.createAt),
|
||||||
totalSize: task.totalSize
|
totalSize: task.totalSize
|
||||||
)))
|
)))
|
||||||
}
|
networkManager?.saveTask(task)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -784,7 +794,7 @@ class DownloadUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let jsonString = String(data: data, encoding: .utf8) else {
|
guard let jsonString = String(data: data, encoding: .utf8) else {
|
||||||
throw NetworkError.invalidData("无法将响应数据转换为json字符串")
|
throw NetworkError.invalidData(String(localized: "无法将响应数据转换为json字符串"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonString
|
return jsonString
|
||||||
@@ -831,6 +841,7 @@ class DownloadUtils {
|
|||||||
try? FileManager.default.removeItem(at: fileURL)
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
networkManager?.saveTask(task)
|
||||||
networkManager?.updateDockBadge()
|
networkManager?.updateDockBadge()
|
||||||
networkManager?.objectWillChange.send()
|
networkManager?.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@@ -842,26 +853,26 @@ class DownloadUtils {
|
|||||||
case let networkError as NetworkError:
|
case let networkError as NetworkError:
|
||||||
switch networkError {
|
switch networkError {
|
||||||
case .noConnection:
|
case .noConnection:
|
||||||
return ("网络连接已断开", true)
|
return (String(localized: "网络连接已断开"), true)
|
||||||
case .timeout:
|
case .timeout:
|
||||||
return ("下载超时", true)
|
return (String(localized: "下载超时"), true)
|
||||||
case .serverUnreachable:
|
case .serverUnreachable:
|
||||||
return ("服务器无法访问", true)
|
return (String(localized: "服务器无法访问"), true)
|
||||||
case .insufficientStorage:
|
case .insufficientStorage:
|
||||||
return ("存储空间不足", false)
|
return (String(localized: "存储空间不足"), false)
|
||||||
case .filePermissionDenied:
|
case .filePermissionDenied:
|
||||||
return ("没有入权限", false)
|
return (String(localized: "没有写入权限"), false)
|
||||||
default:
|
default:
|
||||||
return (networkError.localizedDescription, false)
|
return (networkError.localizedDescription, false)
|
||||||
}
|
}
|
||||||
case let urlError as URLError:
|
case let urlError as URLError:
|
||||||
switch urlError.code {
|
switch urlError.code {
|
||||||
case .notConnectedToInternet:
|
case .notConnectedToInternet:
|
||||||
return ("网络连接已开", true)
|
return (String(localized: "网络连接已断开"), true)
|
||||||
case .timedOut:
|
case .timedOut:
|
||||||
return ("连接超时", true)
|
return (String(localized: "连接超时"), true)
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
return ("下载已取消", false)
|
return (String(localized: "下载已取消"), false)
|
||||||
default:
|
default:
|
||||||
return (urlError.localizedDescription, true)
|
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 SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
class IconCache {
|
class IconCache {
|
||||||
static let shared = IconCache()
|
static let shared = IconCache()
|
||||||
@@ -47,22 +48,50 @@ class AppCardViewModel: ObservableObject {
|
|||||||
get { userDefaults.string(forKey: "defaultDirectory") ?? "" }
|
get { userDefaults.string(forKey: "defaultDirectory") ?? "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(sap: Sap, networkManager: NetworkManager?) {
|
init(sap: Sap, networkManager: NetworkManager?) {
|
||||||
self.sap = sap
|
self.sap = sap
|
||||||
self.networkManager = networkManager
|
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() {
|
func updateDownloadingStatus() {
|
||||||
|
guard let networkManager = networkManager else {
|
||||||
|
Task { @MainActor in
|
||||||
|
self.isDownloading = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
isDownloading = networkManager?.downloadTasks.contains { task in
|
let isActive = networkManager.downloadTasks.contains { task in
|
||||||
return task.sapCode == sap.sapCode && task.status.isActive
|
task.sapCode == sap.sapCode && isTaskActive(task.status)
|
||||||
} ?? false
|
}
|
||||||
|
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 platform = sap.versions[version]?.apPlatform ?? "unknown"
|
||||||
let installerName = sap.sapCode == "APRO"
|
let installerName = sap.sapCode == "APRO"
|
||||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
||||||
@@ -164,29 +193,17 @@ class AppCardViewModel: ObservableObject {
|
|||||||
showExistingFileAlert = true
|
showExistingFileAlert = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startDownload(version, language)
|
do {
|
||||||
}
|
let destinationURL = try await getDestinationURL(version: version, language: language)
|
||||||
}
|
try await networkManager.startDownload(
|
||||||
}
|
sap: sap,
|
||||||
|
selectedVersion: version,
|
||||||
func startDownload(_ version: String, _ language: String) {
|
language: language,
|
||||||
Task {
|
destinationURL: destinationURL
|
||||||
do {
|
)
|
||||||
let destinationURL = try await getDestinationURL(
|
} catch {
|
||||||
version: version,
|
handleError(error)
|
||||||
language: language,
|
}
|
||||||
useDefaultDirectory: useDefaultDirectory,
|
|
||||||
defaultDirectory: defaultDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
totalProgress: 1.0,
|
||||||
totalDownloadedSize: 0,
|
totalDownloadedSize: 0,
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
totalSpeed: 0
|
totalSpeed: 0,
|
||||||
|
platform: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -319,6 +337,9 @@ struct AppCardView: View {
|
|||||||
viewModel.networkManager = networkManager
|
viewModel.networkManager = networkManager
|
||||||
viewModel.updateDownloadingStatus()
|
viewModel.updateDownloadingStatus()
|
||||||
}
|
}
|
||||||
|
.onChange(of: networkManager.downloadTasks) { _ in
|
||||||
|
viewModel.updateDownloadingStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +491,12 @@ struct AlertModifier: ViewModifier {
|
|||||||
if confirmRedownload {
|
if confirmRedownload {
|
||||||
viewModel.showRedownloadConfirm = true
|
viewModel.showRedownloadConfirm = true
|
||||||
} else {
|
} 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("取消", role: .cancel) { }
|
||||||
Button("确认") {
|
Button("确认") {
|
||||||
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
||||||
viewModel.startDownload(viewModel.pendingVersion, viewModel.pendingLanguage)
|
Task {
|
||||||
|
await viewModel.checkAndStartDownload(
|
||||||
|
version: viewModel.pendingVersion,
|
||||||
|
language: viewModel.pendingLanguage
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
@@ -497,7 +528,12 @@ struct AlertModifier: ViewModifier {
|
|||||||
Button("确定", role: .cancel) { }
|
Button("确定", role: .cancel) { }
|
||||||
Button("重试") {
|
Button("重试") {
|
||||||
if !viewModel.selectedVersion.isEmpty {
|
if !viewModel.selectedVersion.isEmpty {
|
||||||
viewModel.startDownload(viewModel.selectedVersion, viewModel.selectedLanguage)
|
Task {
|
||||||
|
await viewModel.checkAndStartDownload(
|
||||||
|
version: viewModel.selectedVersion,
|
||||||
|
language: viewModel.selectedLanguage
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
|
|||||||
@@ -607,7 +607,8 @@ struct PackageRow: View {
|
|||||||
totalProgress: 0.45,
|
totalProgress: 0.45,
|
||||||
totalDownloadedSize: 457424883,
|
totalDownloadedSize: 457424883,
|
||||||
totalSize: 878454797,
|
totalSize: 878454797,
|
||||||
totalSpeed: 1024 * 1024 * 2
|
totalSpeed: 1024 * 1024 * 2,
|
||||||
|
platform: ""
|
||||||
),
|
),
|
||||||
onCancel: {},
|
onCancel: {},
|
||||||
onPause: {},
|
onPause: {},
|
||||||
@@ -642,7 +643,8 @@ struct PackageRow: View {
|
|||||||
totalProgress: 1.0,
|
totalProgress: 1.0,
|
||||||
totalDownloadedSize: 878454797,
|
totalDownloadedSize: 878454797,
|
||||||
totalSize: 878454797,
|
totalSize: 878454797,
|
||||||
totalSpeed: 0
|
totalSpeed: 0,
|
||||||
|
platform: ""
|
||||||
),
|
),
|
||||||
onCancel: {},
|
onCancel: {},
|
||||||
onPause: {},
|
onPause: {},
|
||||||
@@ -677,7 +679,8 @@ struct PackageRow: View {
|
|||||||
totalProgress: 0.52,
|
totalProgress: 0.52,
|
||||||
totalDownloadedSize: 457424883,
|
totalDownloadedSize: 457424883,
|
||||||
totalSize: 878454797,
|
totalSize: 878454797,
|
||||||
totalSpeed: 0
|
totalSpeed: 0,
|
||||||
|
platform: ""
|
||||||
),
|
),
|
||||||
onCancel: {},
|
onCancel: {},
|
||||||
onPause: {},
|
onPause: {},
|
||||||
|
|||||||
@@ -218,6 +218,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"下载超时" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Download timeout"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"下载错误" : {
|
"下载错误" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -418,6 +428,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"存储空间不足" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Insufficient storage space"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"安装" : {
|
"安装" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -619,6 +639,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"无法将响应数据转换为json字符串" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Unable to convert response data to json string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"是否确认重新下载?这将覆盖现有的安装程序。" : {
|
"是否确认重新下载?这将覆盖现有的安装程序。" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"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",
|
"comment" : "Invalid response",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -670,6 +710,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"服务器无法访问" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Server Unreachable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"未安装 Adobe Creative Cloud" : {
|
"未安装 Adobe Creative Cloud" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -818,6 +868,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"没有写入权限" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "No write permission"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"没有找到产品" : {
|
"没有找到产品" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -879,6 +939,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"确认退出" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Confirm Exit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"确认重新下载" : {
|
"确认重新下载" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"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",
|
"comment" : "Download status waiting",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -942,6 +1042,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"网络连接已断开" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Network connection lost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"联系 @X1a0He" : {
|
"联系 @X1a0He" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -1003,6 +1113,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"连接超时" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Connection timeout"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"退出" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Exit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"选择" : {
|
"选择" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
34
appcast.xml
34
appcast.xml
@@ -2,6 +2,36 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Adobe Downloader</title>
|
<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>
|
<item>
|
||||||
<title>1.0.1</title>
|
<title>1.0.1</title>
|
||||||
<pubDate>Thu, 07 Nov 2024 21:29:26 +0800</pubDate>
|
<pubDate>Thu, 07 Nov 2024 21:29:26 +0800</pubDate>
|
||||||
@@ -14,7 +44,7 @@
|
|||||||
sparkle:edSignature="/paRKw18fjGopMIkrNSPJV1k0NloLccfjeyBLjjbNus7IyjFyGmdTH5ccxcbcXnYuFqozFrtKuBizpTCmNJfBw=="/>
|
sparkle:edSignature="/paRKw18fjGopMIkrNSPJV1k0NloLccfjeyBLjjbNus7IyjFyGmdTH5ccxcbcXnYuFqozFrtKuBizpTCmNJfBw=="/>
|
||||||
<description><
|
- 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
|
```markdown
|
||||||
1. Support macOS 13.0 and above
|
1. Fixed the issue that when launching the program for the first time, the default directory is "Downloads", which
|
||||||
2. Added Sparkle for checking update
|
causes a download error message
|
||||||
3. When the default directory is not selected, the Downloads folder will be used as the default directory
|
2. New implementation replaces windowResizability to adapt to macOS 12.0+ (Maybe)
|
||||||
4. When installing via Adobe Downloader and encountering permission issues, provide terminal commands to allow users to
|
3. Added task record persistence(Tested normally on M1 Max macOS 15, other models not tested)
|
||||||
install by themselves
|
|
||||||
5. Adjusted the UI display of existing files
|
PS: This version has been slightly changed. If there are any bugs, please report them in time.
|
||||||
6. Fixed the issue where the number of downloaded packages and total packages was not updated during task download
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Language friendly
|
### Language friendly
|
||||||
@@ -59,6 +58,7 @@ via Telegram.**
|
|||||||
- [x] Support installation of non-Acrobat products
|
- [x] Support installation of non-Acrobat products
|
||||||
- [x] Support multiple products download at the same time
|
- [x] Support multiple products download at the same time
|
||||||
- [x] Supports using default language and default directory
|
- [x] Supports using default language and default directory
|
||||||
|
- [x] Support task record persistence
|
||||||
|
|
||||||
## 👀 Preview
|
## 👀 Preview
|
||||||
|
|
||||||
|
|||||||
16
readme.md
16
readme.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 使用须知
|
## 使用须知
|
||||||
|
|
||||||
**🍎仅支持 macOS 13.0+**
|
**🍎仅支持 macOS 12.0+**
|
||||||
|
|
||||||
> **如果你也喜欢 Adobe Downloader, 或者对你有帮助, 请 Star 仓库吧 🌟, 你的支持是我更新的动力**
|
> **如果你也喜欢 Adobe Downloader, 或者对你有帮助, 请 Star 仓库吧 🌟, 你的支持是我更新的动力**
|
||||||
>
|
>
|
||||||
@@ -26,15 +26,14 @@
|
|||||||
|
|
||||||
- 更多关于 App 的更新日志,请查看 [Update Log](update-log.md)
|
- 更多关于 App 的更新日志,请查看 [Update Log](update-log.md)
|
||||||
|
|
||||||
- 2024-11-07 21:10 更新日志
|
- 2024-11-09 23:00 更新日志
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
1. 修复了当系统版本低于 macOS 14.6 时无法打开程序的问题,现已支持 macOS 13.0 以上
|
1. 修复了初次启动程序时,默认下载目录为 "Downloads" 导致提示 你不能存储文件“ILST”,因为该宗卷是只读宗卷 的问题
|
||||||
2. 增加 Sparkle 用于检测更新
|
2. 新的实现取代了 windowResizability 以适应 macOS 12.0+(可能)
|
||||||
3. 当默认目录为 未选择 时,将 下载 文件夹作为默认目录
|
3. 新增下载记录持久化功能(M1 Max macOS 15上测试正常,未测试其他机型)
|
||||||
4. 当通过 Adobe Downloader 安装遇到权限问题时,提供终端命令让用户自行安装
|
|
||||||
5. 调整了文件已存在的 UI 显示
|
PS: 此版本改动略大,如有bugs,请及时提出
|
||||||
6. 修复了在任务下载中,已下载包与总包数量不更新的问题
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 语言支持
|
### 语言支持
|
||||||
@@ -56,6 +55,7 @@
|
|||||||
- [x] 支持安装非 Acrobat 产品
|
- [x] 支持安装非 Acrobat 产品
|
||||||
- [x] 支持多个产品同时下载
|
- [x] 支持多个产品同时下载
|
||||||
- [x] 支持使用默认语言和默认目录
|
- [x] 支持使用默认语言和默认目录
|
||||||
|
- [x] 支持任务记录持久化
|
||||||
|
|
||||||
## 👀 预览
|
## 👀 预览
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,29 @@
|
|||||||
# Change Log
|
# 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 更新日志
|
- 2024-11-07 21:10 更新日志
|
||||||
|
|
||||||
|
[//]: # (1.0.1)
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
1. 修复了当系统版本低于 macOS 14.6 时无法打开程序的问题,现已支持 macOS 13.0 以上
|
1. 修复了当系统版本低于 macOS 14.6 时无法打开程序的问题,现已支持 macOS 13.0 以上
|
||||||
2. 增加 Sparkle 用于检测更新
|
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
|
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 更新日志
|
- 2024-11-06 15:50 更新日志
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
|||||||
Reference in New Issue
Block a user