2024-10-31 22:35:22 +08:00
|
|
|
//
|
2024-11-05 20:30:18 +08:00
|
|
|
// Adobe Downloader
|
2024-10-31 22:35:22 +08:00
|
|
|
//
|
|
|
|
|
// Created by X1a0He on 2024/10/30.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
2024-11-09 23:15:50 +08:00
|
|
|
import Combine
|
2024-10-31 22:35:22 +08:00
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private enum AppCardConstants {
|
|
|
|
|
static let cardWidth: CGFloat = 250
|
|
|
|
|
static let cardHeight: CGFloat = 200
|
|
|
|
|
static let iconSize: CGFloat = 64
|
|
|
|
|
static let cornerRadius: CGFloat = 10
|
|
|
|
|
static let buttonHeight: CGFloat = 32
|
|
|
|
|
static let titleFontSize: CGFloat = 16
|
|
|
|
|
static let buttonFontSize: CGFloat = 14
|
|
|
|
|
|
|
|
|
|
static let shadowOpacity: Double = 0.05
|
|
|
|
|
static let shadowRadius: CGFloat = 2
|
|
|
|
|
static let strokeOpacity: Double = 0.1
|
|
|
|
|
static let strokeWidth: CGFloat = 2
|
|
|
|
|
static let backgroundOpacity: Double = 0.05
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class IconCache {
|
2024-10-31 22:35:22 +08:00
|
|
|
static let shared = IconCache()
|
2024-11-05 20:30:18 +08:00
|
|
|
private var cache = NSCache<NSString, NSImage>()
|
2024-10-31 22:35:22 +08:00
|
|
|
|
|
|
|
|
func getIcon(for url: String) -> NSImage? {
|
2024-11-05 20:30:18 +08:00
|
|
|
cache.object(forKey: url as NSString)
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setIcon(_ image: NSImage, for url: String) {
|
2024-11-05 20:30:18 +08:00
|
|
|
cache.setObject(image, forKey: url as NSString)
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
@MainActor
|
|
|
|
|
final class AppCardViewModel: ObservableObject {
|
2024-11-05 20:30:18 +08:00
|
|
|
@Published var iconImage: NSImage?
|
|
|
|
|
@Published var showError = false
|
|
|
|
|
@Published var errorMessage = ""
|
|
|
|
|
@Published var showVersionPicker = false
|
|
|
|
|
@Published var selectedVersion = ""
|
|
|
|
|
@Published var showLanguagePicker = false
|
|
|
|
|
@Published var selectedLanguage = ""
|
|
|
|
|
@Published var showExistingFileAlert = false
|
|
|
|
|
@Published var existingFilePath: URL?
|
|
|
|
|
@Published var pendingVersion = ""
|
|
|
|
|
@Published var pendingLanguage = ""
|
|
|
|
|
@Published var showRedownloadConfirm = false
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
let sap: Sap
|
2024-11-05 20:30:18 +08:00
|
|
|
weak var networkManager: NetworkManager?
|
|
|
|
|
|
|
|
|
|
@Published var isDownloading = false
|
|
|
|
|
private let userDefaults = UserDefaults.standard
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private var useDefaultDirectory: Bool {
|
|
|
|
|
StorageData.shared.useDefaultDirectory
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private var defaultDirectory: String {
|
|
|
|
|
StorageData.shared.defaultDirectory
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
init(sap: Sap, networkManager: NetworkManager?) {
|
|
|
|
|
self.sap = sap
|
|
|
|
|
self.networkManager = networkManager
|
2024-11-09 23:15:50 +08:00
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
Task { @MainActor in
|
|
|
|
|
setupObservers()
|
|
|
|
|
}
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
@MainActor
|
2024-11-09 23:15:50 +08:00
|
|
|
private func setupObservers() {
|
2024-11-15 17:47:15 +08:00
|
|
|
networkManager?.$downloadTasks
|
|
|
|
|
.receive(on: RunLoop.main)
|
|
|
|
|
.sink { [weak self] tasks in
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
let hasActiveTask = tasks.contains {
|
|
|
|
|
$0.sapCode == self.sap.sapCode && self.isTaskActive($0.status)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if hasActiveTask != self.isDownloading {
|
|
|
|
|
self.isDownloading = hasActiveTask
|
|
|
|
|
self.objectWillChange.send()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.store(in: &cancellables)
|
|
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
networkManager?.objectWillChange
|
|
|
|
|
.receive(on: RunLoop.main)
|
|
|
|
|
.sink { [weak self] _ in
|
|
|
|
|
self?.updateDownloadingStatus()
|
|
|
|
|
}
|
|
|
|
|
.store(in: &cancellables)
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private func isTaskActive(_ status: DownloadStatus) -> Bool {
|
|
|
|
|
switch status {
|
|
|
|
|
case .downloading, .preparing, .waiting, .retrying:
|
|
|
|
|
return true
|
|
|
|
|
case .paused:
|
|
|
|
|
return false
|
|
|
|
|
case .completed, .failed:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
2024-11-07 21:31:29 +08:00
|
|
|
func updateDownloadingStatus() {
|
2024-11-09 23:15:50 +08:00
|
|
|
guard let networkManager = networkManager else {
|
2024-11-15 17:47:15 +08:00
|
|
|
self.isDownloading = false
|
2024-11-09 23:15:50 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
let hasActiveTask = networkManager.downloadTasks.contains {
|
|
|
|
|
$0.sapCode == sap.sapCode && isTaskActive($0.status)
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
2024-11-15 17:47:15 +08:00
|
|
|
|
|
|
|
|
if hasActiveTask != self.isDownloading {
|
|
|
|
|
self.isDownloading = hasActiveTask
|
|
|
|
|
self.objectWillChange.send()
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
2024-11-15 17:47:15 +08:00
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
func getDestinationURL(version: String, language: String) async throws -> URL {
|
2024-11-05 20:30:18 +08:00
|
|
|
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
|
|
|
|
let installerName = sap.sapCode == "APRO"
|
2024-11-06 16:06:30 +08:00
|
|
|
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
2024-11-06 11:08:44 +08:00
|
|
|
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
|
|
|
|
return URL(fileURLWithPath: defaultDirectory)
|
|
|
|
|
.appendingPathComponent(installerName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
let panel = NSOpenPanel()
|
|
|
|
|
panel.title = "选择保存位置"
|
|
|
|
|
panel.canCreateDirectories = true
|
|
|
|
|
panel.canChooseDirectories = true
|
|
|
|
|
panel.canChooseFiles = false
|
|
|
|
|
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
|
|
|
|
|
|
|
|
|
if panel.runModal() == .OK, let selectedURL = panel.url {
|
|
|
|
|
continuation.resume(returning: selectedURL.appendingPathComponent(installerName))
|
|
|
|
|
} else {
|
|
|
|
|
continuation.resume(throwing: NetworkError.cancelled)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func handleError(_ error: Error) {
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
if case NetworkError.cancelled = error { return }
|
|
|
|
|
errorMessage = error.localizedDescription
|
|
|
|
|
showError = true
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func loadIcon() {
|
2024-11-03 00:12:38 +08:00
|
|
|
if let bestIcon = sap.getBestIcon(),
|
|
|
|
|
let iconURL = URL(string: bestIcon.url) {
|
|
|
|
|
|
|
|
|
|
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
|
|
|
|
|
self.iconImage = cachedImage
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task {
|
|
|
|
|
do {
|
|
|
|
|
var request = URLRequest(url: iconURL)
|
|
|
|
|
request.timeoutInterval = 10
|
|
|
|
|
|
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
|
|
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
2024-11-05 20:34:23 +08:00
|
|
|
(200...299).contains(httpResponse.statusCode) else {
|
2024-11-03 00:12:38 +08:00
|
|
|
throw URLError(.badServerResponse)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-31 22:35:22 +08:00
|
|
|
await MainActor.run {
|
2024-11-05 20:34:23 +08:00
|
|
|
if let image = NSImage(data: data) {
|
|
|
|
|
IconCache.shared.setIcon(image, for: bestIcon.url)
|
|
|
|
|
self.iconImage = image
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
} catch {
|
2024-11-05 20:34:23 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
if let localImage = NSImage(named: sap.sapCode) {
|
2024-11-03 00:12:38 +08:00
|
|
|
self.iconImage = localImage
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-05 20:34:23 +08:00
|
|
|
} else {
|
|
|
|
|
if let localImage = NSImage(named: sap.sapCode) {
|
|
|
|
|
self.iconImage = localImage
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func handleDownloadRequest(_ version: String, useDefaultLanguage: Bool, defaultLanguage: String) async {
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
if useDefaultLanguage {
|
|
|
|
|
Task {
|
|
|
|
|
await checkAndStartDownload(version: version, language: defaultLanguage)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
selectedVersion = version
|
|
|
|
|
showLanguagePicker = true
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func checkAndStartDownload(version: String, language: String) async {
|
|
|
|
|
if let networkManager = networkManager {
|
2024-11-15 17:47:15 +08:00
|
|
|
if let existingPath = networkManager.isVersionDownloaded(sap: sap, version: version, language: language) {
|
2024-11-05 20:30:18 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
existingFilePath = existingPath
|
|
|
|
|
pendingVersion = version
|
|
|
|
|
pendingLanguage = language
|
|
|
|
|
showExistingFileAlert = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-11-09 23:15:50 +08:00
|
|
|
do {
|
|
|
|
|
let destinationURL = try await getDestinationURL(version: version, language: language)
|
|
|
|
|
try await networkManager.startDownload(
|
|
|
|
|
sap: sap,
|
|
|
|
|
selectedVersion: version,
|
|
|
|
|
language: language,
|
|
|
|
|
destinationURL: destinationURL
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
handleError(error)
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-04 14:44:52 +08:00
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func createCompletedTask(_ path: URL) async {
|
|
|
|
|
guard let networkManager = networkManager,
|
|
|
|
|
let productInfo = sap.versions[pendingVersion] else { return }
|
2024-11-04 14:44:52 +08:00
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
let existingTask = networkManager.downloadTasks.first { task in
|
2024-11-07 16:14:42 +08:00
|
|
|
return task.sapCode == sap.sapCode &&
|
|
|
|
|
task.version == pendingVersion &&
|
|
|
|
|
task.language == pendingLanguage &&
|
|
|
|
|
task.directory == path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if existingTask != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-04 14:44:52 +08:00
|
|
|
var productsToDownload: [ProductsToDownload] = []
|
|
|
|
|
let mainProduct = ProductsToDownload(
|
|
|
|
|
sapCode: sap.sapCode,
|
|
|
|
|
version: pendingVersion,
|
|
|
|
|
buildGuid: productInfo.buildGuid
|
|
|
|
|
)
|
|
|
|
|
productsToDownload.append(mainProduct)
|
|
|
|
|
|
|
|
|
|
for dependency in productInfo.dependencies {
|
2024-11-15 17:47:15 +08:00
|
|
|
if let dependencyVersions = networkManager.saps[dependency.sapCode]?.versions {
|
2024-11-04 14:44:52 +08:00
|
|
|
let sortedVersions = dependencyVersions.sorted { first, second in
|
|
|
|
|
first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buildGuid = ""
|
|
|
|
|
for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version {
|
2024-11-18 20:33:45 +08:00
|
|
|
if StorageData.shared.allowedPlatform.contains(versionInfo.apPlatform) {
|
2024-11-04 14:44:52 +08:00
|
|
|
buildGuid = versionInfo.buildGuid
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !buildGuid.isEmpty {
|
|
|
|
|
let dependencyProduct = ProductsToDownload(
|
|
|
|
|
sapCode: dependency.sapCode,
|
|
|
|
|
version: dependency.version,
|
|
|
|
|
buildGuid: buildGuid
|
|
|
|
|
)
|
|
|
|
|
productsToDownload.append(dependencyProduct)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let completedTask = NewDownloadTask(
|
|
|
|
|
sapCode: sap.sapCode,
|
|
|
|
|
version: pendingVersion,
|
|
|
|
|
language: pendingLanguage,
|
|
|
|
|
displayName: sap.displayName,
|
|
|
|
|
directory: path,
|
|
|
|
|
productsToDownload: productsToDownload,
|
|
|
|
|
retryCount: 0,
|
|
|
|
|
createAt: Date(),
|
|
|
|
|
totalStatus: .completed(DownloadStatus.CompletionInfo(
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
totalTime: 0,
|
|
|
|
|
totalSize: 0
|
|
|
|
|
)),
|
|
|
|
|
totalProgress: 1.0,
|
|
|
|
|
totalDownloadedSize: 0,
|
|
|
|
|
totalSize: 0,
|
2024-11-09 23:15:50 +08:00
|
|
|
totalSpeed: 0,
|
|
|
|
|
platform: ""
|
2024-11-04 14:44:52 +08:00
|
|
|
)
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
await MainActor.run {
|
2024-11-04 14:44:52 +08:00
|
|
|
networkManager.downloadTasks.append(completedTask)
|
|
|
|
|
networkManager.objectWillChange.send()
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
|
|
|
|
|
var dependenciesCount: Int {
|
|
|
|
|
if let firstVersion = sap.versions.first?.value {
|
|
|
|
|
return firstVersion.dependencies.count
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
2024-11-15 17:47:15 +08:00
|
|
|
|
|
|
|
|
var hasValidIcon: Bool {
|
|
|
|
|
iconImage != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var canDownload: Bool {
|
|
|
|
|
!isDownloading
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var downloadButtonTitle: String {
|
|
|
|
|
isDownloading ? String(localized: "下载中") : String(localized: "下载")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var downloadButtonIcon: String {
|
|
|
|
|
isDownloading ? "hourglass.circle.fill" : "arrow.down.circle"
|
|
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct AppCardView: View {
|
|
|
|
|
@StateObject private var viewModel: AppCardViewModel
|
|
|
|
|
@EnvironmentObject private var networkManager: NetworkManager
|
2024-11-15 17:47:15 +08:00
|
|
|
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
|
|
|
|
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
2024-11-05 20:30:18 +08:00
|
|
|
|
|
|
|
|
init(sap: Sap) {
|
|
|
|
|
_viewModel = StateObject(wrappedValue: AppCardViewModel(sap: sap, networkManager: nil))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
2024-11-15 17:47:15 +08:00
|
|
|
CardContainer {
|
|
|
|
|
VStack {
|
|
|
|
|
IconView(viewModel: viewModel)
|
|
|
|
|
ProductInfoView(viewModel: viewModel)
|
|
|
|
|
Spacer()
|
|
|
|
|
DownloadButtonView(viewModel: viewModel)
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
2024-11-15 17:47:15 +08:00
|
|
|
.modifier(CardModifier())
|
|
|
|
|
.modifier(SheetModifier(viewModel: viewModel, networkManager: networkManager))
|
|
|
|
|
.modifier(AlertModifier(viewModel: viewModel, confirmRedownload: true))
|
|
|
|
|
.onAppear(perform: setupViewModel)
|
|
|
|
|
.onChange(of: networkManager.downloadTasks, perform: updateDownloadStatus)
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
2024-11-04 14:44:52 +08:00
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private func setupViewModel() {
|
|
|
|
|
viewModel.networkManager = networkManager
|
|
|
|
|
viewModel.updateDownloadingStatus()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func updateDownloadStatus(_ _: [NewDownloadTask]) {
|
|
|
|
|
viewModel.updateDownloadingStatus()
|
2024-11-04 14:44:52 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private struct CardContainer<Content: View>: View {
|
|
|
|
|
let content: Content
|
|
|
|
|
|
|
|
|
|
init(@ViewBuilder content: () -> Content) {
|
|
|
|
|
self.content = content()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
content
|
|
|
|
|
.padding()
|
|
|
|
|
.frame(width: AppCardConstants.cardWidth, height: AppCardConstants.cardHeight)
|
2024-11-04 14:44:52 +08:00
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
private struct IconView: View {
|
2024-11-15 17:47:15 +08:00
|
|
|
@ObservedObject var viewModel: AppCardViewModel
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Group {
|
2024-11-15 17:47:15 +08:00
|
|
|
if viewModel.hasValidIcon {
|
|
|
|
|
Image(nsImage: viewModel.iconImage!)
|
2024-11-03 00:12:38 +08:00
|
|
|
.resizable()
|
|
|
|
|
.interpolation(.high)
|
|
|
|
|
.scaledToFit()
|
|
|
|
|
} else {
|
|
|
|
|
Image(systemName: "app.fill")
|
|
|
|
|
.resizable()
|
|
|
|
|
.scaledToFit()
|
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-15 17:47:15 +08:00
|
|
|
.frame(width: AppCardConstants.iconSize, height: AppCardConstants.iconSize)
|
|
|
|
|
.onAppear(perform: viewModel.loadIcon)
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct ProductInfoView: View {
|
2024-11-15 17:47:15 +08:00
|
|
|
@ObservedObject var viewModel: AppCardViewModel
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack {
|
2024-11-15 17:47:15 +08:00
|
|
|
Text(viewModel.sap.displayName)
|
|
|
|
|
.font(.system(size: AppCardConstants.titleFontSize))
|
2024-11-03 00:12:38 +08:00
|
|
|
.fontWeight(.bold)
|
|
|
|
|
.lineLimit(2)
|
|
|
|
|
.multilineTextAlignment(.center)
|
2024-11-15 17:47:15 +08:00
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
HStack(spacing: 4) {
|
2024-11-15 17:47:15 +08:00
|
|
|
Text("可用版本: \(viewModel.sap.versions.count)")
|
2024-11-03 00:12:38 +08:00
|
|
|
Text("|")
|
2024-11-15 17:47:15 +08:00
|
|
|
Text("依赖包: \(viewModel.dependenciesCount)")
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
|
.frame(height: 20)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
private struct DownloadButtonView: View {
|
|
|
|
|
@ObservedObject var viewModel: AppCardViewModel
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
var body: some View {
|
2024-11-15 17:47:15 +08:00
|
|
|
Button(action: { viewModel.showVersionPicker = true }) {
|
|
|
|
|
Label(viewModel.downloadButtonTitle,
|
|
|
|
|
systemImage: viewModel.downloadButtonIcon)
|
|
|
|
|
.font(.system(size: AppCardConstants.buttonFontSize))
|
2024-11-03 00:12:38 +08:00
|
|
|
.frame(minWidth: 0, maxWidth: .infinity)
|
2024-11-15 17:47:15 +08:00
|
|
|
.frame(height: AppCardConstants.buttonHeight)
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
.buttonStyle(.borderedProminent)
|
2024-11-15 17:47:15 +08:00
|
|
|
.tint(viewModel.isDownloading ? .gray : .blue)
|
|
|
|
|
.disabled(!viewModel.canDownload)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct CardModifier: ViewModifier {
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
|
content
|
|
|
|
|
.background(
|
|
|
|
|
RoundedRectangle(cornerRadius: AppCardConstants.cornerRadius)
|
|
|
|
|
.fill(Color.black.opacity(AppCardConstants.backgroundOpacity))
|
|
|
|
|
)
|
|
|
|
|
.overlay(
|
|
|
|
|
RoundedRectangle(cornerRadius: AppCardConstants.cornerRadius)
|
|
|
|
|
.stroke(Color.gray.opacity(AppCardConstants.strokeOpacity),
|
|
|
|
|
lineWidth: AppCardConstants.strokeWidth)
|
|
|
|
|
)
|
|
|
|
|
.shadow(
|
|
|
|
|
color: Color.primary.opacity(AppCardConstants.shadowOpacity),
|
|
|
|
|
radius: AppCardConstants.shadowRadius,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 1
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct SheetModifier: ViewModifier {
|
|
|
|
|
@ObservedObject var viewModel: AppCardViewModel
|
|
|
|
|
let networkManager: NetworkManager
|
|
|
|
|
@StorageValue(\.useDefaultLanguage) private var useDefaultLanguage
|
|
|
|
|
@StorageValue(\.defaultLanguage) private var defaultLanguage
|
|
|
|
|
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
|
content
|
|
|
|
|
.sheet(isPresented: $viewModel.showVersionPicker) {
|
|
|
|
|
VersionPickerView(sap: viewModel.sap) { version in
|
|
|
|
|
Task {
|
|
|
|
|
await viewModel.handleDownloadRequest(
|
|
|
|
|
version,
|
|
|
|
|
useDefaultLanguage: useDefaultLanguage,
|
|
|
|
|
defaultLanguage: defaultLanguage
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.environmentObject(networkManager)
|
|
|
|
|
}
|
|
|
|
|
.sheet(isPresented: $viewModel.showLanguagePicker) {
|
|
|
|
|
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
|
|
|
|
Task {
|
|
|
|
|
await viewModel.checkAndStartDownload(
|
|
|
|
|
version: viewModel.selectedVersion,
|
|
|
|
|
language: language
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-31 22:35:22 +08:00
|
|
|
#Preview {
|
2024-11-05 20:30:18 +08:00
|
|
|
let networkManager = NetworkManager()
|
|
|
|
|
let sap = Sap(
|
2024-10-31 22:35:22 +08:00
|
|
|
hidden: false,
|
|
|
|
|
displayName: "Photoshop",
|
|
|
|
|
sapCode: "PHSP",
|
|
|
|
|
versions: [
|
2024-11-03 00:12:38 +08:00
|
|
|
"25.0.0": Sap.Versions(
|
2024-10-31 22:35:22 +08:00
|
|
|
sapCode: "PHSP",
|
|
|
|
|
baseVersion: "25.0.0",
|
|
|
|
|
productVersion: "25.0.0",
|
|
|
|
|
apPlatform: "macuniversal",
|
2024-11-03 00:12:38 +08:00
|
|
|
dependencies: [
|
|
|
|
|
Sap.Versions.Dependencies(sapCode: "ACR", version: "9.6"),
|
|
|
|
|
Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0"),
|
|
|
|
|
Sap.Versions.Dependencies(sapCode: "COSY", version: "2.4.1")
|
|
|
|
|
],
|
2024-10-31 22:35:22 +08:00
|
|
|
buildGuid: ""
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
icons: [
|
2024-11-03 00:12:38 +08:00
|
|
|
Sap.ProductIcon(
|
2024-10-31 22:35:22 +08:00
|
|
|
size: "192x192",
|
|
|
|
|
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/25.0.0/192x192.png"
|
|
|
|
|
)
|
|
|
|
|
]
|
2024-11-05 20:30:18 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return AppCardView(sap: sap)
|
|
|
|
|
.environmentObject(networkManager)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct AlertModifier: ViewModifier {
|
|
|
|
|
@ObservedObject var viewModel: AppCardViewModel
|
|
|
|
|
let confirmRedownload: Bool
|
2024-11-07 16:14:42 +08:00
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func body(content: Content) -> some View {
|
|
|
|
|
content
|
2024-11-07 16:14:42 +08:00
|
|
|
.sheet(isPresented: $viewModel.showExistingFileAlert) {
|
|
|
|
|
if let path = viewModel.existingFilePath {
|
|
|
|
|
ExistingFileAlertView(
|
|
|
|
|
path: path,
|
|
|
|
|
onUseExisting: {
|
|
|
|
|
viewModel.showExistingFileAlert = false
|
|
|
|
|
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
|
|
|
|
Task {
|
2024-11-15 17:47:15 +08:00
|
|
|
if let networkManager = viewModel.networkManager,
|
|
|
|
|
!networkManager.downloadTasks.contains(where: { task in
|
|
|
|
|
task.sapCode == viewModel.sap.sapCode &&
|
|
|
|
|
task.version == viewModel.pendingVersion &&
|
|
|
|
|
task.language == viewModel.pendingLanguage
|
|
|
|
|
}) {
|
|
|
|
|
await viewModel.createCompletedTask(path)
|
|
|
|
|
}
|
2024-11-07 16:14:42 +08:00
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
2024-11-07 16:14:42 +08:00
|
|
|
},
|
|
|
|
|
onRedownload: {
|
|
|
|
|
viewModel.showExistingFileAlert = false
|
|
|
|
|
if !viewModel.pendingVersion.isEmpty && !viewModel.pendingLanguage.isEmpty {
|
|
|
|
|
if confirmRedownload {
|
|
|
|
|
viewModel.showRedownloadConfirm = true
|
|
|
|
|
} else {
|
2024-11-09 23:15:50 +08:00
|
|
|
Task {
|
2024-11-15 17:47:15 +08:00
|
|
|
await startRedownload()
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
2024-11-07 16:14:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onCancel: {
|
|
|
|
|
viewModel.showExistingFileAlert = false
|
|
|
|
|
},
|
|
|
|
|
iconImage: viewModel.iconImage
|
|
|
|
|
)
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.alert("确认重新下载", isPresented: $viewModel.showRedownloadConfirm) {
|
|
|
|
|
Button("取消", role: .cancel) { }
|
|
|
|
|
Button("确认") {
|
2024-11-15 17:47:15 +08:00
|
|
|
Task {
|
|
|
|
|
await startRedownload()
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} message: {
|
|
|
|
|
Text("是否确认重新下载?这将覆盖现有的安装程序。")
|
|
|
|
|
}
|
|
|
|
|
.alert("下载错误", isPresented: $viewModel.showError) {
|
|
|
|
|
Button("确定", role: .cancel) { }
|
|
|
|
|
Button("重试") {
|
|
|
|
|
if !viewModel.selectedVersion.isEmpty {
|
2024-11-09 23:15:50 +08:00
|
|
|
Task {
|
|
|
|
|
await viewModel.checkAndStartDownload(
|
|
|
|
|
version: viewModel.selectedVersion,
|
|
|
|
|
language: viewModel.selectedLanguage
|
|
|
|
|
)
|
|
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} message: {
|
|
|
|
|
Text(viewModel.errorMessage)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-15 17:47:15 +08:00
|
|
|
|
|
|
|
|
private func startRedownload() async {
|
|
|
|
|
guard let networkManager = viewModel.networkManager else { return }
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
networkManager.downloadTasks.removeAll { task in
|
|
|
|
|
task.sapCode == viewModel.sap.sapCode &&
|
|
|
|
|
task.version == viewModel.pendingVersion &&
|
|
|
|
|
task.language == viewModel.pendingLanguage
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let existingPath = viewModel.existingFilePath {
|
|
|
|
|
try? FileManager.default.removeItem(at: existingPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let destinationURL = try await viewModel.getDestinationURL(
|
|
|
|
|
version: viewModel.pendingVersion,
|
|
|
|
|
language: viewModel.pendingLanguage
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try await networkManager.startDownload(
|
|
|
|
|
sap: viewModel.sap,
|
|
|
|
|
selectedVersion: viewModel.pendingVersion,
|
|
|
|
|
language: viewModel.pendingLanguage,
|
|
|
|
|
destinationURL: destinationURL
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
viewModel.handleError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|