mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 11:18:53 +08:00
refactor: complete initial reconstruction
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
|
.fleet
|
||||||
|
|
||||||
## User settings
|
## User settings
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<key>Adobe-Downloader.xcscheme_^#shared#^_</key>
|
<key>Adobe-Downloader.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>AdobeDownloaderHelperTool.xcscheme_^#shared#^_</key>
|
<key>AdobeDownloaderHelperTool.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ struct Adobe_DownloaderApp: App {
|
|||||||
|
|
||||||
Settings {
|
Settings {
|
||||||
AboutView(updater: updaterController.updater)
|
AboutView(updater: updaterController.updater)
|
||||||
.environmentObject(globalNetworkManager)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ enum NetworkError: Error, LocalizedError {
|
|||||||
case unsupportedPlatform(String)
|
case unsupportedPlatform(String)
|
||||||
case incompatibleVersion(String, String)
|
case incompatibleVersion(String, String)
|
||||||
case installError(String)
|
case installError(String)
|
||||||
|
case productNotFound
|
||||||
|
|
||||||
private var errorGroup: Int {
|
private var errorGroup: Int {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -64,7 +65,7 @@ enum NetworkError: Error, LocalizedError {
|
|||||||
case .httpError, .serverError, .clientError: return 4000
|
case .httpError, .serverError, .clientError: return 4000
|
||||||
case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000
|
case .downloadError, .downloadCancelled, .insufficientStorage, .cancelled: return 5000
|
||||||
case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000
|
case .fileSystemError, .fileExists, .fileNotFound, .filePermissionDenied: return 6000
|
||||||
case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError: return 7000
|
case .applicationInfoError, .unsupportedPlatform, .incompatibleVersion, .installError, .productNotFound: return 7000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ enum NetworkError: Error, LocalizedError {
|
|||||||
case .unsupportedPlatform: return 2
|
case .unsupportedPlatform: return 2
|
||||||
case .incompatibleVersion: return 3
|
case .incompatibleVersion: return 3
|
||||||
case .installError: return 4
|
case .installError: return 4
|
||||||
|
case .productNotFound: return 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +162,8 @@ enum NetworkError: Error, LocalizedError {
|
|||||||
return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
|
return NSLocalizedString("下载已取消", value: "下载已取消", comment: "Download cancelled")
|
||||||
case .installError(let message):
|
case .installError(let message):
|
||||||
return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message)
|
return String(format: NSLocalizedString("安装错误: %@", value: "安装错误: %@", comment: "Install error"), message)
|
||||||
|
case .productNotFound:
|
||||||
|
return NSLocalizedString("产品未找到", value: "产品未找到", comment: "Product not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
// 下面是所有全局变量的私有存储
|
// 下面是所有全局变量的私有存储
|
||||||
private var _globalStiResult: NewParseResult?
|
private var _globalStiResult: NewParseResult?
|
||||||
private var _globalCcmResult: NewParseResult?
|
private var _globalCcmResult: NewParseResult?
|
||||||
|
private var _globalProducts: [Product]?
|
||||||
|
private var _globalUniqueProducts: [UniqueProduct]?
|
||||||
private var _globalCdn: String = ""
|
private var _globalCdn: String = ""
|
||||||
private var _globalNetworkService: NewNetworkService?
|
private var _globalNetworkService: NewNetworkService?
|
||||||
private var _globalNetworkManager: NetworkManager?
|
private var _globalNetworkManager: NetworkManager?
|
||||||
@@ -96,6 +98,30 @@ var globalCancelTracker: CancelTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var globalProducts: [Product] {
|
||||||
|
get {
|
||||||
|
if _globalProducts == nil {
|
||||||
|
_globalProducts = []
|
||||||
|
}
|
||||||
|
return _globalProducts!
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_globalProducts = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalUniqueProducts: [UniqueProduct] {
|
||||||
|
get {
|
||||||
|
if _globalUniqueProducts == nil {
|
||||||
|
_globalUniqueProducts = []
|
||||||
|
}
|
||||||
|
return _globalUniqueProducts!
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_globalUniqueProducts = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getAllProducts() -> [Product] {
|
func getAllProducts() -> [Product] {
|
||||||
var allProducts = [Product]()
|
var allProducts = [Product]()
|
||||||
let stiProducts = globalStiResult.products
|
let stiProducts = globalStiResult.products
|
||||||
@@ -108,3 +134,93 @@ func getAllProducts() -> [Product] {
|
|||||||
}
|
}
|
||||||
return allProducts
|
return allProducts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 根据产品ID和版本号快速查找产品
|
||||||
|
/// - Parameters:
|
||||||
|
/// - id: 产品ID
|
||||||
|
/// - version: 版本号(可选)
|
||||||
|
/// - Returns: 如果提供版本号,返回指定版本的产品;否则返回最新版本的产品
|
||||||
|
func findProduct(id: String, version: String? = nil) -> Product? {
|
||||||
|
// 首先在全局产品列表中查找匹配ID的产品
|
||||||
|
guard let product = globalProducts.first(where: { $0.id == id }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定版本,直接返回找到的产品
|
||||||
|
guard let version = version else {
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查产品的平台和语言集是否包含指定版本
|
||||||
|
for platform in product.platforms {
|
||||||
|
for languageSet in platform.languageSet {
|
||||||
|
if languageSet.productVersion == version {
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取产品的所有可用版本
|
||||||
|
/// - Parameter id: 产品ID
|
||||||
|
/// - Returns: 版本号数组,已去重并按版本号排序
|
||||||
|
func getProductVersions(id: String) -> [String] {
|
||||||
|
guard let product = globalProducts.first(where: { $0.id == id }) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有平台和语言集中的版本号
|
||||||
|
var versions = Set<String>()
|
||||||
|
for platform in product.platforms {
|
||||||
|
for languageSet in platform.languageSet {
|
||||||
|
versions.insert(languageSet.productVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为数组并按版本号排序(降序)
|
||||||
|
return Array(versions).sorted { version1, version2 in
|
||||||
|
version1.compare(version2, options: .numeric) == .orderedDescending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取产品在指定版本下的所有可用语言
|
||||||
|
/// - Parameters:
|
||||||
|
/// - id: 产品ID
|
||||||
|
/// - version: 版本号
|
||||||
|
/// - Returns: 语言代码数组
|
||||||
|
func getProductLanguages(id: String, version: String) -> [String] {
|
||||||
|
guard let product = globalProducts.first(where: { $0.id == id }) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var languages = Set<String>()
|
||||||
|
for platform in product.platforms {
|
||||||
|
for languageSet in platform.languageSet {
|
||||||
|
if languageSet.productVersion == version {
|
||||||
|
languages.insert(languageSet.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(languages).sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查找所有匹配指定ID的产品
|
||||||
|
/// - Parameter id: 产品ID
|
||||||
|
/// - Returns: 匹配的产品数组
|
||||||
|
func findProducts(id: String) -> [Product] {
|
||||||
|
var matchedProducts = [Product]()
|
||||||
|
|
||||||
|
// 从 globalProducts 中查找
|
||||||
|
matchedProducts.append(contentsOf: globalProducts.filter { $0.id == id })
|
||||||
|
|
||||||
|
// 从 globalCcmResult 中查找
|
||||||
|
matchedProducts.append(contentsOf: globalCcmResult.products.filter { $0.id == id })
|
||||||
|
|
||||||
|
// 从 globalStiResult 中查找
|
||||||
|
matchedProducts.append(contentsOf: globalStiResult.products.filter { $0.id == id })
|
||||||
|
|
||||||
|
return matchedProducts
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ struct ContentView: View {
|
|||||||
@State private var showDownloadManager = false
|
@State private var showDownloadManager = false
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@State private var currentApiVersion = StorageData.shared.apiVersion
|
@State private var currentApiVersion = StorageData.shared.apiVersion
|
||||||
@State private var cachedProducts: [UniqueProduct] = []
|
|
||||||
|
|
||||||
private var apiVersion: String {
|
private var apiVersion: String {
|
||||||
get { StorageData.shared.apiVersion }
|
get { StorageData.shared.apiVersion }
|
||||||
@@ -17,13 +16,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var filteredProducts: [UniqueProduct] {
|
private var filteredProducts: [UniqueProduct] {
|
||||||
if searchText.isEmpty {
|
if searchText.isEmpty { return globalUniqueProducts }
|
||||||
return cachedProducts
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedProducts.filter {
|
return globalUniqueProducts.filter {
|
||||||
$0.displayName.localizedCaseInsensitiveContains(searchText) ||
|
$0.displayName.localizedCaseInsensitiveContains(searchText) || $0.id.localizedCaseInsensitiveContains(searchText)
|
||||||
$0.id.localizedCaseInsensitiveContains(searchText)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,22 +27,14 @@ struct ContentView: View {
|
|||||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateProductsCache() {
|
private func refreshData() {
|
||||||
// 先获取有效的产品
|
isRefreshing = true
|
||||||
let validProducts = globalCcmResult.products
|
errorMessage = nil
|
||||||
.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
|
|
||||||
|
Task {
|
||||||
// 使用字典合并相同ID的产品
|
await globalNetworkManager.fetchProducts()
|
||||||
var uniqueProductsDict = [String: UniqueProduct]()
|
await MainActor.run { isRefreshing = false }
|
||||||
for product in validProducts {
|
|
||||||
uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为数组并按显示名称排序
|
|
||||||
let uniqueProducts = Array(uniqueProductsDict.values)
|
|
||||||
.sorted { $0.displayName < $1.displayName }
|
|
||||||
|
|
||||||
cachedProducts = uniqueProducts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -60,9 +48,7 @@ struct ContentView: View {
|
|||||||
await globalNetworkManager.fetchProducts()
|
await globalNetworkManager.fetchProducts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)) {
|
)) { Text("Apple Silicon") }
|
||||||
Text("Apple Silicon")
|
|
||||||
}
|
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.tint(.green)
|
.tint(.green)
|
||||||
.disabled(isRefreshing)
|
.disabled(isRefreshing)
|
||||||
@@ -137,7 +123,7 @@ struct ContentView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
Text("Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader")
|
Text("Adobe Downloader 完全免费: https://github.com/X1a0He/Adobe-Downloader")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
@@ -204,8 +190,8 @@ struct ContentView: View {
|
|||||||
],
|
],
|
||||||
spacing: 20
|
spacing: 20
|
||||||
) {
|
) {
|
||||||
ForEach(filteredProducts, id: \.sapCode) { sap in
|
ForEach(filteredProducts, id: \.id) { uniqueProduct in
|
||||||
AppCardView(sap: sap)
|
AppCardView(uniqueProduct: uniqueProduct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -224,63 +210,30 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showDownloadManager) {
|
.sheet(isPresented: $showDownloadManager) { DownloadManagerView() }
|
||||||
DownloadManagerView()
|
.onAppear { if globalCcmResult.products.isEmpty { refreshData() } }
|
||||||
.environmentObject(globalNetworkManager)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if globalCcmResult.products.isEmpty {
|
|
||||||
refreshData()
|
|
||||||
} else {
|
|
||||||
updateProductsCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: globalNetworkManager.saps) { _ in
|
|
||||||
updateProductsCache()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshData() {
|
struct SearchField: View {
|
||||||
isRefreshing = true
|
@Binding var text: String
|
||||||
errorMessage = nil
|
|
||||||
|
|
||||||
Task {
|
var body: some View {
|
||||||
await globalNetworkManager.fetchProducts()
|
HStack {
|
||||||
await MainActor.run {
|
Image(systemName: "magnifyingglass")
|
||||||
updateProductsCache()
|
.foregroundColor(.secondary)
|
||||||
isRefreshing = false
|
TextField("搜索应用", text: $text)
|
||||||
}
|
.textFieldStyle(PlainTextFieldStyle())
|
||||||
}
|
if !text.isEmpty {
|
||||||
}
|
Button(action: { text = "" }) {
|
||||||
}
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
struct SearchField: View {
|
}
|
||||||
@Binding var text: String
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "magnifyingglass")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
TextField("搜索应用", text: $text)
|
|
||||||
.textFieldStyle(PlainTextFieldStyle())
|
|
||||||
if !text.isEmpty {
|
|
||||||
Button(action: { text = "" }) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
}
|
||||||
|
.padding(8)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
.padding(8)
|
|
||||||
.background(Color(NSColor.controlBackgroundColor))
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
let networkManager = NetworkManager()
|
|
||||||
|
|
||||||
return ContentView()
|
|
||||||
.environmentObject(networkManager)
|
|
||||||
.frame(width: 792, height: 600)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,13 +55,9 @@ class NewNetworkService {
|
|||||||
|
|
||||||
let products = globalCcmResult.products
|
let products = globalCcmResult.products
|
||||||
|
|
||||||
if products.isEmpty {
|
if products.isEmpty { return ([], []) }
|
||||||
return ([], [])
|
|
||||||
}
|
|
||||||
|
|
||||||
let validProducts = products.filter {
|
let validProducts = products.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) }
|
||||||
$0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
var uniqueProductsDict = [String: UniqueProduct]()
|
var uniqueProductsDict = [String: UniqueProduct]()
|
||||||
for product in validProducts {
|
for product in validProducts {
|
||||||
|
|||||||
@@ -49,16 +49,10 @@ class NetworkManager: ObservableObject {
|
|||||||
func fetchProducts() async {
|
func fetchProducts() async {
|
||||||
loadingState = .loading
|
loadingState = .loading
|
||||||
do {
|
do {
|
||||||
let (saps, sapCodes) = try await globalNetworkService.fetchProductsData()
|
let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
||||||
|
|
||||||
let (newProducts, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
|
||||||
print("新产品数量: \(newProducts.count), 唯一产品数量: \(uniqueProducts.count), CDN: \(globalCdn)")
|
|
||||||
for uniqueProduct in uniqueProducts {
|
|
||||||
print("新唯一产品: \(uniqueProduct)")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
globalProducts = products
|
||||||
|
globalUniqueProducts = uniqueProducts.sorted { $0.displayName < $1.displayName }
|
||||||
self.loadingState = .success
|
self.loadingState = .success
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -153,8 +147,10 @@ class NetworkManager: ObservableObject {
|
|||||||
|
|
||||||
while retryCount < maxRetries {
|
while retryCount < maxRetries {
|
||||||
do {
|
do {
|
||||||
let (saps, sapCodes) = try await globalNetworkService.fetchProductsData()
|
let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
globalProducts = products
|
||||||
|
globalUniqueProducts = uniqueProducts
|
||||||
self.loadingState = .success
|
self.loadingState = .success
|
||||||
self.isFetchingProducts = false
|
self.isFetchingProducts = false
|
||||||
}
|
}
|
||||||
@@ -290,18 +286,18 @@ class NetworkManager: ObservableObject {
|
|||||||
return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid)
|
return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isVersionDownloaded(product: Product, version: String, language: String) -> URL? {
|
func isVersionDownloaded(productId: String, version: String, language: String) -> URL? {
|
||||||
if let task = downloadTasks.first(where: {
|
if let task = downloadTasks.first(where: {
|
||||||
$0.sapCode == sap.sapCode &&
|
$0.productId == productId &&
|
||||||
$0.version == version &&
|
$0.productVersion == version &&
|
||||||
$0.language == language &&
|
$0.language == language &&
|
||||||
!$0.status.isCompleted
|
!$0.status.isCompleted
|
||||||
}) { return task.directory }
|
}) { return task.directory }
|
||||||
|
|
||||||
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
let platform = globalProducts.first(where: { $0.id == productId })?.platforms.first?.id ?? "unknown"
|
||||||
let fileName = sap.sapCode == "APRO"
|
let fileName = productId == "APRO"
|
||||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
? "Adobe Downloader \(productId)_\(version)_\(platform).dmg"
|
||||||
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
: "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)"
|
||||||
|
|
||||||
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
||||||
let defaultPath = URL(fileURLWithPath: defaultDirectory)
|
let defaultPath = URL(fileURLWithPath: defaultDirectory)
|
||||||
@@ -344,7 +340,7 @@ class NetworkManager: ObservableObject {
|
|||||||
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
for task in savedTasks {
|
for task in savedTasks {
|
||||||
for product in task.productsToDownload {
|
for product in task.dependenciesToDownload {
|
||||||
product.updateCompletedPackages()
|
product.updateCompletedPackages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,7 +379,7 @@ class NetworkManager: ObservableObject {
|
|||||||
for task in downloadTasks {
|
for task in downloadTasks {
|
||||||
if case .paused(let info) = task.status,
|
if case .paused(let info) = task.status,
|
||||||
info.reason == .networkIssue {
|
info.reason == .networkIssue {
|
||||||
await downloadUtils.resumeDownloadTask(taskId: task.id)
|
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,7 +387,7 @@ class NetworkManager: ObservableObject {
|
|||||||
private func pauseActiveTasks() async {
|
private func pauseActiveTasks() async {
|
||||||
for task in downloadTasks {
|
for task in downloadTasks {
|
||||||
if case .downloading = task.status {
|
if case .downloading = task.status {
|
||||||
await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
|
await globalNewDownloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,9 @@ class NewDownloadUtils {
|
|||||||
if condition.contains("[OSArchitecture]==\(AppStatics.architectureSymbol)") {
|
if condition.contains("[OSArchitecture]==\(AppStatics.architectureSymbol)") {
|
||||||
shouldDownload = true
|
shouldDownload = true
|
||||||
}
|
}
|
||||||
|
if condition.contains("[OSArchitecture]==x64") {
|
||||||
|
shouldDownload = true
|
||||||
|
}
|
||||||
if condition.contains(installLanguage) || task.language == "ALL" {
|
if condition.contains(installLanguage) || task.language == "ALL" {
|
||||||
shouldDownload = true
|
shouldDownload = true
|
||||||
}
|
}
|
||||||
@@ -311,7 +314,6 @@ class NewDownloadUtils {
|
|||||||
if !FileManager.default.fileExists(atPath: driverPath.path) {
|
if !FileManager.default.fileExists(atPath: driverPath.path) {
|
||||||
if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) {
|
if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) {
|
||||||
let driverXml = generateDriverXML(
|
let driverXml = generateDriverXML(
|
||||||
sapCode: task.productId,
|
|
||||||
version: task.productVersion,
|
version: task.productVersion,
|
||||||
language: task.language,
|
language: task.language,
|
||||||
productInfo: productInfo,
|
productInfo: productInfo,
|
||||||
@@ -412,19 +414,19 @@ class NewDownloadUtils {
|
|||||||
|
|
||||||
func handleError(_ taskId: UUID, _ error: Error) async {
|
func handleError(_ taskId: UUID, _ error: Error) async {
|
||||||
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
|
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
|
||||||
guard task != nil else { return }
|
guard let task = task else { return }
|
||||||
|
|
||||||
let (errorMessage, isRecoverable) = classifyError(error)
|
let (errorMessage, isRecoverable) = classifyError(error)
|
||||||
|
|
||||||
if isRecoverable,
|
if isRecoverable,
|
||||||
let downloadTask = await globalCancelTracker?.downloadTasks[taskId] {
|
let downloadTask = await globalCancelTracker.downloadTasks[taskId] {
|
||||||
let resumeData = await withCheckedContinuation { continuation in
|
let resumeData = await withCheckedContinuation { continuation in
|
||||||
downloadTask.cancel(byProducingResumeData: { data in
|
downloadTask.cancel(byProducingResumeData: { data in
|
||||||
continuation.resume(returning: data)
|
continuation.resume(returning: data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if let resumeData = resumeData {
|
if let resumeData = resumeData {
|
||||||
await globalCancelTracker?.storeResumeData(taskId, data: resumeData)
|
await globalCancelTracker.storeResumeData(taskId, data: resumeData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +443,7 @@ class NewDownloadUtils {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
|
try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
|
||||||
if await !(globalCancelTracker?.isCancelled(taskId) ?? false) {
|
if await globalCancelTracker.isCancelled(taskId) == false {
|
||||||
await resumeDownloadTask(taskId: taskId)
|
await resumeDownloadTask(taskId: taskId)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -458,7 +460,7 @@ class NewDownloadUtils {
|
|||||||
|
|
||||||
if let currentPackage = task.currentPackage {
|
if let currentPackage = task.currentPackage {
|
||||||
let destinationDir = task.directory
|
let destinationDir = task.directory
|
||||||
.appendingPathComponent("\(task.productId)")
|
.appendingPathComponent("\(task.productId ?? "")")
|
||||||
let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
|
let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
|
||||||
try? FileManager.default.removeItem(at: fileURL)
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
@@ -473,16 +475,18 @@ class NewDownloadUtils {
|
|||||||
|
|
||||||
func resumeDownloadTask(taskId: UUID) async {
|
func resumeDownloadTask(taskId: UUID) async {
|
||||||
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
|
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
|
||||||
guard task != nil else { return }
|
guard let task = task else { return }
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
let totalPackages = task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }
|
||||||
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
|
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
|
||||||
fileName: task.currentPackage?.fullPackageName ?? "",
|
fileName: task.currentPackage?.fullPackageName ?? "",
|
||||||
currentPackageIndex: 0,
|
currentPackageIndex: 0,
|
||||||
totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count },
|
totalPackages: totalPackages,
|
||||||
startTime: Date(),
|
startTime: Date(),
|
||||||
estimatedTimeRemaining: nil
|
estimatedTimeRemaining: nil
|
||||||
)))
|
)))
|
||||||
|
task.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
await globalNetworkManager.saveTask(task)
|
await globalNetworkManager.saveTask(task)
|
||||||
@@ -491,7 +495,7 @@ class NewDownloadUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if task.productId == "APRO" {
|
if task.productId == "APRO" {
|
||||||
if let resumeData = await globalCancelTracker?.getResumeData(taskId),
|
if let resumeData = await globalCancelTracker.getResumeData(taskId),
|
||||||
let currentPackage = task.currentPackage,
|
let currentPackage = task.currentPackage,
|
||||||
let product = task.dependenciesToDownload.first {
|
let product = task.dependenciesToDownload.first {
|
||||||
try? await downloadPackage(
|
try? await downloadPackage(
|
||||||
@@ -598,9 +602,9 @@ class NewDownloadUtils {
|
|||||||
|
|
||||||
product.updateCompletedPackages()
|
product.updateCompletedPackages()
|
||||||
}
|
}
|
||||||
await globalNetworkManager.saveTask(task)
|
await globalNetworkManager?.saveTask(task)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
globalNetworkManager.objectWillChange.send()
|
globalNetworkManager?.objectWillChange.send()
|
||||||
}
|
}
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
}
|
}
|
||||||
@@ -643,7 +647,7 @@ class NewDownloadUtils {
|
|||||||
lastUpdateTime = now
|
lastUpdateTime = now
|
||||||
lastBytes = totalBytesWritten
|
lastBytes = totalBytesWritten
|
||||||
|
|
||||||
globalNetworkManager.objectWillChange.send()
|
globalNetworkManager?.objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -664,27 +668,27 @@ class NewDownloadUtils {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session)
|
await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||||
await globalCancelTracker?.clearResumeData(task.id)
|
await globalCancelTracker.clearResumeData(task.id)
|
||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Product, displayName: String) -> String {
|
func generateDriverXML(version: String, language: String, productInfo: Product, displayName: String) -> String {
|
||||||
// 获取匹配的 platform 和 languageSet
|
// 获取匹配的 platform 和 languageSet
|
||||||
guard let platform = productInfo.platforms.first(where: { $0.id == "mac" }),
|
guard let platform = globalProducts.first(where: { $0.id == productInfo.id })?.platforms.first?.id,
|
||||||
let languageSet = platform.languageSet.first else {
|
let languageSet = globalProducts.first(where: { $0.id == productInfo.id })?.platforms.first?.languageSet else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建依赖列表
|
// 构建依赖列表
|
||||||
let dependencies = languageSet.dependencies.map { dependency in
|
let dependencies = languageSet.first?.dependencies.map { dependency in
|
||||||
"""
|
"""
|
||||||
<Dependency>
|
<Dependency>
|
||||||
<SAPCode>\(dependency.sapCode)</SAPCode>
|
<SAPCode>\(productInfo.id)</SAPCode>
|
||||||
<BaseVersion>\(dependency.baseVersion)</BaseVersion>
|
<BaseVersion>\(languageSet.first?.baseVersion)</BaseVersion>
|
||||||
<EsdDirectory>\(dependency.sapCode)</EsdDirectory>
|
<EsdDirectory>\(productInfo.id)</EsdDirectory>
|
||||||
</Dependency>
|
</Dependency>
|
||||||
"""
|
"""
|
||||||
}.joined(separator: "\n")
|
}.joined(separator: "\n")
|
||||||
@@ -693,10 +697,10 @@ class NewDownloadUtils {
|
|||||||
<DriverInfo>
|
<DriverInfo>
|
||||||
<ProductInfo>
|
<ProductInfo>
|
||||||
<n>Adobe \(displayName)</n>
|
<n>Adobe \(displayName)</n>
|
||||||
<SAPCode>\(sapCode)</SAPCode>
|
<SAPCode>\(productInfo.id)</SAPCode>
|
||||||
<CodexVersion>\(version)</CodexVersion>
|
<CodexVersion>\(version)</CodexVersion>
|
||||||
<Platform>mac</Platform>
|
<Platform>mac</Platform>
|
||||||
<EsdDirectory>\(sapCode)</EsdDirectory>
|
<EsdDirectory>\(productInfo.id)</EsdDirectory>
|
||||||
<Dependencies>
|
<Dependencies>
|
||||||
\(dependencies)
|
\(dependencies)
|
||||||
</Dependencies>
|
</Dependencies>
|
||||||
@@ -811,11 +815,11 @@ class NewDownloadUtils {
|
|||||||
task.objectWillChange.send()
|
task.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
await globalNetworkManager.saveTask(task)
|
await globalNetworkManager?.saveTask(task)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
globalNetworkManager.updateDockBadge()
|
globalNetworkManager?.updateDockBadge()
|
||||||
globalNetworkManager.objectWillChange.send()
|
globalNetworkManager?.objectWillChange.send()
|
||||||
}
|
}
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
}
|
}
|
||||||
@@ -842,10 +846,10 @@ class NewDownloadUtils {
|
|||||||
lastBytes = totalBytesWritten
|
lastBytes = totalBytesWritten
|
||||||
|
|
||||||
task.objectWillChange.send()
|
task.objectWillChange.send()
|
||||||
globalNetworkManager.objectWillChange.send()
|
globalNetworkManager?.objectWillChange.send()
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await globalNetworkManager.saveTask(task)
|
await globalNetworkManager?.saveTask(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -860,20 +864,19 @@ class NewDownloadUtils {
|
|||||||
let downloadTask = session.downloadTask(with: downloadRequest)
|
let downloadTask = session.downloadTask(with: downloadRequest)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session)
|
await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
||||||
|
|
||||||
if await (globalCancelTracker?.isCancelled(task.id) ?? false) {
|
if await globalCancelTracker.isCancelled(task.id) {
|
||||||
continuation.resume(throwing: NetworkError.cancelled)
|
continuation.resume(throwing: NetworkError.cancelled)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
|
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
|
||||||
let task = await globalCancelTracker?.downloadTasks[taskId]
|
let task = await globalCancelTracker.downloadTasks[taskId]
|
||||||
if let downloadTask = task {
|
if let downloadTask = task {
|
||||||
let data = await withCheckedContinuation { continuation in
|
let data = await withCheckedContinuation { continuation in
|
||||||
downloadTask.cancel(byProducingResumeData: { data in
|
downloadTask.cancel(byProducingResumeData: { data in
|
||||||
@@ -881,7 +884,7 @@ class NewDownloadUtils {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if let data = data {
|
if let data = data {
|
||||||
await globalCancelTracker?.storeResumeData(taskId, data: data)
|
await globalCancelTracker.storeResumeData(taskId, data: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,4 +947,223 @@ class NewDownloadUtils {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func executePrivilegedCommand(_ command: String) async -> String {
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
PrivilegedHelperManager.shared.executeCommand(command) { result in
|
||||||
|
if result.starts(with: "Error:") {
|
||||||
|
print("命令执行失败: \(command)")
|
||||||
|
print("错误信息: \(result)")
|
||||||
|
}
|
||||||
|
continuation.resume(returning: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadX1a0HeCCPackages(
|
||||||
|
progressHandler: @escaping (Double, String) -> Void,
|
||||||
|
cancellationHandler: @escaping () -> Bool,
|
||||||
|
shouldProcess: Bool = true
|
||||||
|
) async throws {
|
||||||
|
let baseUrl = "https://cdn-ffc.oobesaas.adobe.com/core/v1/applications?name=CreativeCloud&platform=\(AppStatics.isAppleSilicon ? "macarm64" : "osx10")"
|
||||||
|
|
||||||
|
let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||||
|
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let configuration = URLSessionConfiguration.default
|
||||||
|
configuration.timeoutIntervalForRequest = 30
|
||||||
|
configuration.timeoutIntervalForResource = 300
|
||||||
|
configuration.httpAdditionalHeaders = NetworkConstants.downloadHeaders
|
||||||
|
let session = URLSession(configuration: configuration)
|
||||||
|
|
||||||
|
do {
|
||||||
|
var request = URLRequest(url: URL(string: baseUrl)!)
|
||||||
|
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
throw NetworkError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
let xmlDoc = try XMLDocument(data: data)
|
||||||
|
|
||||||
|
let packageSets = try xmlDoc.nodes(forXPath: "//packageSet[name='ADC']")
|
||||||
|
guard let adcPackageSet = packageSets.first else {
|
||||||
|
throw NetworkError.invalidData("找不到ADC包集")
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetPackages = ["HDBox", "IPCBox"]
|
||||||
|
var packagesToDownload: [(name: String, url: URL, size: Int64)] = []
|
||||||
|
|
||||||
|
for packageName in targetPackages {
|
||||||
|
let packageNodes = try adcPackageSet.nodes(forXPath: ".//package[name='\(packageName)']")
|
||||||
|
guard let package = packageNodes.first else {
|
||||||
|
print("未找到包: \(packageName)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let manifestUrl = try package.nodes(forXPath: ".//manifestUrl").first?.stringValue,
|
||||||
|
let cdnBase = try xmlDoc.nodes(forXPath: "//cdn/secure").first?.stringValue else {
|
||||||
|
print("无法获取manifest URL或CDN基础URL")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifestFullUrl = cdnBase + manifestUrl
|
||||||
|
|
||||||
|
var manifestRequest = URLRequest(url: URL(string: manifestFullUrl)!)
|
||||||
|
NetworkConstants.downloadHeaders.forEach { manifestRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||||
|
let (manifestData, manifestResponse) = try await session.data(for: manifestRequest)
|
||||||
|
|
||||||
|
guard let manifestHttpResponse = manifestResponse as? HTTPURLResponse,
|
||||||
|
(200...299).contains(manifestHttpResponse.statusCode) else {
|
||||||
|
print("获取manifest失败: HTTP \(String(describing: (manifestResponse as? HTTPURLResponse)?.statusCode))")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifestDoc = try XMLDocument(data: manifestData)
|
||||||
|
let assetPathNodes = try manifestDoc.nodes(forXPath: "//asset_path")
|
||||||
|
let sizeNodes = try manifestDoc.nodes(forXPath: "//asset_size")
|
||||||
|
guard let assetPath = assetPathNodes.first?.stringValue,
|
||||||
|
let sizeStr = sizeNodes.first?.stringValue,
|
||||||
|
let size = Int64(sizeStr),
|
||||||
|
let downloadUrl = URL(string: assetPath) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packagesToDownload.append((packageName, downloadUrl, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !packagesToDownload.isEmpty else {
|
||||||
|
throw NetworkError.invalidData("没有找到可下载的包")
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalCount = packagesToDownload.count
|
||||||
|
for (index, package) in packagesToDownload.enumerated() {
|
||||||
|
if cancellationHandler() {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
progressHandler(Double(index) / Double(totalCount), "正在下载 \(package.name)...")
|
||||||
|
}
|
||||||
|
|
||||||
|
let destinationURL = tempDirectory.appendingPathComponent("\(package.name).zip")
|
||||||
|
var downloadRequest = URLRequest(url: package.url)
|
||||||
|
print(downloadRequest)
|
||||||
|
NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||||
|
let (downloadURL, downloadResponse) = try await session.download(for: downloadRequest)
|
||||||
|
|
||||||
|
guard let httpResponse = downloadResponse as? HTTPURLResponse,
|
||||||
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
print("下载失败: HTTP \(String(describing: (downloadResponse as? HTTPURLResponse)?.statusCode))")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try FileManager.default.moveItem(at: downloadURL, to: destinationURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
progressHandler(0.9, shouldProcess ? "正在安装组件..." : "正在完成下载...")
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetDirectory = "/Library/Application\\ Support/Adobe/Adobe\\ Desktop\\ Common"
|
||||||
|
let rawTargetDirectory = "/Library/Application Support/Adobe/Adobe Desktop Common"
|
||||||
|
|
||||||
|
if !FileManager.default.fileExists(atPath: rawTargetDirectory) {
|
||||||
|
let createDirResult = await executePrivilegedCommand("/bin/mkdir -p \(targetDirectory)")
|
||||||
|
if createDirResult.starts(with: "Error:") {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.installError("创建目录失败: \(createDirResult)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let chmodResult = await executePrivilegedCommand("/bin/chmod 755 \(targetDirectory)")
|
||||||
|
if chmodResult.starts(with: "Error:") {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.installError("设置权限失败: \(chmodResult)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for package in packagesToDownload {
|
||||||
|
let packageDir = "\(targetDirectory)/\(package.name)"
|
||||||
|
|
||||||
|
let removeResult = await executePrivilegedCommand("/bin/rm -rf \(packageDir)")
|
||||||
|
if removeResult.starts(with: "Error:") {
|
||||||
|
print("移除旧目录失败: \(removeResult)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mkdirResult = await executePrivilegedCommand("/bin/mkdir -p \(packageDir)")
|
||||||
|
if mkdirResult.starts(with: "Error:") {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.installError("创建 \(package.name) 目录失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
let unzipResult = await executePrivilegedCommand("cd \(packageDir) && /usr/bin/unzip -o '\(tempDirectory.path)/\(package.name).zip'")
|
||||||
|
if unzipResult.starts(with: "Error:") {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.installError("解压 \(package.name) 失败: \(unzipResult)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let chmodResult = await executePrivilegedCommand("/bin/chmod -R 755 \(packageDir)")
|
||||||
|
if chmodResult.starts(with: "Error:") {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.installError("设置 \(package.name) 权限失败: \(chmodResult)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let chownResult = await executePrivilegedCommand("/usr/sbin/chown -R root:wheel \(packageDir)")
|
||||||
|
if chownResult.starts(with: "Error:") {
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
throw NetworkError.installError("设置 \(package.name) 所有者失败: \(chownResult)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
if shouldProcess {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
ModifySetup.backupAndModifySetupFile { success, message in
|
||||||
|
if success {
|
||||||
|
continuation.resume()
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: NetworkError.installError(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModifySetup.clearVersionCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
try? FileManager.default.removeItem(at: tempDirectory)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
progressHandler(1.0, shouldProcess ? "安装完成" : "下载完成")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("发生错误: \(error.localizedDescription)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
|
||||||
|
await globalCancelTracker.cancel(taskId)
|
||||||
|
|
||||||
|
if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) {
|
||||||
|
if removeFiles {
|
||||||
|
try? FileManager.default.removeItem(at: task.directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
||||||
|
message: String(localized: "下载已取消"),
|
||||||
|
error: NetworkError.downloadCancelled,
|
||||||
|
timestamp: Date(),
|
||||||
|
recoverable: false
|
||||||
|
)))
|
||||||
|
|
||||||
|
await globalNetworkManager.saveTask(task)
|
||||||
|
await MainActor.run {
|
||||||
|
globalNetworkManager.updateDockBadge()
|
||||||
|
globalNetworkManager.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ class TaskPersistenceManager {
|
|||||||
self.cancelTracker = tracker
|
self.cancelTracker = tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getTaskFileName(sapCode: String, version: String, language: String, platform: String) -> String {
|
private func getTaskFileName(productId: String, version: String, language: String, platform: String) -> String {
|
||||||
return sapCode == "APRO"
|
return productId == "APRO"
|
||||||
? "Adobe Downloader \(sapCode)_\(version)_\(platform)-task.json"
|
? "Adobe Downloader \(productId)_\(version)_\(platform)-task.json"
|
||||||
: "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json"
|
: "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)-task.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveTask(_ task: NewDownloadTask) async {
|
func saveTask(_ task: NewDownloadTask) async {
|
||||||
let fileName = getTaskFileName(
|
let fileName = getTaskFileName(
|
||||||
sapCode: task.productId,
|
productId: task.productId,
|
||||||
version: task.productVersion,
|
version: task.productVersion,
|
||||||
language: task.language,
|
language: task.language,
|
||||||
platform: task.platform
|
platform: task.platform
|
||||||
@@ -209,7 +209,7 @@ class TaskPersistenceManager {
|
|||||||
|
|
||||||
func removeTask(_ task: NewDownloadTask) {
|
func removeTask(_ task: NewDownloadTask) {
|
||||||
let fileName = getTaskFileName(
|
let fileName = getTaskFileName(
|
||||||
sapCode: task.productId,
|
productId: task.productId,
|
||||||
version: task.productVersion,
|
version: task.productVersion,
|
||||||
language: task.language,
|
language: task.language,
|
||||||
platform: task.platform
|
platform: task.platform
|
||||||
@@ -220,16 +220,16 @@ class TaskPersistenceManager {
|
|||||||
try? fileManager.removeItem(at: fileURL)
|
try? fileManager.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createExistingProgramTask(sapCode: String, version: String, language: String, displayName: String, platform: String, directory: URL) async {
|
func createExistingProgramTask(productId: String, version: String, language: String, displayName: String, platform: String, directory: URL) async {
|
||||||
let fileName = getTaskFileName(
|
let fileName = getTaskFileName(
|
||||||
sapCode: sapCode,
|
productId: productId,
|
||||||
version: version,
|
version: version,
|
||||||
language: language,
|
language: language,
|
||||||
platform: platform
|
platform: platform
|
||||||
)
|
)
|
||||||
|
|
||||||
let product = DependenciesToDownload(
|
let product = DependenciesToDownload(
|
||||||
sapCode: sapCode,
|
sapCode: productId,
|
||||||
version: version,
|
version: version,
|
||||||
buildGuid: "",
|
buildGuid: "",
|
||||||
applicationJson: ""
|
applicationJson: ""
|
||||||
@@ -249,7 +249,7 @@ class TaskPersistenceManager {
|
|||||||
product.packages = [package]
|
product.packages = [package]
|
||||||
|
|
||||||
let task = NewDownloadTask(
|
let task = NewDownloadTask(
|
||||||
productId: sapCode,
|
productId: productId,
|
||||||
productVersion: version,
|
productVersion: version,
|
||||||
language: language,
|
language: language,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
|
|||||||
@@ -277,17 +277,19 @@ struct GeneralSettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
ScrollView {
|
||||||
DownloadSettingsView(viewModel: viewModel)
|
VStack(spacing: 16) {
|
||||||
HelperSettingsView(viewModel: viewModel,
|
DownloadSettingsView(viewModel: viewModel)
|
||||||
showHelperAlert: $showHelperAlert,
|
HelperSettingsView(viewModel: viewModel,
|
||||||
helperAlertMessage: $helperAlertMessage,
|
showHelperAlert: $showHelperAlert,
|
||||||
helperAlertSuccess: $helperAlertSuccess)
|
helperAlertMessage: $helperAlertMessage,
|
||||||
CCSettingsView(viewModel: viewModel)
|
helperAlertSuccess: $helperAlertSuccess)
|
||||||
UpdateSettingsView(viewModel: viewModel)
|
CCSettingsView(viewModel: viewModel)
|
||||||
CleanConfigView()
|
UpdateSettingsView(viewModel: viewModel)
|
||||||
|
CleanConfigView()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.alert(helperAlertSuccess ? "操作成功" : "操作失败", isPresented: $showHelperAlert) {
|
.alert(helperAlertSuccess ? "操作成功" : "操作失败", isPresented: $showHelperAlert) {
|
||||||
Button("确定") { }
|
Button("确定") { }
|
||||||
@@ -298,28 +300,7 @@ struct GeneralSettingsView: View {
|
|||||||
Button("取消", role: .cancel) { }
|
Button("取消", role: .cancel) { }
|
||||||
Button("下载") {
|
Button("下载") {
|
||||||
Task {
|
Task {
|
||||||
viewModel.isDownloadingSetup = true
|
await downloadSetup(shouldProcess: false)
|
||||||
viewModel.isCancelled = false
|
|
||||||
do {
|
|
||||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
|
||||||
progressHandler: { progress, status in
|
|
||||||
viewModel.setupDownloadProgress = progress
|
|
||||||
viewModel.setupDownloadStatus = status
|
|
||||||
},
|
|
||||||
cancellationHandler: { viewModel.isCancelled }
|
|
||||||
)
|
|
||||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
|
||||||
viewModel.isSuccess = true
|
|
||||||
viewModel.alertMessage = String(localized: "Setup 组件安装成功")
|
|
||||||
} catch NetworkError.cancelled {
|
|
||||||
viewModel.isSuccess = false
|
|
||||||
viewModel.alertMessage = String(localized: "下载已取消")
|
|
||||||
} catch {
|
|
||||||
viewModel.isSuccess = false
|
|
||||||
viewModel.alertMessage = error.localizedDescription
|
|
||||||
}
|
|
||||||
viewModel.showAlert = true
|
|
||||||
viewModel.isDownloadingSetup = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
@@ -329,29 +310,7 @@ struct GeneralSettingsView: View {
|
|||||||
Button("取消", role: .cancel) { }
|
Button("取消", role: .cancel) { }
|
||||||
Button("确定") {
|
Button("确定") {
|
||||||
Task {
|
Task {
|
||||||
viewModel.isDownloadingSetup = true
|
await downloadSetup(shouldProcess: true)
|
||||||
viewModel.isCancelled = false
|
|
||||||
do {
|
|
||||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
|
||||||
progressHandler: { progress, status in
|
|
||||||
viewModel.setupDownloadProgress = progress
|
|
||||||
viewModel.setupDownloadStatus = status
|
|
||||||
},
|
|
||||||
cancellationHandler: { viewModel.isCancelled },
|
|
||||||
shouldProcess: true
|
|
||||||
)
|
|
||||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
|
||||||
viewModel.isSuccess = true
|
|
||||||
viewModel.alertMessage = String(localized: "X1a0He CC 下载并处理成功")
|
|
||||||
} catch NetworkError.cancelled {
|
|
||||||
viewModel.isSuccess = false
|
|
||||||
viewModel.alertMessage = String(localized: "下载已取消")
|
|
||||||
} catch {
|
|
||||||
viewModel.isSuccess = false
|
|
||||||
viewModel.alertMessage = error.localizedDescription
|
|
||||||
}
|
|
||||||
viewModel.showAlert = true
|
|
||||||
viewModel.isDownloadingSetup = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
@@ -361,29 +320,7 @@ struct GeneralSettingsView: View {
|
|||||||
Button("取消", role: .cancel) { }
|
Button("取消", role: .cancel) { }
|
||||||
Button("确定") {
|
Button("确定") {
|
||||||
Task {
|
Task {
|
||||||
viewModel.isDownloadingSetup = true
|
await downloadSetup(shouldProcess: false)
|
||||||
viewModel.isCancelled = false
|
|
||||||
do {
|
|
||||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
|
||||||
progressHandler: { progress, status in
|
|
||||||
viewModel.setupDownloadProgress = progress
|
|
||||||
viewModel.setupDownloadStatus = status
|
|
||||||
},
|
|
||||||
cancellationHandler: { viewModel.isCancelled },
|
|
||||||
shouldProcess: false
|
|
||||||
)
|
|
||||||
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
|
||||||
viewModel.isSuccess = true
|
|
||||||
viewModel.alertMessage = String(localized: "X1a0He CC 下载成功")
|
|
||||||
} catch NetworkError.cancelled {
|
|
||||||
viewModel.isSuccess = false
|
|
||||||
viewModel.alertMessage = String(localized: "下载已取消")
|
|
||||||
} catch {
|
|
||||||
viewModel.isSuccess = false
|
|
||||||
viewModel.alertMessage = error.localizedDescription
|
|
||||||
}
|
|
||||||
viewModel.showAlert = true
|
|
||||||
viewModel.isDownloadingSetup = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
@@ -401,6 +338,34 @@ struct GeneralSettingsView: View {
|
|||||||
viewModel.objectWillChange.send()
|
viewModel.objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func downloadSetup(shouldProcess: Bool) async {
|
||||||
|
viewModel.isDownloadingSetup = true
|
||||||
|
viewModel.isCancelled = false
|
||||||
|
do {
|
||||||
|
try await globalNewDownloadUtils.downloadX1a0HeCCPackages(
|
||||||
|
progressHandler: { progress, status in
|
||||||
|
viewModel.setupDownloadProgress = progress
|
||||||
|
viewModel.setupDownloadStatus = status
|
||||||
|
},
|
||||||
|
cancellationHandler: { viewModel.isCancelled },
|
||||||
|
shouldProcess: shouldProcess
|
||||||
|
)
|
||||||
|
viewModel.setupVersion = ModifySetup.checkComponentVersion()
|
||||||
|
viewModel.isSuccess = true
|
||||||
|
viewModel.alertMessage = shouldProcess ?
|
||||||
|
String(localized: "X1a0He CC 下载并处理成功") :
|
||||||
|
String(localized: "X1a0He CC 下载成功")
|
||||||
|
} catch NetworkError.cancelled {
|
||||||
|
viewModel.isSuccess = false
|
||||||
|
viewModel.alertMessage = String(localized: "下载已取消")
|
||||||
|
} catch {
|
||||||
|
viewModel.isSuccess = false
|
||||||
|
viewModel.alertMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
viewModel.showAlert = true
|
||||||
|
viewModel.isDownloadingSetup = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DownloadSettingsView: View {
|
struct DownloadSettingsView: View {
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
@Published var pendingLanguage = ""
|
@Published var pendingLanguage = ""
|
||||||
@Published var showRedownloadConfirm = false
|
@Published var showRedownloadConfirm = false
|
||||||
|
|
||||||
let sap: Sap
|
let uniqueProduct: UniqueProduct
|
||||||
weak var networkManager: NetworkManager?
|
|
||||||
|
|
||||||
@Published var isDownloading = false
|
@Published var isDownloading = false
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
@@ -67,10 +66,9 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(sap: Sap, networkManager: NetworkManager?) {
|
init(uniqueProduct: UniqueProduct) {
|
||||||
self.sap = sap
|
self.uniqueProduct = uniqueProduct
|
||||||
self.networkManager = networkManager
|
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
setupObservers()
|
setupObservers()
|
||||||
}
|
}
|
||||||
@@ -78,12 +76,12 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func setupObservers() {
|
private func setupObservers() {
|
||||||
networkManager?.$downloadTasks
|
globalNetworkManager.$downloadTasks
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] tasks in
|
.sink { [weak self] tasks in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let hasActiveTask = tasks.contains {
|
let hasActiveTask = tasks.contains {
|
||||||
$0.sapCode == self.sap.sapCode && self.isTaskActive($0.status)
|
$0.productId == self.uniqueProduct.id && self.isTaskActive($0.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasActiveTask != self.isDownloading {
|
if hasActiveTask != self.isDownloading {
|
||||||
@@ -93,7 +91,7 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
networkManager?.objectWillChange
|
globalNetworkManager.objectWillChange
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.updateDownloadingStatus()
|
self?.updateDownloadingStatus()
|
||||||
@@ -114,13 +112,8 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func updateDownloadingStatus() {
|
func updateDownloadingStatus() {
|
||||||
guard let networkManager = networkManager else {
|
let hasActiveTask = globalNetworkManager.downloadTasks.contains {
|
||||||
self.isDownloading = false
|
$0.productId == uniqueProduct.id && isTaskActive($0.status)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasActiveTask = networkManager.downloadTasks.contains {
|
|
||||||
$0.sapCode == sap.sapCode && isTaskActive($0.status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasActiveTask != self.isDownloading {
|
if hasActiveTask != self.isDownloading {
|
||||||
@@ -130,10 +123,10 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDestinationURL(version: String, language: String) async throws -> URL {
|
func getDestinationURL(version: String, language: String) async throws -> URL {
|
||||||
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
let platform = globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.id
|
||||||
let installerName = sap.sapCode == "APRO"
|
let installerName = uniqueProduct.id == "APRO"
|
||||||
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
? "Adobe Downloader \(uniqueProduct.id)_\(version)_\(platform).dmg"
|
||||||
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
: "Adobe Downloader \(uniqueProduct.id)_\(version)-\(language)-\(platform)"
|
||||||
|
|
||||||
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
||||||
return URL(fileURLWithPath: defaultDirectory)
|
return URL(fileURLWithPath: defaultDirectory)
|
||||||
@@ -167,10 +160,9 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadIcon() {
|
func loadIcon() {
|
||||||
if let bestIcon = sap.getBestIcon(),
|
if let bestIcon = globalProducts.first(where: { $0.id == uniqueProduct.id })?.getBestIcon(),
|
||||||
let iconURL = URL(string: bestIcon.url) {
|
let iconURL = URL(string: bestIcon.value) {
|
||||||
|
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) {
|
||||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
|
|
||||||
self.iconImage = cachedImage
|
self.iconImage = cachedImage
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -189,20 +181,20 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
if let image = NSImage(data: data) {
|
if let image = NSImage(data: data) {
|
||||||
IconCache.shared.setIcon(image, for: bestIcon.url)
|
IconCache.shared.setIcon(image, for: bestIcon.value)
|
||||||
self.iconImage = image
|
self.iconImage = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
if let localImage = NSImage(named: sap.sapCode) {
|
if let localImage = NSImage(named: uniqueProduct.id) {
|
||||||
self.iconImage = localImage
|
self.iconImage = localImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let localImage = NSImage(named: sap.sapCode) {
|
if let localImage = NSImage(named: uniqueProduct.id) {
|
||||||
self.iconImage = localImage
|
self.iconImage = localImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,37 +214,32 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkAndStartDownload(version: String, language: String) async {
|
func checkAndStartDownload(version: String, language: String) async {
|
||||||
if let networkManager = networkManager {
|
if let existingPath = globalNetworkManager.isVersionDownloaded(productId: uniqueProduct.id, version: version, language: language) {
|
||||||
if let existingPath = networkManager.isVersionDownloaded(sap: sap, version: version, language: language) {
|
await MainActor.run {
|
||||||
await MainActor.run {
|
existingFilePath = existingPath
|
||||||
existingFilePath = existingPath
|
pendingVersion = version
|
||||||
pendingVersion = version
|
pendingLanguage = language
|
||||||
pendingLanguage = language
|
showExistingFileAlert = true
|
||||||
showExistingFileAlert = true
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
do {
|
||||||
do {
|
let destinationURL = try await getDestinationURL(version: version, language: language)
|
||||||
let destinationURL = try await getDestinationURL(version: version, language: language)
|
try await globalNetworkManager.startDownload(
|
||||||
try await networkManager.startDownload(
|
productId: uniqueProduct.id,
|
||||||
sap: sap,
|
selectedVersion: version,
|
||||||
selectedVersion: version,
|
language: language,
|
||||||
language: language,
|
destinationURL: destinationURL
|
||||||
destinationURL: destinationURL
|
)
|
||||||
)
|
} catch {
|
||||||
} catch {
|
handleError(error)
|
||||||
handleError(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCompletedTask(_ path: URL) async {
|
func createCompletedTask(_ path: URL) async {
|
||||||
guard let networkManager = networkManager,
|
let existingTask = globalNetworkManager.downloadTasks.first { task in
|
||||||
let productInfo = sap.versions[pendingVersion] else { return }
|
return task.productId == uniqueProduct.id &&
|
||||||
|
task.productVersion == pendingVersion &&
|
||||||
let existingTask = networkManager.downloadTasks.first { task in
|
|
||||||
return task.sapCode == sap.sapCode &&
|
|
||||||
task.version == pendingVersion &&
|
|
||||||
task.language == pendingLanguage &&
|
task.language == pendingLanguage &&
|
||||||
task.directory == path
|
task.directory == path
|
||||||
}
|
}
|
||||||
@@ -262,27 +249,24 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await TaskPersistenceManager.shared.createExistingProgramTask(
|
await TaskPersistenceManager.shared.createExistingProgramTask(
|
||||||
sapCode: sap.sapCode,
|
productId: uniqueProduct.id,
|
||||||
version: pendingVersion,
|
version: pendingVersion,
|
||||||
language: pendingLanguage,
|
language: pendingLanguage,
|
||||||
displayName: sap.displayName,
|
displayName: uniqueProduct.displayName,
|
||||||
platform: productInfo.apPlatform,
|
platform: globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.id ?? "unknown",
|
||||||
directory: path
|
directory: path
|
||||||
)
|
)
|
||||||
|
|
||||||
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
networkManager.downloadTasks = savedTasks
|
globalNetworkManager.downloadTasks = savedTasks
|
||||||
networkManager.updateDockBadge()
|
globalNetworkManager.updateDockBadge()
|
||||||
networkManager.objectWillChange.send()
|
globalNetworkManager.objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dependenciesCount: Int {
|
var dependenciesCount: Int {
|
||||||
if let firstVersion = sap.versions.first?.value {
|
return globalProducts.first(where: { $0.id == uniqueProduct.id })?.platforms.first?.languageSet.first?.dependencies.count ?? 0
|
||||||
return firstVersion.dependencies.count
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasValidIcon: Bool {
|
var hasValidIcon: Bool {
|
||||||
@@ -304,12 +288,11 @@ final class AppCardViewModel: ObservableObject {
|
|||||||
|
|
||||||
struct AppCardView: View {
|
struct AppCardView: View {
|
||||||
@StateObject private var viewModel: AppCardViewModel
|
@StateObject private var viewModel: AppCardViewModel
|
||||||
@EnvironmentObject private var networkManager: NetworkManager
|
|
||||||
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
||||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||||
|
|
||||||
init(sap: Sap) {
|
init(uniqueProduct: UniqueProduct) {
|
||||||
_viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil))
|
_viewModel = StateObject(wrappedValue: AppCardViewModel(uniqueProduct: uniqueProduct))
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -322,18 +305,19 @@ struct AppCardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(CardModifier())
|
.modifier(CardModifier())
|
||||||
.modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager))
|
.modifier(SheetModifier(viewModel: viewModel))
|
||||||
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
|
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
|
||||||
.onAppear(perform: setupViewModel)
|
.onAppear(perform: setupViewModel)
|
||||||
.onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus)
|
.onChange(of: globalNetworkManager.downloadTasks.count) { _ in
|
||||||
|
updateDownloadStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupViewModel() {
|
private func setupViewModel() {
|
||||||
viewModel.networkManager = networkManager
|
|
||||||
viewModel.updateDownloadingStatus()
|
viewModel.updateDownloadingStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateDownloadStatus(_ _: [NewDownloadTask]) {
|
private func updateDownloadStatus() {
|
||||||
viewModel.updateDownloadingStatus()
|
viewModel.updateDownloadingStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,16 +363,19 @@ private struct ProductInfoView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text(viewModel.sap.displayName)
|
Text(viewModel.uniqueProduct.displayName)
|
||||||
.font(.system(size: AppCardConstants.titleFontSize))
|
.font(.system(size: AppCardConstants.titleFontSize))
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Text("可用版本: \(viewModel.sap.versions.count)")
|
let product = findProduct(id: viewModel.uniqueProduct.id)
|
||||||
|
let versions = Set(product?.platforms.first?.languageSet.map { $0.productVersion } ?? [])
|
||||||
|
let dependenciesCount = product?.platforms.first?.languageSet.first?.dependencies.count ?? 0
|
||||||
|
Text("可用版本: \(versions.count)")
|
||||||
Text("|")
|
Text("|")
|
||||||
Text("依赖包: \(viewModel.dependenciesCount)")
|
Text("依赖包: \(dependenciesCount)")
|
||||||
}
|
}
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
@@ -437,23 +424,23 @@ private struct CardModifier: ViewModifier {
|
|||||||
|
|
||||||
private struct SheetModifier: ViewModifier {
|
private struct SheetModifier: ViewModifier {
|
||||||
@ObservedObject var viewModel: AppCardViewModel
|
@ObservedObject var viewModel: AppCardViewModel
|
||||||
let networkManager: NetworkManager
|
|
||||||
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
||||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.sheet(isPresented: $viewModel.showVersionPicker) {
|
.sheet(isPresented: $viewModel.showVersionPicker) {
|
||||||
VersionPickerView(sap: viewModel.sap) { version in
|
if let product = findProduct(id: viewModel.uniqueProduct.id) {
|
||||||
Task {
|
VersionPickerView(product: product) { version in
|
||||||
await viewModel.handleDownloadRequest(
|
Task {
|
||||||
version,
|
await viewModel.handleDownloadRequest(
|
||||||
useDefaultLanguage: useDefaultLanguage,
|
version,
|
||||||
defaultLanguage: defaultLanguage
|
useDefaultLanguage: useDefaultLanguage,
|
||||||
)
|
defaultLanguage: defaultLanguage
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(networkManager)
|
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $viewModel.showLanguagePicker) {
|
.sheet(isPresented: $viewModel.showLanguagePicker) {
|
||||||
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
||||||
@@ -482,10 +469,9 @@ struct AlertModifier: ViewModifier {
|
|||||||
viewModel.showExistingFileAlert = false
|
viewModel.showExistingFileAlert = false
|
||||||
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
||||||
Task {
|
Task {
|
||||||
if let networkManager = viewModel.networkManager,
|
if !globalNetworkManager.downloadTasks.contains(where: { task in
|
||||||
!networkManager.downloadTasks.contains(where: { task in
|
task.productId == viewModel.uniqueProduct.id &&
|
||||||
task.sapCode == viewModel.sap.sapCode &&
|
task.productVersion == viewModel.pendingVersion &&
|
||||||
task.version == viewModel.pendingVersion &&
|
|
||||||
task.language == viewModel.pendingLanguage
|
task.language == viewModel.pendingLanguage
|
||||||
}) {
|
}) {
|
||||||
await viewModel.createCompletedTask(path)
|
await viewModel.createCompletedTask(path)
|
||||||
@@ -540,12 +526,10 @@ struct AlertModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startRedownload() async {
|
private func startRedownload() async {
|
||||||
guard let networkManager = viewModel.networkManager else { return }
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
networkManager.downloadTasks.removeAll { task in
|
globalNetworkManager.downloadTasks.removeAll { task in
|
||||||
task.sapCode == viewModel.sap.sapCode &&
|
task.productId == viewModel.uniqueProduct.id &&
|
||||||
task.version == viewModel.pendingVersion &&
|
task.productVersion == viewModel.pendingVersion &&
|
||||||
task.language == viewModel.pendingLanguage
|
task.language == viewModel.pendingLanguage
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,8 +542,8 @@ struct AlertModifier: ViewModifier {
|
|||||||
language: viewModel.pendingLanguage
|
language: viewModel.pendingLanguage
|
||||||
)
|
)
|
||||||
|
|
||||||
try await networkManager.startDownload(
|
try await globalNetworkManager.startDownload(
|
||||||
sap: viewModel.sap,
|
productId: viewModel.uniqueProduct.id,
|
||||||
selectedVersion: viewModel.pendingVersion,
|
selectedVersion: viewModel.pendingVersion,
|
||||||
language: viewModel.pendingLanguage,
|
language: viewModel.pendingLanguage,
|
||||||
destinationURL: destinationURL
|
destinationURL: destinationURL
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct DownloadManagerView: View {
|
struct DownloadManagerView: View {
|
||||||
@EnvironmentObject private var networkManager: NetworkManager
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var sortOrder: SortOrder = .addTime
|
@State private var sortOrder: SortOrder = .addTime
|
||||||
@@ -27,7 +26,7 @@ struct DownloadManagerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func removeTask(_ task: NewDownloadTask) {
|
private func removeTask(_ task: NewDownloadTask) {
|
||||||
networkManager.removeTask(taskId: task.id)
|
globalNetworkManager.removeTask(taskId: task.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sortTasks(_ tasks: [NewDownloadTask]) -> [NewDownloadTask] {
|
private func sortTasks(_ tasks: [NewDownloadTask]) -> [NewDownloadTask] {
|
||||||
@@ -47,123 +46,163 @@ struct DownloadManagerView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack {
|
DownloadManagerToolbar(
|
||||||
Text("下载管理")
|
sortOrder: $sortOrder,
|
||||||
.font(.headline)
|
dismiss: dismiss
|
||||||
Spacer()
|
)
|
||||||
HStack(){
|
DownloadTaskList(
|
||||||
Menu {
|
tasks: sortTasks(globalNetworkManager.downloadTasks),
|
||||||
ForEach([SortOrder.addTime, .name, .status], id: \.self) { order in
|
removeTask: removeTask
|
||||||
Button(action: {
|
)
|
||||||
sortOrder = order
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Text(order.description)
|
|
||||||
if sortOrder == order {
|
|
||||||
Image(systemName: "checkmark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "arrow.up.arrow.down")
|
|
||||||
Text(sortOrder.description)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 120)
|
|
||||||
.fixedSize()
|
|
||||||
|
|
||||||
Button("全部暂停") {
|
|
||||||
Task {
|
|
||||||
for task in networkManager.downloadTasks {
|
|
||||||
if case .downloading = task.status {
|
|
||||||
await networkManager.downloadUtils.pauseDownloadTask(
|
|
||||||
taskId: task.id,
|
|
||||||
reason: .userRequested
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("全部继续") {
|
|
||||||
Task {
|
|
||||||
for task in networkManager.downloadTasks {
|
|
||||||
if case .paused = task.status {
|
|
||||||
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("清理已完成") {
|
|
||||||
networkManager.downloadTasks.removeAll { task in
|
|
||||||
if case .completed = task.status {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
networkManager.updateDockBadge()
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("关闭") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.tint(.red)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
ScrollView(showsIndicators: false) {
|
|
||||||
LazyVStack(spacing: 8) {
|
|
||||||
ForEach(sortTasks(networkManager.downloadTasks)) { task in
|
|
||||||
DownloadProgressView(
|
|
||||||
task: task,
|
|
||||||
onCancel: {
|
|
||||||
Task {
|
|
||||||
await networkManager.downloadUtils.cancelDownloadTask(taskId: task.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPause: {
|
|
||||||
Task {
|
|
||||||
await networkManager.downloadUtils.pauseDownloadTask(
|
|
||||||
taskId: task.id,
|
|
||||||
reason: .userRequested
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onResume: {
|
|
||||||
Task {
|
|
||||||
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRetry: {
|
|
||||||
Task {
|
|
||||||
await networkManager.downloadUtils.resumeDownloadTask(taskId: task.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRemove: {
|
|
||||||
removeTask(task)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
.background(Color(NSColor.windowBackgroundColor))
|
|
||||||
}
|
}
|
||||||
.frame(width:800, height: 600)
|
.frame(width:800, height: 600)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct DownloadManagerToolbar: View {
|
||||||
|
@Binding var sortOrder: DownloadManagerView.SortOrder
|
||||||
|
let dismiss: DismissAction
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text("下载管理")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
SortMenuView(sortOrder: $sortOrder)
|
||||||
|
.frame(minWidth: 120)
|
||||||
|
.fixedSize()
|
||||||
|
|
||||||
|
ToolbarButtons(dismiss: dismiss)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ToolbarButtons: View {
|
||||||
|
let dismiss: DismissAction
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
Button("全部暂停") {
|
||||||
|
Task {
|
||||||
|
for task in globalNetworkManager.downloadTasks {
|
||||||
|
if case .downloading = task.status {
|
||||||
|
await globalNewDownloadUtils.pauseDownloadTask(
|
||||||
|
taskId: task.id,
|
||||||
|
reason: .userRequested
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("全部继续") {
|
||||||
|
Task {
|
||||||
|
for task in globalNetworkManager.downloadTasks {
|
||||||
|
if case .paused = task.status {
|
||||||
|
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("清理已完成") {
|
||||||
|
globalNetworkManager.downloadTasks.removeAll { task in
|
||||||
|
if case .completed = task.status {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
globalNetworkManager.updateDockBadge()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("关闭") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DownloadTaskList: View {
|
||||||
|
let tasks: [NewDownloadTask]
|
||||||
|
let removeTask: (NewDownloadTask) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
LazyVStack(spacing: 8) {
|
||||||
|
ForEach(tasks) { task in
|
||||||
|
DownloadProgressView(
|
||||||
|
task: task,
|
||||||
|
onCancel: { TaskOperations.cancelTask(task) },
|
||||||
|
onPause: { TaskOperations.pauseTask(task) },
|
||||||
|
onResume: { TaskOperations.resumeTask(task) },
|
||||||
|
onRetry: { TaskOperations.resumeTask(task) },
|
||||||
|
onRemove: { removeTask(task) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
.background(Color(NSColor.windowBackgroundColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TaskOperations {
|
||||||
|
static func cancelTask(_ task: NewDownloadTask) {
|
||||||
|
Task {
|
||||||
|
await globalNewDownloadUtils.cancelDownloadTask(taskId: task.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func pauseTask(_ task: NewDownloadTask) {
|
||||||
|
Task {
|
||||||
|
await globalNewDownloadUtils.pauseDownloadTask(
|
||||||
|
taskId: task.id,
|
||||||
|
reason: .userRequested
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func resumeTask(_ task: NewDownloadTask) {
|
||||||
|
Task {
|
||||||
|
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension DownloadManagerView.SortOrder: Hashable {}
|
extension DownloadManagerView.SortOrder: Hashable {}
|
||||||
|
|
||||||
|
struct SortMenuView: View {
|
||||||
|
@Binding var sortOrder: DownloadManagerView.SortOrder
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach([DownloadManagerView.SortOrder.addTime, .name, .status], id: \.self) { order in
|
||||||
|
Button(action: {
|
||||||
|
sortOrder = order
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(order.description)
|
||||||
|
if sortOrder == order {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "arrow.up.arrow.down")
|
||||||
|
Text(sortOrder.description)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
DownloadManagerView()
|
DownloadManagerView()
|
||||||
.environmentObject(NetworkManager())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct DownloadProgressView: View {
|
struct DownloadProgressView: View {
|
||||||
@EnvironmentObject private var networkManager: NetworkManager
|
|
||||||
@ObservedObject var task: NewDownloadTask
|
@ObservedObject var task: NewDownloadTask
|
||||||
let onCancel: () -> Void
|
let onCancel: () -> Void
|
||||||
let onPause: () -> Void
|
let onPause: () -> Void
|
||||||
@@ -26,7 +25,7 @@ struct DownloadProgressView: View {
|
|||||||
private var statusLabel: some View {
|
private var statusLabel: some View {
|
||||||
Text(task.status.description)
|
Text(task.status.description)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(statusColor)
|
.foregroundColor(.white)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.background(statusBackgroundColor)
|
.background(statusBackgroundColor)
|
||||||
@@ -131,7 +130,7 @@ struct DownloadProgressView: View {
|
|||||||
showInstallPrompt = false
|
showInstallPrompt = false
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
Task {
|
Task {
|
||||||
await networkManager.installProduct(at: task.directory)
|
await globalNetworkManager.installProduct(at: task.directory)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
showSetupProcessAlert = true
|
showSetupProcessAlert = true
|
||||||
@@ -145,7 +144,7 @@ struct DownloadProgressView: View {
|
|||||||
showInstallPrompt = false
|
showInstallPrompt = false
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
Task {
|
Task {
|
||||||
await networkManager.installProduct(at: task.directory)
|
await globalNetworkManager.installProduct(at: task.directory)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
showSetupProcessAlert = true
|
showSetupProcessAlert = true
|
||||||
@@ -205,7 +204,7 @@ struct DownloadProgressView: View {
|
|||||||
showInstallPrompt = false
|
showInstallPrompt = false
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
Task {
|
Task {
|
||||||
await networkManager.installProduct(at: task.directory)
|
await globalNetworkManager.installProduct(at: task.directory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
@@ -217,18 +216,18 @@ struct DownloadProgressView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $isInstalling) {
|
.sheet(isPresented: $isInstalling) {
|
||||||
Group {
|
Group {
|
||||||
if case .installing(let progress, let status) = networkManager.installationState {
|
if case .installing(let progress, let status) = globalNetworkManager.installationState {
|
||||||
InstallProgressView(
|
InstallProgressView(
|
||||||
productName: task.displayName,
|
productName: task.displayName,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
status: status,
|
status: status,
|
||||||
onCancel: {
|
onCancel: {
|
||||||
networkManager.cancelInstallation()
|
globalNetworkManager.cancelInstallation()
|
||||||
isInstalling = false
|
isInstalling = false
|
||||||
},
|
},
|
||||||
onRetry: nil
|
onRetry: nil
|
||||||
)
|
)
|
||||||
} else if case .completed = networkManager.installationState {
|
} else if case .completed = globalNetworkManager.installationState {
|
||||||
InstallProgressView(
|
InstallProgressView(
|
||||||
productName: task.displayName,
|
productName: task.displayName,
|
||||||
progress: 1.0,
|
progress: 1.0,
|
||||||
@@ -238,7 +237,7 @@ struct DownloadProgressView: View {
|
|||||||
},
|
},
|
||||||
onRetry: nil
|
onRetry: nil
|
||||||
)
|
)
|
||||||
} else if case .failed(let error) = networkManager.installationState {
|
} else if case .failed(let error) = globalNetworkManager.installationState {
|
||||||
InstallProgressView(
|
InstallProgressView(
|
||||||
productName: task.displayName,
|
productName: task.displayName,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
@@ -248,7 +247,7 @@ struct DownloadProgressView: View {
|
|||||||
},
|
},
|
||||||
onRetry: {
|
onRetry: {
|
||||||
Task {
|
Task {
|
||||||
await networkManager.retryInstallation(at: task.directory)
|
await globalNetworkManager.retryInstallation(at: task.directory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -258,7 +257,7 @@ struct DownloadProgressView: View {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
status: String(localized: "准备安装..."),
|
status: String(localized: "准备安装..."),
|
||||||
onCancel: {
|
onCancel: {
|
||||||
networkManager.cancelInstallation()
|
globalNetworkManager.cancelInstallation()
|
||||||
isInstalling = false
|
isInstalling = false
|
||||||
},
|
},
|
||||||
onRetry: nil
|
onRetry: nil
|
||||||
@@ -301,12 +300,12 @@ struct DownloadProgressView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func loadIcon() {
|
private func loadIcon() {
|
||||||
let product = globalCcmResult.products.first { $0.id == task.productId }
|
let product = findProduct(id: task.productId)
|
||||||
if product != nil {
|
if product != nil {
|
||||||
if let bestIcon = product.getBestIcon(),
|
if let bestIcon = product?.getBestIcon(),
|
||||||
let iconURL = URL(string: bestIcon.url) {
|
let iconURL = URL(string: bestIcon.value) {
|
||||||
|
|
||||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
|
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) {
|
||||||
self.iconImage = cachedImage
|
self.iconImage = cachedImage
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -324,13 +323,13 @@ struct DownloadProgressView: View {
|
|||||||
throw URLError(.badServerResponse)
|
throw URLError(.badServerResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconCache.shared.setIcon(image, for: bestIcon.url)
|
IconCache.shared.setIcon(image, for: bestIcon.value)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.iconImage = image
|
self.iconImage = image
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if let localImage = NSImage(named: task.sapCode) {
|
if let localImage = NSImage(named: task.productId) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.iconImage = localImage
|
self.iconImage = localImage
|
||||||
}
|
}
|
||||||
@@ -357,184 +356,20 @@ struct DownloadProgressView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack(spacing: 12) {
|
TaskHeaderView(iconImage: iconImage, task: task, loadIcon: loadIcon, formatPath: formatPath, openInFinder: openInFinder)
|
||||||
Group {
|
|
||||||
if let iconImage = iconImage {
|
|
||||||
Image(nsImage: iconImage)
|
|
||||||
.resizable()
|
|
||||||
.interpolation(.high)
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
} else {
|
|
||||||
Image(systemName: "app.fill")
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
.onAppear(perform: loadIcon)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text(task.displayName)
|
|
||||||
.font(.headline)
|
|
||||||
Text(task.productVersion)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
statusLabel
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(formatPath(task.directory.path))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.middle)
|
|
||||||
.onTapGesture {
|
|
||||||
openInFinder(task.directory.path)
|
|
||||||
}
|
|
||||||
.help(task.directory.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
TaskProgressView(task: task, formatRemainingTime: formatRemainingTime, formatSpeed: formatSpeed)
|
||||||
HStack {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text(task.formattedDownloadedSize)
|
|
||||||
Text("/")
|
|
||||||
Text(task.formattedTotalSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if task.totalSpeed > 0 {
|
|
||||||
Text(formatRemainingTime(
|
|
||||||
totalSize: task.totalSize,
|
|
||||||
downloadedSize: task.totalDownloadedSize,
|
|
||||||
speed: task.totalSpeed
|
|
||||||
))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("\(Int(task.totalProgress * 100))%")
|
|
||||||
|
|
||||||
if task.totalSpeed > 0 {
|
|
||||||
Text(formatSpeed(task.totalSpeed))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
|
|
||||||
ProgressView(value: task.totalProgress)
|
|
||||||
.progressViewStyle(.linear)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !task.dependenciesToDownload.isEmpty {
|
if !task.dependenciesToDownload.isEmpty {
|
||||||
Divider()
|
Divider()
|
||||||
|
PackageListView(
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
task: task,
|
||||||
HStack {
|
isPackageListExpanded: $isPackageListExpanded,
|
||||||
Button(action: {
|
showCommandLineInstall: $showCommandLineInstall,
|
||||||
withAnimation {
|
showCopiedAlert: $showCopiedAlert,
|
||||||
isPackageListExpanded.toggle()
|
expandedProducts: $expandedProducts,
|
||||||
}
|
actionButtons: AnyView(actionButtons)
|
||||||
}) {
|
)
|
||||||
HStack {
|
|
||||||
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("产品和包列表")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Button(action: {
|
|
||||||
let containerURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
|
||||||
let tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
|
|
||||||
let fileName = "\(task.sapCode == "APRO" ? "Adobe Downloader \(task.sapCode)_\(task.version)_\(task.platform)" : "Adobe Downloader \(task.sapCode)_\(task.version)-\(task.language)-\(task.platform)")-task.json"
|
|
||||||
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
|
||||||
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: tasksDirectory.path)
|
|
||||||
}) {
|
|
||||||
Label("查看持久化文件", systemImage: "doc.text.magnifyingglass")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.tint(.blue)
|
|
||||||
.controlSize(.regular)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if case .completed = task.status, task.sapCode != "APRO" {
|
|
||||||
Button(action: {
|
|
||||||
showCommandLineInstall.toggle()
|
|
||||||
}) {
|
|
||||||
Label("命令行安装", systemImage: "terminal")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.tint(.purple)
|
|
||||||
.controlSize(.regular)
|
|
||||||
.popover(isPresented: $showCommandLineInstall, arrowEdge: .bottom) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Button("复制命令") {
|
|
||||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
|
||||||
let driverPath = "\(task.directory.path)/driver.xml"
|
|
||||||
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
|
||||||
let pasteboard = NSPasteboard.general
|
|
||||||
pasteboard.clearContents()
|
|
||||||
pasteboard.setString(command, forType: .string)
|
|
||||||
showCopiedAlert = true
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
||||||
showCopiedAlert = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if showCopiedAlert {
|
|
||||||
Text("已复制")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.green)
|
|
||||||
}
|
|
||||||
|
|
||||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
|
||||||
let driverPath = "\(task.directory.path)/driver.xml"
|
|
||||||
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
|
||||||
Text(command)
|
|
||||||
.font(.system(.caption, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.padding(8)
|
|
||||||
.background(Color.secondary.opacity(0.1))
|
|
||||||
.cornerRadius(6)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.frame(width: 400)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actionButtons
|
|
||||||
}
|
|
||||||
|
|
||||||
if isPackageListExpanded {
|
|
||||||
ScrollView(showsIndicators: false) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
ForEach(task.productsToDownload, id: \.sapCode) { product in
|
|
||||||
ProductRow(
|
|
||||||
product: product,
|
|
||||||
isCurrentProduct: task.currentPackage?.id == product.packages.first?.id,
|
|
||||||
expandedProducts: $expandedProducts
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -548,8 +383,256 @@ struct DownloadProgressView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct TaskHeaderView: View {
|
||||||
|
let iconImage: NSImage?
|
||||||
|
let task: NewDownloadTask
|
||||||
|
let loadIcon: () -> Void
|
||||||
|
let formatPath: (String) -> String
|
||||||
|
let openInFinder: (String) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Group {
|
||||||
|
if let iconImage = iconImage {
|
||||||
|
Image(nsImage: iconImage)
|
||||||
|
.resizable()
|
||||||
|
.interpolation(.high)
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "app.fill")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.onAppear(perform: loadIcon)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(task.displayName)
|
||||||
|
.font(.headline)
|
||||||
|
Text(task.productVersion)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(formatPath(task.directory.path))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
.onTapGesture {
|
||||||
|
openInFinder(task.directory.path)
|
||||||
|
}
|
||||||
|
.help(task.directory.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusLabel: some View {
|
||||||
|
Text(task.status.description)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.background(statusBackgroundColor)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusBackgroundColor: Color {
|
||||||
|
switch task.status {
|
||||||
|
case .downloading:
|
||||||
|
return Color.blue
|
||||||
|
case .preparing:
|
||||||
|
return Color.purple.opacity(0.8)
|
||||||
|
case .completed:
|
||||||
|
return Color.green.opacity(0.8)
|
||||||
|
case .failed:
|
||||||
|
return Color.red.opacity(0.8)
|
||||||
|
case .paused:
|
||||||
|
return Color.orange.opacity(0.8)
|
||||||
|
case .waiting:
|
||||||
|
return Color.gray.opacity(0.8)
|
||||||
|
case .retrying:
|
||||||
|
return Color.yellow.opacity(0.8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TaskProgressView: View {
|
||||||
|
let task: NewDownloadTask
|
||||||
|
let formatRemainingTime: (Int64, Int64, Double) -> String
|
||||||
|
let formatSpeed: (Double) -> String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(task.formattedDownloadedSize)
|
||||||
|
Text("/")
|
||||||
|
Text(task.formattedTotalSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if task.totalSpeed > 0 {
|
||||||
|
Text(formatRemainingTime(
|
||||||
|
task.totalSize,
|
||||||
|
task.totalDownloadedSize,
|
||||||
|
task.totalSpeed
|
||||||
|
))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("\(Int(task.totalProgress * 100))%")
|
||||||
|
|
||||||
|
if task.totalSpeed > 0 {
|
||||||
|
Text(formatSpeed(task.totalSpeed))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
|
||||||
|
ProgressView(value: task.totalProgress)
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PackageListView: View {
|
||||||
|
let task: NewDownloadTask
|
||||||
|
@Binding var isPackageListExpanded: Bool
|
||||||
|
@Binding var showCommandLineInstall: Bool
|
||||||
|
@Binding var showCopiedAlert: Bool
|
||||||
|
@Binding var expandedProducts: Set<String>
|
||||||
|
let actionButtons: AnyView
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack {
|
||||||
|
Button(action: {
|
||||||
|
withAnimation {
|
||||||
|
isPackageListExpanded.toggle()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("产品和包列表")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Button(action: {
|
||||||
|
let containerURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
|
let tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
|
||||||
|
let fileName = "\(task.productId == "APRO" ? "Adobe Downloader \(task.productId)_\(task.productVersion)_\(task.platform)" : "Adobe Downloader \(task.productId)_\(task.productVersion)-\(task.language)-\(task.platform)")-task.json"
|
||||||
|
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
||||||
|
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: tasksDirectory.path)
|
||||||
|
}) {
|
||||||
|
Label("查看持久化文件", systemImage: "doc.text.magnifyingglass")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(.blue)
|
||||||
|
.controlSize(.regular)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if case .completed = task.status, task.productId != "APRO" {
|
||||||
|
CommandLineInstallButton(
|
||||||
|
task: task,
|
||||||
|
showCommandLineInstall: $showCommandLineInstall,
|
||||||
|
showCopiedAlert: $showCopiedAlert
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPackageListExpanded {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(task.dependenciesToDownload, id: \.sapCode) { product in
|
||||||
|
ProductRow(
|
||||||
|
product: product,
|
||||||
|
isCurrentProduct: task.currentPackage?.id == product.packages.first?.id,
|
||||||
|
expandedProducts: $expandedProducts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxHeight: 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CommandLineInstallButton: View {
|
||||||
|
let task: NewDownloadTask
|
||||||
|
@Binding var showCommandLineInstall: Bool
|
||||||
|
@Binding var showCopiedAlert: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
showCommandLineInstall.toggle()
|
||||||
|
}) {
|
||||||
|
Label("命令行安装", systemImage: "terminal")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(.purple)
|
||||||
|
.controlSize(.regular)
|
||||||
|
.popover(isPresented: $showCommandLineInstall, arrowEdge: .bottom) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Button("复制命令") {
|
||||||
|
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||||
|
let driverPath = "\(task.directory.path)/driver.xml"
|
||||||
|
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
pasteboard.clearContents()
|
||||||
|
pasteboard.setString(command, forType: .string)
|
||||||
|
showCopiedAlert = true
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
showCopiedAlert = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if showCopiedAlert {
|
||||||
|
Text("已复制")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
}
|
||||||
|
|
||||||
|
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||||
|
let driverPath = "\(task.directory.path)/driver.xml"
|
||||||
|
let command = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||||||
|
Text(command)
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.padding(8)
|
||||||
|
.background(Color.secondary.opacity(0.1))
|
||||||
|
.cornerRadius(6)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(width: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ProductRow: View {
|
struct ProductRow: View {
|
||||||
@ObservedObject var dependencies: DependenciesToDownload
|
@ObservedObject var product: DependenciesToDownload
|
||||||
let isCurrentProduct: Bool
|
let isCurrentProduct: Bool
|
||||||
@Binding var expandedProducts: Set<String>
|
@Binding var expandedProducts: Set<String>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct ShouldExistsSetUpView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
HeaderView()
|
SetupAlertHeaderView()
|
||||||
MessageView()
|
MessageView()
|
||||||
ButtonsView(
|
ButtonsView(
|
||||||
isDownloading: $isDownloading,
|
isDownloading: $isDownloading,
|
||||||
@@ -47,17 +47,19 @@ struct ShouldExistsSetUpView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct HeaderView: View {
|
private struct SetupAlertHeaderView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
VStack {
|
||||||
.font(.system(size: 64))
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.foregroundColor(.orange)
|
.font(.system(size: 64))
|
||||||
.padding(.bottom, 5)
|
.foregroundColor(.orange)
|
||||||
.frame(alignment: .bottomTrailing)
|
.padding(.bottom, 5)
|
||||||
|
.frame(alignment: .bottomTrailing)
|
||||||
|
|
||||||
Text("未检测到 Adobe CC 组件")
|
Text("未检测到 Adobe CC 组件")
|
||||||
.font(.system(size: 24))
|
.font(.system(size: 24))
|
||||||
.bold()
|
.bold()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +178,7 @@ private struct ButtonsView: View {
|
|||||||
isCancelled = false
|
isCancelled = false
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await networkManager.downloadUtils.downloadX1a0HeCCPackages(
|
try await globalNewDownloadUtils.downloadX1a0HeCCPackages(
|
||||||
progressHandler: { progress, status in
|
progressHandler: { progress, status in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
downloadProgress = progress
|
downloadProgress = progress
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ private enum VersionPickerConstants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct VersionPickerView: View {
|
struct VersionPickerView: View {
|
||||||
@EnvironmentObject private var networkManager: NetworkManager
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
||||||
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
@StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon
|
||||||
@@ -37,7 +36,7 @@ struct VersionPickerView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon)
|
VersionPickerHeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon)
|
||||||
VersionListView(
|
VersionListView(
|
||||||
product: product,
|
product: product,
|
||||||
expandedVersions: $expandedVersions,
|
expandedVersions: $expandedVersions,
|
||||||
@@ -49,7 +48,7 @@ struct VersionPickerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct HeaderView: View {
|
private struct VersionPickerHeaderView: View {
|
||||||
let product: Product
|
let product: Product
|
||||||
let downloadAppleSilicon: Bool
|
let downloadAppleSilicon: Bool
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@@ -160,7 +159,7 @@ private struct VersionRow: View {
|
|||||||
|
|
||||||
private var existingPath: URL? {
|
private var existingPath: URL? {
|
||||||
globalNetworkManager.isVersionDownloaded(
|
globalNetworkManager.isVersionDownloaded(
|
||||||
product: product,
|
productId: product.id,
|
||||||
version: version,
|
version: version,
|
||||||
language: defaultLanguage
|
language: defaultLanguage
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"(可能导致处理 Setup 组件失败)" : {
|
"(可能导致处理 Setup 组件失败)" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -22,7 +21,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"(将导致无法使用安装功能)" : {
|
"(将导致无法使用安装功能)" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -157,8 +155,12 @@
|
|||||||
},
|
},
|
||||||
"Adobe Downloader %@" : {
|
"Adobe Downloader %@" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Adobe Downloader 完全免费: https://github.com/X1a0He/Adobe-Downloader" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader" : {
|
"Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -288,6 +290,7 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Debug 模式" : {
|
"Debug 模式" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -422,6 +425,7 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Setup 组件安装成功" : {
|
"Setup 组件安装成功" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -769,6 +773,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"产品未找到" : {
|
||||||
|
"comment" : "Product not found"
|
||||||
|
},
|
||||||
"仅下载" : {
|
"仅下载" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -1385,6 +1392,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"将执行的命令:" : {
|
"将执行的命令:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1425,6 +1433,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"展开全部" : {
|
"展开全部" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1445,7 +1454,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"已处理" : {
|
"已处理" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1456,7 +1464,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"已备份" : {
|
"已备份" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1662,6 +1669,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"折叠全部" : {
|
"折叠全部" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2099,9 +2107,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"查看持久化文件" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"检查中" : {
|
"检查中" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
Reference in New Issue
Block a user