feat: 增加自定义包下载(未完善)

This commit is contained in:
X1a0He
2025-07-12 00:28:19 +08:00
parent 9d6b0efd41
commit dce7c9c7a3
7 changed files with 953 additions and 15 deletions

View File

@@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"

View File

@@ -190,6 +190,10 @@ class Package: Identifiable, ObservableObject, Codable {
@Published var status: PackageStatus = .waiting
@Published var downloaded: Bool = false
@Published var isSelected: Bool = false
var isRequired: Bool = false
var condition: String = ""
var lastUpdated: Date = Date()
var lastRecordedSize: Int64 = 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.fullPackageName = fullPackageName
self.downloadSize = downloadSize
self.downloadURL = downloadURL
self.packageVersion = packageVersion
self.condition = condition
self.isRequired = isRequired
self.isSelected = isRequired
if !isRequired {
self.isSelected = shouldBeSelectedByDefault
}
}
func updateProgress(downloadedSize: Int64, speed: Double) {
@@ -264,6 +274,29 @@ class Package: Identifiable, ObservableObject, Codable {
var hasValidSize: Bool {
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) {
Task { @MainActor in
@@ -273,7 +306,7 @@ class Package: Identifiable, ObservableObject, Codable {
}
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 {
@@ -283,6 +316,9 @@ class Package: Identifiable, ObservableObject, Codable {
try container.encode(fullPackageName, forKey: .fullPackageName)
try container.encode(downloadSize, forKey: .downloadSize)
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 {
@@ -293,6 +329,12 @@ class Package: Identifiable, ObservableObject, Codable {
downloadSize = try container.decode(Int64.self, forKey: .downloadSize)
downloadURL = try container.decode(String.self, forKey: .downloadURL)
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
}
}
}

View File

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

View File

@@ -287,7 +287,9 @@ class NewDownloadUtils {
fullPackageName: fullPackageName,
downloadSize: downloadSize,
downloadURL: downloadURL,
packageVersion: packageVersion
packageVersion: packageVersion,
condition: condition,
isRequired: dependencyToDownload.sapCode == productInfo.id
))
}
}
@@ -333,6 +335,89 @@ class NewDownloadUtils {
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 {
actor DownloadProgress {
var currentPackageIndex: Int = 0

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

View File

@@ -25,6 +25,8 @@ struct VersionPickerView: View {
@StorageValue(\.defaultLanguage) private var defaultLanguage
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
@State private var expandedVersions: Set<String> = []
@State private var showingCustomDownload = false
@State private var customDownloadVersion = ""
private let productId: String
private let onSelect: (String) -> Void
@@ -41,10 +43,86 @@ struct VersionPickerView: View {
productId: productId,
expandedVersions: $expandedVersions,
onSelect: onSelect,
dismiss: dismiss
dismiss: dismiss,
onCustomDownload: { version in
customDownloadVersion = version
showingCustomDownload = true
}
)
}
.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>
let onSelect: (String) -> Void
let dismiss: DismissAction
let onCustomDownload: (String) -> Void
@State private var scrollPosition: String?
@State private var cachedVersions: [(key: String, value: Product.Platform)] = []
@@ -130,7 +209,8 @@ private struct VersionListView: View {
info: info,
isExpanded: expandedVersions.contains(version),
onSelect: handleVersionSelect,
onToggle: handleVersionToggle
onToggle: handleVersionToggle,
onCustomDownload: handleCustomDownload
)
.id(version)
.transition(.opacity)
@@ -210,6 +290,10 @@ private struct VersionListView: View {
}
}
}
private func handleCustomDownload(_ version: String) {
onCustomDownload(version)
}
}
private struct VersionRow: View, Equatable {
@@ -221,6 +305,7 @@ private struct VersionRow: View, Equatable {
let isExpanded: Bool
let onSelect: (String) -> Void
let onToggle: (String) -> Void
let onCustomDownload: (String) -> Void
static func == (lhs: VersionRow, rhs: VersionRow) -> Bool {
lhs.productId == rhs.productId &&
@@ -249,7 +334,8 @@ private struct VersionRow: View, Equatable {
VersionDetails(
info: info,
version: version,
onSelect: onSelect
onSelect: onSelect,
onCustomDownload: onCustomDownload
)
}
}
@@ -393,6 +479,7 @@ private struct VersionDetails: View {
let info: Product.Platform
let version: String
let onSelect: (String) -> Void
let onCustomDownload: (String) -> Void
private var hasDependencies: Bool {
!(info.languageSet.first?.dependencies.isEmpty ?? true)
@@ -462,7 +549,13 @@ private struct VersionDetails: View {
.cornerRadius(6)
}
DownloadButton(version: version, onSelect: onSelect)
DownloadButton(
version: version,
onSelect: onSelect,
onCustomDownload: { version in
onCustomDownload(version)
}
)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 8)
@@ -628,10 +721,11 @@ private struct ModulesList: View {
private struct DownloadButton: View {
let version: String
let onSelect: (String) -> Void
let onCustomDownload: (String) -> Void
var body: some View {
Button("下载") {
onSelect(version)
onCustomDownload(version)
}
.foregroundColor(.white)
.buttonStyle(BeautifulButtonStyle(baseColor: Color.blue))

View File

@@ -17,6 +17,7 @@
},
"(可能导致处理失败)" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -27,6 +28,7 @@
}
},
"(无法使用安装功能)" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -135,6 +137,12 @@
},
"•" : {
},
"✅" : {
},
"❌" : {
},
"Adobe Creative Cloud" : {
@@ -278,7 +286,6 @@
},
"Debug 模式" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -359,6 +366,7 @@
}
},
"Helper 未连接" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -414,9 +422,15 @@
},
"macOS %@" : {
},
"Match:" : {
},
"OK" : {
},
"Reason:" : {
},
"Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。" : {
"localizations" : {
@@ -439,6 +453,7 @@
}
},
"Setup 组件未处理,无法安装" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -457,6 +472,9 @@
}
}
}
},
"Target:" : {
},
"v%@" : {
@@ -1076,7 +1094,6 @@
}
},
"可选模块" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -1175,6 +1192,12 @@
}
}
}
},
"复制全部" : {
},
"复制包信息" : {
},
"复制命令" : {
"localizations" : {
@@ -1205,6 +1228,9 @@
}
}
}
},
"复制所有包信息" : {
},
"多次尝试连接失败" : {
"localizations" : {
@@ -1441,7 +1467,6 @@
}
},
"展开全部" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -1482,6 +1507,7 @@
}
},
"已处理" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -1492,6 +1518,7 @@
}
},
"已备份" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -1593,6 +1620,9 @@
}
}
}
},
"已选择 %lld 个包" : {
},
"常见问题" : {
"localizations" : {
@@ -1625,6 +1655,9 @@
}
}
}
},
"开始下载" : {
},
"开始清理" : {
"localizations" : {
@@ -1665,6 +1698,12 @@
}
}
}
},
"必需" : {
},
"总大小: %@" : {
},
"所有操作已成功完成" : {
"localizations" : {
@@ -1727,7 +1766,6 @@
}
},
"折叠全部" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -1738,7 +1776,6 @@
}
},
"持久化文件" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -2101,6 +2138,7 @@
}
},
"未处理" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -2111,6 +2149,7 @@
}
},
"未备份" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -2131,7 +2170,6 @@
}
},
"未对 Setup 组件进行处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行处理" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -2142,6 +2180,7 @@
}
},
"未对 Setup 组件进行处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面对 Setup 组件进行处理" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -2227,6 +2266,9 @@
}
}
}
},
"条件: %@" : {
},
"查看" : {
"localizations" : {
@@ -2308,6 +2350,9 @@
}
}
}
},
"正在准备自定义下载..." : {
},
"正在加载..." : {
"localizations" : {
@@ -2498,6 +2543,9 @@
}
}
}
},
"正在获取包信息..." : {
},
"正在连接" : {
"localizations" : {
@@ -2609,6 +2657,9 @@
}
}
}
},
"版本 %@" : {
},
"版本不兼容: 当前版本 %@, 需要版本 %@" : {
"comment" : "Incompatible version",
@@ -3199,6 +3250,9 @@
}
}
}
},
"选择要下载的包" : {
},
"选择要清理的内容" : {
"localizations" : {