mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 11:18:53 +08:00
feat: 增加自定义包下载(未完善)
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
shouldAutocreateTestPlan = "YES">
|
shouldAutocreateTestPlan = "YES">
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ class Package: Identifiable, ObservableObject, Codable {
|
|||||||
@Published var status: PackageStatus = .waiting
|
@Published var status: PackageStatus = .waiting
|
||||||
@Published var downloaded: Bool = false
|
@Published var downloaded: Bool = false
|
||||||
|
|
||||||
|
@Published var isSelected: Bool = false
|
||||||
|
var isRequired: Bool = false
|
||||||
|
var condition: String = ""
|
||||||
|
|
||||||
var lastUpdated: Date = Date()
|
var lastUpdated: Date = Date()
|
||||||
var lastRecordedSize: Int64 = 0
|
var lastRecordedSize: Int64 = 0
|
||||||
var retryCount: Int = 0
|
var retryCount: Int = 0
|
||||||
@@ -221,12 +225,18 @@ class Package: Identifiable, ObservableObject, Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String) {
|
init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String, condition: String = "", isRequired: Bool = false) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.fullPackageName = fullPackageName
|
self.fullPackageName = fullPackageName
|
||||||
self.downloadSize = downloadSize
|
self.downloadSize = downloadSize
|
||||||
self.downloadURL = downloadURL
|
self.downloadURL = downloadURL
|
||||||
self.packageVersion = packageVersion
|
self.packageVersion = packageVersion
|
||||||
|
self.condition = condition
|
||||||
|
self.isRequired = isRequired
|
||||||
|
self.isSelected = isRequired
|
||||||
|
if !isRequired {
|
||||||
|
self.isSelected = shouldBeSelectedByDefault
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateProgress(downloadedSize: Int64, speed: Double) {
|
func updateProgress(downloadedSize: Int64, speed: Double) {
|
||||||
@@ -264,6 +274,29 @@ class Package: Identifiable, ObservableObject, Codable {
|
|||||||
var hasValidSize: Bool {
|
var hasValidSize: Bool {
|
||||||
downloadSize > 0
|
downloadSize > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldBeSelectedByDefault: Bool {
|
||||||
|
let targetArchitecture = StorageData.shared.downloadAppleSilicon ? "arm64" : "x64"
|
||||||
|
let language = StorageData.shared.defaultLanguage
|
||||||
|
let isCore = type == "core"
|
||||||
|
|
||||||
|
if isCore {
|
||||||
|
if condition.isEmpty {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
if condition.contains("[OSArchitecture]==\(targetArchitecture)") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if condition.contains("[installLanguage]==\(language)") || language == "ALL" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return condition.contains("[installLanguage]==\(language)") || language == "ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func updateStatus(_ status: PackageStatus) {
|
func updateStatus(_ status: PackageStatus) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
@@ -273,7 +306,7 @@ class Package: Identifiable, ObservableObject, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, type, fullPackageName, downloadSize, downloadURL, packageVersion
|
case id, type, fullPackageName, downloadSize, downloadURL, packageVersion, condition, isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
@@ -283,6 +316,9 @@ class Package: Identifiable, ObservableObject, Codable {
|
|||||||
try container.encode(fullPackageName, forKey: .fullPackageName)
|
try container.encode(fullPackageName, forKey: .fullPackageName)
|
||||||
try container.encode(downloadSize, forKey: .downloadSize)
|
try container.encode(downloadSize, forKey: .downloadSize)
|
||||||
try container.encode(downloadURL, forKey: .downloadURL)
|
try container.encode(downloadURL, forKey: .downloadURL)
|
||||||
|
try container.encode(packageVersion, forKey: .packageVersion)
|
||||||
|
try container.encode(condition, forKey: .condition)
|
||||||
|
try container.encode(isRequired, forKey: .isRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
required init(from decoder: Decoder) throws {
|
||||||
@@ -293,6 +329,12 @@ class Package: Identifiable, ObservableObject, Codable {
|
|||||||
downloadSize = try container.decode(Int64.self, forKey: .downloadSize)
|
downloadSize = try container.decode(Int64.self, forKey: .downloadSize)
|
||||||
downloadURL = try container.decode(String.self, forKey: .downloadURL)
|
downloadURL = try container.decode(String.self, forKey: .downloadURL)
|
||||||
packageVersion = try container.decode(String.self, forKey: .packageVersion)
|
packageVersion = try container.decode(String.self, forKey: .packageVersion)
|
||||||
|
condition = try container.decodeIfPresent(String.self, forKey: .condition) ?? ""
|
||||||
|
isRequired = try container.decodeIfPresent(Bool.self, forKey: .isRequired) ?? false
|
||||||
|
isSelected = isRequired
|
||||||
|
if !isRequired {
|
||||||
|
isSelected = shouldBeSelectedByDefault
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,50 @@ class NetworkManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startCustomDownload(productId: String, selectedVersion: String, language: String, destinationURL: URL, customDependencies: [DependenciesToDownload]) async throws {
|
||||||
|
guard let productInfo = globalCcmResult.products.first(where: { $0.id == productId && $0.version == selectedVersion }) else {
|
||||||
|
throw NetworkError.productNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = NewDownloadTask(
|
||||||
|
productId: productInfo.id,
|
||||||
|
productVersion: selectedVersion,
|
||||||
|
language: language,
|
||||||
|
displayName: productInfo.displayName,
|
||||||
|
directory: destinationURL,
|
||||||
|
dependenciesToDownload: [],
|
||||||
|
createAt: Date(),
|
||||||
|
totalStatus: .preparing(DownloadStatus.PrepareInfo(
|
||||||
|
message: "正在准备自定义下载...",
|
||||||
|
timestamp: Date(),
|
||||||
|
stage: .initializing
|
||||||
|
)),
|
||||||
|
totalProgress: 0,
|
||||||
|
totalDownloadedSize: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
totalSpeed: 0,
|
||||||
|
platform: globalProducts.first(where: { $0.id == productId })?.platforms.first?.id ?? "unknown")
|
||||||
|
|
||||||
|
downloadTasks.append(task)
|
||||||
|
updateDockBadge()
|
||||||
|
await saveTask(task)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await globalNewDownloadUtils.handleCustomDownload(task: task, customDependencies: customDependencies)
|
||||||
|
} catch {
|
||||||
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
||||||
|
message: error.localizedDescription,
|
||||||
|
error: error,
|
||||||
|
timestamp: Date(),
|
||||||
|
recoverable: true
|
||||||
|
)))
|
||||||
|
await saveTask(task)
|
||||||
|
await MainActor.run {
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func removeTask(taskId: UUID, removeFiles: Bool = true) {
|
func removeTask(taskId: UUID, removeFiles: Bool = true) {
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@@ -287,7 +287,9 @@ class NewDownloadUtils {
|
|||||||
fullPackageName: fullPackageName,
|
fullPackageName: fullPackageName,
|
||||||
downloadSize: downloadSize,
|
downloadSize: downloadSize,
|
||||||
downloadURL: downloadURL,
|
downloadURL: downloadURL,
|
||||||
packageVersion: packageVersion
|
packageVersion: packageVersion,
|
||||||
|
condition: condition,
|
||||||
|
isRequired: dependencyToDownload.sapCode == productInfo.id
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,6 +335,89 @@ class NewDownloadUtils {
|
|||||||
await startDownloadProcess(task: task)
|
await startDownloadProcess(task: task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleCustomDownload(task: NewDownloadTask, customDependencies: [DependenciesToDownload]) async throws {
|
||||||
|
await MainActor.run {
|
||||||
|
task.setStatus(.preparing(DownloadStatus.PrepareInfo(
|
||||||
|
message: String(localized: "正在准备自定义下载..."),
|
||||||
|
timestamp: Date(),
|
||||||
|
stage: .fetchingInfo
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for dependencyToDownload in customDependencies {
|
||||||
|
let productDir = task.directory.appendingPathComponent("\(dependencyToDownload.sapCode)")
|
||||||
|
if !FileManager.default.fileExists(atPath: productDir.path) {
|
||||||
|
try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let applicationJson = dependencyToDownload.applicationJson {
|
||||||
|
var processedJsonString = applicationJson
|
||||||
|
|
||||||
|
if let jsonData = applicationJson.data(using: .utf8),
|
||||||
|
var appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
|
||||||
|
|
||||||
|
let selectedPackageNames = Set(dependencyToDownload.packages.filter { $0.isSelected }.map { $0.fullPackageName })
|
||||||
|
|
||||||
|
if var packages = appInfo["Packages"] as? [String: Any],
|
||||||
|
let packageArray = packages["Package"] as? [[String: Any]] {
|
||||||
|
|
||||||
|
let filteredPackages = packageArray.filter { package in
|
||||||
|
if let packageName = package["PackageName"] as? String {
|
||||||
|
let fullPackageName = packageName.hasSuffix(".zip") ? packageName : "\(packageName).zip"
|
||||||
|
return selectedPackageNames.contains(fullPackageName)
|
||||||
|
}
|
||||||
|
if let fullPackageName = package["fullPackageName"] as? String {
|
||||||
|
return selectedPackageNames.contains(fullPackageName)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
packages["Package"] = filteredPackages
|
||||||
|
appInfo["Packages"] = packages
|
||||||
|
}
|
||||||
|
|
||||||
|
if var modules = appInfo["Modules"] as? [String: Any] {
|
||||||
|
modules["Module"] = [] as [[String: Any]]
|
||||||
|
appInfo["Modules"] = modules
|
||||||
|
}
|
||||||
|
|
||||||
|
if let processedData = try? JSONSerialization.data(withJSONObject: appInfo, options: .prettyPrinted),
|
||||||
|
let processedString = String(data: processedData, encoding: .utf8) {
|
||||||
|
processedJsonString = processedString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let jsonURL = productDir.appendingPathComponent("application.json")
|
||||||
|
try processedJsonString.write(to: jsonURL, atomically: true, encoding: String.Encoding.utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filteredDependencies = customDependencies.map { dependency in
|
||||||
|
let selectedPackages = dependency.packages.filter { $0.isSelected }
|
||||||
|
let filteredDependency = DependenciesToDownload(
|
||||||
|
sapCode: dependency.sapCode,
|
||||||
|
version: dependency.version,
|
||||||
|
buildGuid: dependency.buildGuid,
|
||||||
|
applicationJson: dependency.applicationJson ?? ""
|
||||||
|
)
|
||||||
|
filteredDependency.packages = selectedPackages
|
||||||
|
return filteredDependency
|
||||||
|
}.filter { !$0.packages.isEmpty }
|
||||||
|
|
||||||
|
let totalSize = filteredDependencies.reduce(0) { productSum, product in
|
||||||
|
productSum + product.packages.reduce(0) { packageSum, pkg in
|
||||||
|
packageSum + (pkg.downloadSize > 0 ? pkg.downloadSize : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
task.dependenciesToDownload = filteredDependencies
|
||||||
|
task.totalSize = totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
await startDownloadProcess(task: task)
|
||||||
|
}
|
||||||
|
|
||||||
private func startDownloadProcess(task: NewDownloadTask) async {
|
private func startDownloadProcess(task: NewDownloadTask) async {
|
||||||
actor DownloadProgress {
|
actor DownloadProgress {
|
||||||
var currentPackageIndex: Int = 0
|
var currentPackageIndex: Int = 0
|
||||||
|
|||||||
619
Adobe Downloader/Views/CustomDownloadView.swift
Normal file
619
Adobe Downloader/Views/CustomDownloadView.swift
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
//
|
||||||
|
// CustomDownloadView.swift
|
||||||
|
// Adobe Downloader
|
||||||
|
//
|
||||||
|
// Created by X1a0He on 2024/10/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CustomDownloadView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@StateObject private var loadingState = CustomDownloadLoadingState()
|
||||||
|
@State private var allPackages: [Package] = []
|
||||||
|
@State private var dependenciesToDownload: [DependenciesToDownload] = []
|
||||||
|
|
||||||
|
let productId: String
|
||||||
|
let version: String
|
||||||
|
let onDownloadStart: ([DependenciesToDownload]) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if loadingState.isLoading {
|
||||||
|
CustomDownloadLoadingView(
|
||||||
|
loadingState: loadingState,
|
||||||
|
productId: productId,
|
||||||
|
version: version
|
||||||
|
)
|
||||||
|
} else if loadingState.error != nil {
|
||||||
|
VStack {
|
||||||
|
Text("加载失败")
|
||||||
|
.font(.headline)
|
||||||
|
Text(loadingState.error!)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Button("重试") {
|
||||||
|
loadingState.error = nil
|
||||||
|
loadingState.isLoading = true
|
||||||
|
loadPackageInfo()
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: Color.blue))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else {
|
||||||
|
CustomPackageSelectorView(
|
||||||
|
productId: productId,
|
||||||
|
version: version,
|
||||||
|
packages: allPackages,
|
||||||
|
dependenciesToDownload: dependenciesToDownload,
|
||||||
|
onDownloadStart: { dependencies in
|
||||||
|
onDownloadStart(dependencies)
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
onCancel: { dismiss() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if loadingState.isLoading {
|
||||||
|
loadPackageInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPackageInfo() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let (packages, dependencies) = try await fetchPackageInfo()
|
||||||
|
await MainActor.run {
|
||||||
|
allPackages = packages
|
||||||
|
dependenciesToDownload = dependencies
|
||||||
|
loadingState.isLoading = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
loadingState.isLoading = false
|
||||||
|
loadingState.error = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchPackageInfo() async throws -> ([Package], [DependenciesToDownload]) {
|
||||||
|
guard let product = findProduct(id: productId) else {
|
||||||
|
throw NetworkError.invalidData("找不到产品信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
var allPackages: [Package] = []
|
||||||
|
var dependenciesToDownload: [DependenciesToDownload] = []
|
||||||
|
|
||||||
|
let firstPlatform = product.platforms.first
|
||||||
|
let buildGuid = firstPlatform?.languageSet.first?.buildGuid ?? ""
|
||||||
|
|
||||||
|
var dependencyInfos: [DependenciesToDownload] = []
|
||||||
|
dependencyInfos.append(DependenciesToDownload(sapCode: product.id, version: product.version, buildGuid: buildGuid))
|
||||||
|
|
||||||
|
let dependencies = firstPlatform?.languageSet.first?.dependencies
|
||||||
|
if let dependencies = dependencies {
|
||||||
|
for dependency in dependencies {
|
||||||
|
dependencyInfos.append(DependenciesToDownload(sapCode: dependency.sapCode, version: dependency.productVersion, buildGuid: dependency.buildGuid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dependencyInfo in dependencyInfos {
|
||||||
|
await MainActor.run {
|
||||||
|
loadingState.currentTask = "正在处理 \(dependencyInfo.sapCode) 的包信息..."
|
||||||
|
}
|
||||||
|
|
||||||
|
let jsonString = try await globalNetworkService.getApplicationInfo(buildGuid: dependencyInfo.buildGuid)
|
||||||
|
dependencyInfo.applicationJson = jsonString
|
||||||
|
|
||||||
|
var processedJsonString = jsonString
|
||||||
|
if dependencyInfo.sapCode == product.id {
|
||||||
|
if let jsonData = jsonString.data(using: .utf8),
|
||||||
|
var appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
|
||||||
|
|
||||||
|
if var modules = appInfo["Modules"] as? [String: Any] {
|
||||||
|
modules["Module"] = [] as [[String: Any]]
|
||||||
|
appInfo["Modules"] = modules
|
||||||
|
}
|
||||||
|
|
||||||
|
if let processedData = try? JSONSerialization.data(withJSONObject: appInfo, options: .prettyPrinted),
|
||||||
|
let processedString = String(data: processedData, encoding: .utf8) {
|
||||||
|
processedJsonString = processedString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let jsonData = processedJsonString.data(using: .utf8),
|
||||||
|
let appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
|
||||||
|
let packages = appInfo["Packages"] as? [String: Any],
|
||||||
|
let packageArray = packages["Package"] as? [[String: Any]] else {
|
||||||
|
throw NetworkError.invalidData("无法解析产品信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
for package in packageArray {
|
||||||
|
guard let downloadURL = package["Path"] as? String, !downloadURL.isEmpty else { continue }
|
||||||
|
|
||||||
|
let packageVersion: String = package["PackageVersion"] as? String ?? ""
|
||||||
|
let fullPackageName: String
|
||||||
|
|
||||||
|
if let name = package["fullPackageName"] as? String, !name.isEmpty {
|
||||||
|
fullPackageName = name
|
||||||
|
} else if let name = package["PackageName"] as? String, !name.isEmpty {
|
||||||
|
fullPackageName = "\(name).zip"
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadSize: Int64
|
||||||
|
switch package["DownloadSize"] {
|
||||||
|
case let sizeNumber as NSNumber:
|
||||||
|
downloadSize = sizeNumber.int64Value
|
||||||
|
case let sizeString as String:
|
||||||
|
downloadSize = Int64(sizeString) ?? 0
|
||||||
|
default:
|
||||||
|
downloadSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageType = package["Type"] as? String ?? "non-core"
|
||||||
|
let condition = package["Condition"] as? String ?? ""
|
||||||
|
|
||||||
|
// 判断是否应该默认选中这个包
|
||||||
|
let isCore = packageType == "core"
|
||||||
|
let targetArchitecture = StorageData.shared.downloadAppleSilicon ? "arm64" : "x64"
|
||||||
|
let language = StorageData.shared.defaultLanguage
|
||||||
|
let installLanguage = "[installLanguage]==\(language)"
|
||||||
|
|
||||||
|
var shouldDefaultSelect = false
|
||||||
|
var isRequired = false
|
||||||
|
|
||||||
|
if dependencyInfo.sapCode == product.id {
|
||||||
|
if isCore {
|
||||||
|
shouldDefaultSelect = condition.isEmpty ||
|
||||||
|
condition.contains("[OSArchitecture]==\(targetArchitecture)") ||
|
||||||
|
condition.contains(installLanguage) || language == "ALL"
|
||||||
|
// 主产品的core包且满足条件的设为必需
|
||||||
|
isRequired = shouldDefaultSelect
|
||||||
|
} else {
|
||||||
|
shouldDefaultSelect = condition.contains(installLanguage) || language == "ALL"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldDefaultSelect = condition.isEmpty ||
|
||||||
|
(condition.contains("[OSVersion]") && checkOSVersionCondition(condition)) ||
|
||||||
|
condition.contains(installLanguage) || language == "ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageObj = Package(
|
||||||
|
type: packageType,
|
||||||
|
fullPackageName: fullPackageName,
|
||||||
|
downloadSize: downloadSize,
|
||||||
|
downloadURL: downloadURL,
|
||||||
|
packageVersion: packageVersion,
|
||||||
|
condition: condition,
|
||||||
|
isRequired: isRequired
|
||||||
|
)
|
||||||
|
|
||||||
|
packageObj.isSelected = shouldDefaultSelect
|
||||||
|
|
||||||
|
dependencyInfo.packages.append(packageObj)
|
||||||
|
allPackages.append(packageObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesToDownload.append(dependencyInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (allPackages, dependenciesToDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkOSVersionCondition(_ condition: String) -> Bool {
|
||||||
|
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||||
|
let currentVersion = Double("\(osVersion.majorVersion).\(osVersion.minorVersion)") ?? 0.0
|
||||||
|
|
||||||
|
let versionPattern = #"\[OSVersion\](>=|<=|<|>|==)([\d.]+)"#
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: versionPattern) else { return false }
|
||||||
|
|
||||||
|
let nsRange = NSRange(condition.startIndex..<condition.endIndex, in: condition)
|
||||||
|
let matches = regex.matches(in: condition, range: nsRange)
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
guard match.numberOfRanges >= 3,
|
||||||
|
let operatorRange = Range(match.range(at: 1), in: condition),
|
||||||
|
let versionRange = Range(match.range(at: 2), in: condition),
|
||||||
|
let requiredVersion = Double(condition[versionRange]) else { continue }
|
||||||
|
|
||||||
|
let operatorSymbol = String(condition[operatorRange])
|
||||||
|
if !compareVersions(current: currentVersion, required: requiredVersion, operator: operatorSymbol) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !matches.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private func compareVersions(current: Double, required: Double, operator: String) -> Bool {
|
||||||
|
switch `operator` {
|
||||||
|
case ">=":
|
||||||
|
return current >= required
|
||||||
|
case "<=":
|
||||||
|
return current <= required
|
||||||
|
case ">":
|
||||||
|
return current > required
|
||||||
|
case "<":
|
||||||
|
return current < required
|
||||||
|
case "==":
|
||||||
|
return current == required
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomDownloadLoadingState: ObservableObject {
|
||||||
|
@Published var isLoading = true
|
||||||
|
@Published var currentTask = ""
|
||||||
|
@Published var error: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CustomDownloadLoadingView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@ObservedObject var loadingState: CustomDownloadLoadingState
|
||||||
|
|
||||||
|
let productId: String
|
||||||
|
let version: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
VStack {
|
||||||
|
if let product = findProduct(id: productId) {
|
||||||
|
HStack {
|
||||||
|
if let icon = product.getBestIcon() {
|
||||||
|
AsyncImage(url: URL(string: icon.value)) { image in
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
} placeholder: {
|
||||||
|
Image(systemName: "app.fill")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(product.displayName)
|
||||||
|
.font(.headline)
|
||||||
|
Text("版本 \(version)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle())
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
|
||||||
|
Text("正在获取包信息...")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
if !loadingState.currentTask.isEmpty {
|
||||||
|
Text(loadingState.currentTask)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("取消") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: Color.gray.opacity(0.2)))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.frame(width: 500, height: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CustomPackageSelectorView: View {
|
||||||
|
@State private var selectedPackages: Set<UUID> = []
|
||||||
|
@State private var searchText = ""
|
||||||
|
@State private var showCopiedAlert = false
|
||||||
|
|
||||||
|
let productId: String
|
||||||
|
let version: String
|
||||||
|
let packages: [Package]
|
||||||
|
let dependenciesToDownload: [DependenciesToDownload]
|
||||||
|
let onDownloadStart: ([DependenciesToDownload]) -> Void
|
||||||
|
let onCancel: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
Text("选择要下载的包")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: copyAllInfo) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
Text("复制全部")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
}
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: Color.blue))
|
||||||
|
.help("复制所有包信息")
|
||||||
|
|
||||||
|
Button("取消") {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: Color.gray.opacity(0.2)))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(spacing: 8) {
|
||||||
|
ForEach(dependenciesToDownload, id: \.sapCode) { dependency in
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "cube.box.fill")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(.blue.opacity(0.8))
|
||||||
|
|
||||||
|
Text("\(dependency.sapCode) \(dependency.version)\(dependency.sapCode != "APRO" ? " - (\(dependency.buildGuid))" : "")")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(.primary.opacity(0.8))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
|
||||||
|
if dependency.sapCode != "APRO" {
|
||||||
|
Button(action: {
|
||||||
|
copyToClipboard(dependency.buildGuid)
|
||||||
|
}) {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: .blue))
|
||||||
|
.help("复制 buildGuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
ForEach(dependency.packages) { package in
|
||||||
|
EnhancedPackageRow(
|
||||||
|
package: package,
|
||||||
|
isSelected: selectedPackages.contains(package.id),
|
||||||
|
onToggle: { isSelected in
|
||||||
|
if !package.isRequired {
|
||||||
|
if isSelected {
|
||||||
|
selectedPackages.insert(package.id)
|
||||||
|
} else {
|
||||||
|
selectedPackages.remove(package.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCopyPackageInfo: {
|
||||||
|
copyPackageInfo(package)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor).opacity(0.3))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("已选择 \(selectedPackages.count) 个包")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text("总大小: \(formattedTotalSize)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("开始下载") {
|
||||||
|
startCustomDownload()
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: Color.blue))
|
||||||
|
.disabled(selectedPackages.isEmpty)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.frame(width: 800, height: 650)
|
||||||
|
.onAppear {
|
||||||
|
initializeSelection()
|
||||||
|
}
|
||||||
|
.popover(isPresented: $showCopiedAlert, arrowEdge: .trailing) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("已复制")
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var formattedTotalSize: String {
|
||||||
|
let totalSize = selectedPackages.compactMap { id in
|
||||||
|
packages.first { $0.id == id }?.downloadSize
|
||||||
|
}.reduce(0, +)
|
||||||
|
|
||||||
|
return ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initializeSelection() {
|
||||||
|
for package in packages {
|
||||||
|
if package.isRequired || package.isSelected {
|
||||||
|
selectedPackages.insert(package.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startCustomDownload() {
|
||||||
|
for dependency in dependenciesToDownload {
|
||||||
|
for package in dependency.packages {
|
||||||
|
package.isSelected = selectedPackages.contains(package.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalDependencies = dependenciesToDownload.filter { dependency in
|
||||||
|
dependency.packages.contains { $0.isSelected }
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadStart(finalDependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copyToClipboard(_ text: String) {
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
pasteboard.clearContents()
|
||||||
|
pasteboard.setString(text, forType: .string)
|
||||||
|
showCopiedAlert = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||||
|
showCopiedAlert = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copyPackageInfo(_ package: Package) {
|
||||||
|
let packageInfo = "\(package.fullPackageName) (\(package.packageVersion)) - \(package.type)"
|
||||||
|
copyToClipboard(packageInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copyAllInfo() {
|
||||||
|
var result = ""
|
||||||
|
|
||||||
|
for (index, dependency) in dependenciesToDownload.enumerated() {
|
||||||
|
let dependencyInfo: String
|
||||||
|
if dependency.sapCode == "APRO" {
|
||||||
|
dependencyInfo = "\(dependency.sapCode) \(dependency.version)"
|
||||||
|
} else {
|
||||||
|
dependencyInfo = "\(dependency.sapCode) \(dependency.version) - (\(dependency.buildGuid))"
|
||||||
|
}
|
||||||
|
result += dependencyInfo + "\n"
|
||||||
|
|
||||||
|
for (pkgIndex, package) in dependency.packages.enumerated() {
|
||||||
|
let isLastPackage = pkgIndex == dependency.packages.count - 1
|
||||||
|
let prefix = isLastPackage ? " └── " : " ├── "
|
||||||
|
result += "\(prefix)\(package.fullPackageName) (\(package.packageVersion)) - \(package.type)\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if index < dependenciesToDownload.count - 1 {
|
||||||
|
result += "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct EnhancedPackageRow: View {
|
||||||
|
let package: Package
|
||||||
|
let isSelected: Bool
|
||||||
|
let onToggle: (Bool) -> Void
|
||||||
|
let onCopyPackageInfo: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Button(action: {
|
||||||
|
if !package.isRequired {
|
||||||
|
onToggle(!isSelected)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: isSelected ? "checkmark.square.fill" : "square")
|
||||||
|
.foregroundColor(package.isRequired ? .secondary : .blue)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.disabled(package.isRequired)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("\(package.fullPackageName) (\(package.packageVersion))")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(.primary.opacity(0.8))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
|
||||||
|
Text(package.type)
|
||||||
|
.font(.system(size: 10, weight: .medium))
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 3)
|
||||||
|
.fill(package.type == "core" ? Color.blue.opacity(0.1) : Color.orange.opacity(0.1))
|
||||||
|
)
|
||||||
|
.foregroundColor(package.type == "core" ? .blue : .orange)
|
||||||
|
|
||||||
|
if package.isRequired {
|
||||||
|
Text("必需")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(Color.red.opacity(0.8))
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(package.formattedSize)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Button(action: onCopyPackageInfo) {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.font(.system(size: 9))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.buttonStyle(BeautifulButtonStyle(baseColor: .gray.opacity(0.6)))
|
||||||
|
.help("复制包信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !package.condition.isEmpty {
|
||||||
|
Text("条件: \(package.condition)")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.secondary.opacity(0.8))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(isSelected ? Color.blue.opacity(0.1) : Color.clear)
|
||||||
|
.overlay(
|
||||||
|
Rectangle()
|
||||||
|
.stroke(Color.secondary.opacity(0.2), lineWidth: 0.5)
|
||||||
|
.opacity(0.5),
|
||||||
|
alignment: .bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ struct VersionPickerView: View {
|
|||||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||||
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
||||||
@State private var expandedVersions: Set<String> = []
|
@State private var expandedVersions: Set<String> = []
|
||||||
|
@State private var showingCustomDownload = false
|
||||||
|
@State private var customDownloadVersion = ""
|
||||||
|
|
||||||
private let productId: String
|
private let productId: String
|
||||||
private let onSelect: (String) -> Void
|
private let onSelect: (String) -> Void
|
||||||
@@ -41,10 +43,86 @@ struct VersionPickerView: View {
|
|||||||
productId: productId,
|
productId: productId,
|
||||||
expandedVersions: $expandedVersions,
|
expandedVersions: $expandedVersions,
|
||||||
onSelect: onSelect,
|
onSelect: onSelect,
|
||||||
dismiss: dismiss
|
dismiss: dismiss,
|
||||||
|
onCustomDownload: { version in
|
||||||
|
customDownloadVersion = version
|
||||||
|
showingCustomDownload = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.frame(width: VersionPickerConstants.viewWidth, height: VersionPickerConstants.viewHeight)
|
.frame(width: VersionPickerConstants.viewWidth, height: VersionPickerConstants.viewHeight)
|
||||||
|
.sheet(isPresented: $showingCustomDownload) {
|
||||||
|
CustomDownloadView(
|
||||||
|
productId: productId,
|
||||||
|
version: customDownloadVersion,
|
||||||
|
onDownloadStart: { dependencies in
|
||||||
|
handleCustomDownload(dependencies: dependencies)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDestinationURL(productId: String, version: String, language: String) async throws -> URL {
|
||||||
|
let platform = globalProducts.first(where: { $0.id == productId && $0.version == version })?.platforms.first?.id ?? "unknown"
|
||||||
|
let installerName = productId == "APRO"
|
||||||
|
? "Adobe Downloader \(productId)_\(version)_\(platform).dmg"
|
||||||
|
: "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)"
|
||||||
|
|
||||||
|
if StorageData.shared.useDefaultDirectory && !StorageData.shared.defaultDirectory.isEmpty {
|
||||||
|
return URL(fileURLWithPath: StorageData.shared.defaultDirectory)
|
||||||
|
.appendingPathComponent(installerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let panel = NSOpenPanel()
|
||||||
|
panel.title = "选择保存位置"
|
||||||
|
panel.canCreateDirectories = true
|
||||||
|
panel.canChooseDirectories = true
|
||||||
|
panel.canChooseFiles = false
|
||||||
|
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||||
|
|
||||||
|
if panel.runModal() == .OK, let selectedURL = panel.url {
|
||||||
|
continuation.resume(returning: selectedURL.appendingPathComponent(installerName))
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: NetworkError.cancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleCustomDownload(dependencies: [DependenciesToDownload]) {
|
||||||
|
showingCustomDownload = false
|
||||||
|
|
||||||
|
Task {
|
||||||
|
let destinationURL: URL
|
||||||
|
do {
|
||||||
|
destinationURL = try await getDestinationURL(
|
||||||
|
productId: productId,
|
||||||
|
version: customDownloadVersion,
|
||||||
|
language: StorageData.shared.defaultLanguage
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
await MainActor.run { dismiss() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await globalNetworkManager.startCustomDownload(
|
||||||
|
productId: productId,
|
||||||
|
selectedVersion: customDownloadVersion,
|
||||||
|
language: StorageData.shared.defaultLanguage,
|
||||||
|
destinationURL: destinationURL,
|
||||||
|
customDependencies: dependencies
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
print("自定义下载失败: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +193,7 @@ private struct VersionListView: View {
|
|||||||
@Binding var expandedVersions: Set<String>
|
@Binding var expandedVersions: Set<String>
|
||||||
let onSelect: (String) -> Void
|
let onSelect: (String) -> Void
|
||||||
let dismiss: DismissAction
|
let dismiss: DismissAction
|
||||||
|
let onCustomDownload: (String) -> Void
|
||||||
@State private var scrollPosition: String?
|
@State private var scrollPosition: String?
|
||||||
@State private var cachedVersions: [(key: String, value: Product.Platform)] = []
|
@State private var cachedVersions: [(key: String, value: Product.Platform)] = []
|
||||||
|
|
||||||
@@ -130,7 +209,8 @@ private struct VersionListView: View {
|
|||||||
info: info,
|
info: info,
|
||||||
isExpanded: expandedVersions.contains(version),
|
isExpanded: expandedVersions.contains(version),
|
||||||
onSelect: handleVersionSelect,
|
onSelect: handleVersionSelect,
|
||||||
onToggle: handleVersionToggle
|
onToggle: handleVersionToggle,
|
||||||
|
onCustomDownload: handleCustomDownload
|
||||||
)
|
)
|
||||||
.id(version)
|
.id(version)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
@@ -210,6 +290,10 @@ private struct VersionListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleCustomDownload(_ version: String) {
|
||||||
|
onCustomDownload(version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct VersionRow: View, Equatable {
|
private struct VersionRow: View, Equatable {
|
||||||
@@ -221,6 +305,7 @@ private struct VersionRow: View, Equatable {
|
|||||||
let isExpanded: Bool
|
let isExpanded: Bool
|
||||||
let onSelect: (String) -> Void
|
let onSelect: (String) -> Void
|
||||||
let onToggle: (String) -> Void
|
let onToggle: (String) -> Void
|
||||||
|
let onCustomDownload: (String) -> Void
|
||||||
|
|
||||||
static func == (lhs: VersionRow, rhs: VersionRow) -> Bool {
|
static func == (lhs: VersionRow, rhs: VersionRow) -> Bool {
|
||||||
lhs.productId == rhs.productId &&
|
lhs.productId == rhs.productId &&
|
||||||
@@ -249,7 +334,8 @@ private struct VersionRow: View, Equatable {
|
|||||||
VersionDetails(
|
VersionDetails(
|
||||||
info: info,
|
info: info,
|
||||||
version: version,
|
version: version,
|
||||||
onSelect: onSelect
|
onSelect: onSelect,
|
||||||
|
onCustomDownload: onCustomDownload
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,6 +479,7 @@ private struct VersionDetails: View {
|
|||||||
let info: Product.Platform
|
let info: Product.Platform
|
||||||
let version: String
|
let version: String
|
||||||
let onSelect: (String) -> Void
|
let onSelect: (String) -> Void
|
||||||
|
let onCustomDownload: (String) -> Void
|
||||||
|
|
||||||
private var hasDependencies: Bool {
|
private var hasDependencies: Bool {
|
||||||
!(info.languageSet.first?.dependencies.isEmpty ?? true)
|
!(info.languageSet.first?.dependencies.isEmpty ?? true)
|
||||||
@@ -462,7 +549,13 @@ private struct VersionDetails: View {
|
|||||||
.cornerRadius(6)
|
.cornerRadius(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadButton(version: version, onSelect: onSelect)
|
DownloadButton(
|
||||||
|
version: version,
|
||||||
|
onSelect: onSelect,
|
||||||
|
onCustomDownload: { version in
|
||||||
|
onCustomDownload(version)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
@@ -628,10 +721,11 @@ private struct ModulesList: View {
|
|||||||
private struct DownloadButton: View {
|
private struct DownloadButton: View {
|
||||||
let version: String
|
let version: String
|
||||||
let onSelect: (String) -> Void
|
let onSelect: (String) -> Void
|
||||||
|
let onCustomDownload: (String) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button("下载") {
|
Button("下载") {
|
||||||
onSelect(version)
|
onCustomDownload(version)
|
||||||
}
|
}
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.buttonStyle(BeautifulButtonStyle(baseColor: Color.blue))
|
.buttonStyle(BeautifulButtonStyle(baseColor: Color.blue))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"(可能导致处理失败)" : {
|
"(可能导致处理失败)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"(无法使用安装功能)" : {
|
"(无法使用安装功能)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -135,6 +137,12 @@
|
|||||||
},
|
},
|
||||||
"•" : {
|
"•" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"✅" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"❌" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Adobe Creative Cloud" : {
|
"Adobe Creative Cloud" : {
|
||||||
|
|
||||||
@@ -278,7 +286,6 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Debug 模式" : {
|
"Debug 模式" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -359,6 +366,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 未连接" : {
|
"Helper 未连接" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -414,9 +422,15 @@
|
|||||||
},
|
},
|
||||||
"macOS %@" : {
|
"macOS %@" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Match:" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Reason:" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。" : {
|
"Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -439,6 +453,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Setup 组件未处理,无法安装" : {
|
"Setup 组件未处理,无法安装" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -457,6 +472,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Target:" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"v%@" : {
|
"v%@" : {
|
||||||
|
|
||||||
@@ -1076,7 +1094,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"可选模块" : {
|
"可选模块" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1175,6 +1192,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"复制全部" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"复制包信息" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"复制命令" : {
|
"复制命令" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1205,6 +1228,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"复制所有包信息" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"多次尝试连接失败" : {
|
"多次尝试连接失败" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1441,7 +1467,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"展开全部" : {
|
"展开全部" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1482,6 +1507,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"已处理" : {
|
"已处理" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1492,6 +1518,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"已备份" : {
|
"已备份" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1593,6 +1620,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"已选择 %lld 个包" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"常见问题" : {
|
"常见问题" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1625,6 +1655,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"开始下载" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"开始清理" : {
|
"开始清理" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1665,6 +1698,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"必需" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"总大小: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"所有操作已成功完成" : {
|
"所有操作已成功完成" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1727,7 +1766,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"折叠全部" : {
|
"折叠全部" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1738,7 +1776,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"持久化文件" : {
|
"持久化文件" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2101,6 +2138,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"未处理" : {
|
"未处理" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2111,6 +2149,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"未备份" : {
|
"未备份" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2131,7 +2170,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"未对 Setup 组件进行处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行处理" : {
|
"未对 Setup 组件进行处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行处理" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2142,6 +2180,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"未对 Setup 组件进行处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面对 Setup 组件进行处理" : {
|
"未对 Setup 组件进行处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面对 Setup 组件进行处理" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2227,6 +2266,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"条件: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"查看" : {
|
"查看" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2308,6 +2350,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"正在准备自定义下载..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"正在加载..." : {
|
"正在加载..." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2498,6 +2543,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"正在获取包信息..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"正在连接" : {
|
"正在连接" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2609,6 +2657,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"版本 %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"版本不兼容: 当前版本 %@, 需要版本 %@" : {
|
"版本不兼容: 当前版本 %@, 需要版本 %@" : {
|
||||||
"comment" : "Incompatible version",
|
"comment" : "Incompatible version",
|
||||||
@@ -3199,6 +3250,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"选择要下载的包" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"选择要清理的内容" : {
|
"选择要清理的内容" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
Reference in New Issue
Block a user