Files
Adobe-Downloader/Adobe Downloader/Views/DownloadProgressView.swift

789 lines
26 KiB
Swift
Raw Normal View History

//
// Adobe-Downloader
//
// Created by X1a0He on 2024/10/30.
//
import SwiftUI
struct DownloadProgressView: View {
@EnvironmentObject private var networkManager: NetworkManager
let task: NewDownloadTask
let onCancel: () -> Void
let onPause: () -> Void
let onResume: () -> Void
let onRetry: () -> Void
let onRemove: () -> Void
@State private var showInstallPrompt = false
@State private var isInstalling = false
@State private var isPackageListExpanded: Bool = false
private var statusLabel: some View {
Text(task.status.description)
.font(.caption)
.foregroundColor(statusColor)
.padding(.vertical, 2)
.padding(.horizontal, 6)
.background(statusBackgroundColor)
.cornerRadius(4)
}
private var statusColor: Color {
switch task.status {
case .downloading:
return .white
case .preparing:
return .white
case .completed:
return .white
case .failed:
return .white
case .paused:
return .white
case .waiting:
return .white
case .retrying:
return .white
}
}
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 var actionButtons: some View {
HStack(spacing: 8) {
switch task.status {
case .downloading, .preparing, .waiting:
Button(action: onPause) {
Label("暂停", systemImage: "pause.fill")
}
.buttonStyle(.borderedProminent)
.tint(.orange)
Button(action: onCancel) {
Label("取消", systemImage: "xmark")
}
.buttonStyle(.borderedProminent)
.tint(.red)
case .paused:
Button(action: onResume) {
Label("继续", systemImage: "play.fill")
}
.buttonStyle(.borderedProminent)
.tint(.blue)
Button(action: onCancel) {
Label("取消", systemImage: "xmark")
}
.buttonStyle(.borderedProminent)
.tint(.red)
case .failed(let info):
if info.recoverable {
Button(action: onRetry) {
Label("重试", systemImage: "arrow.clockwise")
}
.buttonStyle(.borderedProminent)
.tint(.blue)
}
Button(action: onRemove) {
Label("移除", systemImage: "xmark")
}
.buttonStyle(.borderedProminent)
.tint(.red)
case .completed:
HStack(spacing: 8) {
Button(action: { showInstallPrompt = true }) {
Label("安装", systemImage: "square.and.arrow.down.on.square")
}
.buttonStyle(.borderedProminent)
.tint(.green)
Button(action: onRemove) {
Label("删除", systemImage: "trash")
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
case .retrying:
Button(action: onCancel) {
Label("取消", systemImage: "xmark")
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
}
.controlSize(.small)
.sheet(isPresented: $showInstallPrompt) {
VStack(spacing: 20) {
Text("是否要安装 \(task.displayName)?")
.font(.headline)
HStack(spacing: 16) {
Button("取消") {
showInstallPrompt = false
}
.buttonStyle(.bordered)
Button("安装") {
showInstallPrompt = false
isInstalling = true
Task {
await networkManager.installProduct(at: task.directory)
}
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.frame(width: 300)
}
.sheet(isPresented: $isInstalling) {
Group {
if case .installing(let progress, let status) = networkManager.installationState {
InstallProgressView(
productName: task.displayName,
progress: progress,
status: status,
onCancel: {
networkManager.cancelInstallation()
isInstalling = false
},
onRetry: nil
)
} else if case .completed = networkManager.installationState {
InstallProgressView(
productName: task.displayName,
progress: 1.0,
status: "安装完成",
onCancel: {
isInstalling = false
},
onRetry: nil
)
} else if case .failed(let error) = networkManager.installationState {
InstallProgressView(
productName: task.displayName,
progress: 0,
status: "安装失败: \(error.localizedDescription)",
onCancel: {
isInstalling = false
},
onRetry: {
Task {
await networkManager.retryInstallation(at: task.directory)
}
}
)
} else {
InstallProgressView(
productName: task.displayName,
progress: 0,
status: "准备安装...",
onCancel: {
networkManager.cancelInstallation()
isInstalling = false
},
onRetry: nil
)
}
}
.frame(minWidth: 400, minHeight: 200)
.background(Color(NSColor.windowBackgroundColor))
}
}
private func formatFileSize(_ size: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
return formatter.string(fromByteCount: size)
}
private func formatSpeed(_ bytesPerSecond: Double) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
formatter.includesUnit = true
formatter.isAdaptive = true
return formatter.string(fromByteCount: Int64(bytesPerSecond)) + "/s"
}
private func openInFinder(_ url: URL) {
NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: url.deletingLastPathComponent().path)
}
private func formatRemainingTime(totalSize: Int64, downloadedSize: Int64, speed: Double) -> String {
guard speed > 0 else { return "" }
let remainingBytes = Double(totalSize - downloadedSize)
let remainingSeconds = Int(remainingBytes / speed)
let minutes = remainingSeconds / 60
let seconds = remainingSeconds % 60
return String(format: "%02d:%02d", minutes, seconds)
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(task.displayName)
.font(.headline)
Spacer()
Text(task.version)
.foregroundColor(.secondary)
}
//
Text(task.directory.path)
.font(.caption)
.foregroundColor(.blue)
.lineLimit(1)
.truncationMode(.middle)
.onTapGesture {
openInFinder(task.directory)
}
//
statusLabel
.padding(.vertical, 2)
//
VStack(alignment: .leading, spacing: 4) {
HStack {
HStack(spacing: 4) {
Text(formatFileSize(task.totalDownloadedSize))
Text("/")
Text(formatFileSize(task.totalSize))
}
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.productsToDownload.isEmpty {
Divider()
VStack(alignment: .leading, spacing: 6) {
Button(action: {
withAnimation {
isPackageListExpanded.toggle()
}
}) {
HStack {
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
.foregroundColor(.secondary)
Text("产品和包列表")
.font(.caption)
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if isPackageListExpanded {
ScrollView {
ForEach(task.productsToDownload, id: \.sapCode) { product in
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: "cube.box")
.foregroundColor(.blue)
Text("\(product.sapCode) (\(product.version))")
.font(.caption)
.fontWeight(.medium)
}
.padding(.vertical, 2)
ForEach(product.packages) { package in
PackageRow(
package: package,
isCurrentPackage: task.currentPackage?.id == package.id
)
}
}
.padding(.leading, 4)
}
}
.frame(maxHeight: 200)
}
}
}
HStack {
Spacer()
actionButtons
}
}
.padding()
.background(Color(NSColor.windowBackgroundColor))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
)
.shadow(color: Color.primary.opacity(0.05), radius: 2, x: 0, y: 1)
}
}
struct PackageRow: View {
@ObservedObject var package: Package
let isCurrentPackage: Bool
var body: some View {
VStack(spacing: 4) {
//
HStack {
//
Rectangle()
.fill(Color.clear)
.frame(width: 20)
Text(package.fullPackageName)
.font(.caption)
.foregroundColor(.primary)
Text(package.type)
.font(.caption2)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(Color.blue.opacity(0.1))
.cornerRadius(2)
Spacer()
//
if !isCurrentPackage || package.status != .downloading {
Text(package.status.description)
.font(.caption)
.foregroundColor(.secondary)
}
}
//
if isCurrentPackage && package.status == .downloading {
VStack(spacing: 2) {
//
HStack {
Rectangle()
.fill(Color.clear)
.frame(width: 20)
ProgressView(value: package.progress)
.progressViewStyle(.linear)
Text("\(Int(package.progress * 100))% \(package.formattedSpeed)")
.font(.caption2)
.foregroundColor(.secondary)
}
//
HStack {
Rectangle()
.fill(Color.clear)
.frame(width: 20)
Text("\(package.formattedDownloadedSize) / \(package.formattedSize)")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
}
}
}
}
.padding(.vertical, 2)
.padding(.horizontal, 4)
.background(isCurrentPackage ? Color.blue.opacity(0.05) : Color.clear)
.cornerRadius(4)
}
}
//
#Preview("下载中") {
struct PreviewWrapper: View {
@StateObject private var task: NewDownloadTask
init() {
let task = NewDownloadTask(
sapCode: "PHSP",
version: "26.0.0",
language: "zh_CN",
displayName: "Adobe Photoshop",
directory: URL(fileURLWithPath: "/Users/Downloads/Install PHSP_26.0-zh_CN-macuniversal.app"),
productsToDownload: [
ProductsToDownload(
sapCode: "PHSP",
version: "26.0.0",
buildGuid: "123",
applicationJson: ""
),
ProductsToDownload(
sapCode: "ACR",
version: "9.6.0",
buildGuid: "456",
applicationJson: ""
)
],
retryCount: 0,
createAt: Date(),
totalStatus: .downloading(DownloadStatus.DownloadInfo(
fileName: "AdobePhotoshop26-Core.zip",
currentPackageIndex: 0,
totalPackages: 8,
startTime: Date(),
estimatedTimeRemaining: nil
)),
totalProgress: 0.35,
totalDownloadedSize: 738_197_504,
totalSize: 2_147_483_648,
totalSpeed: 1_048_576
)
// PHSP
let phspPackages = [
//
Package(
type: "core",
fullPackageName: "AdobePhotoshop26-Core.zip",
downloadSize: 2_112_950_169,
downloadURL: "/products/PHSP/AdobePhotoshop26-Core.zip"
),
//
Package(
type: "core",
fullPackageName: "AdobePhotoshop26-Core_stripped.zip",
downloadSize: 1_874_257_058,
downloadURL: "/products/PHSP/AdobePhotoshop26-Core_stripped.zip"
),
//
Package(
type: "core",
fullPackageName: "AdobePhotoshop26-nl_NL.zip",
downloadSize: 490_628,
downloadURL: "/products/PHSP/AdobePhotoshop26-nl_NL.zip"
)
]
// ACR
let acrPackages = [
//
Package(
type: "core",
fullPackageName: "AdobeCameraRaw8.0All.zip",
downloadSize: 255_223_665,
downloadURL: "/products/ACR/AdobeCameraRaw8.0All.zip"
),
//
Package(
type: "core",
fullPackageName: "AdobeCameraRaw8.0-support.zip",
downloadSize: 76_896_003,
downloadURL: "/products/ACR/AdobeCameraRaw8.0-support.zip"
)
]
//
phspPackages[0].status = .downloading
phspPackages[0].downloadedSize = 738_197_504
phspPackages[0].progress = 0.35
phspPackages[0].speed = 1_048_576
phspPackages[1].status = .waiting
phspPackages[2].status = .completed
phspPackages[2].downloaded = true
phspPackages[2].progress = 1.0
acrPackages[0].status = .waiting
acrPackages[1].status = .failed("下载失败")
task.productsToDownload[0].packages = phspPackages
task.productsToDownload[1].packages = acrPackages
//
task.currentPackage = phspPackages[0]
self._task = StateObject(wrappedValue: task)
}
var body: some View {
DownloadProgressView(
task: task,
onCancel: {},
onPause: {},
onResume: {},
onRetry: {},
onRemove: {}
)
.environmentObject(NetworkManager())
.padding()
.frame(width: 600)
}
}
return PreviewWrapper()
}
//
#Preview("下载中(展开包列表)") {
struct PreviewWrapper: View {
@State private var isExpanded = true
let task: NewDownloadTask
var body: some View {
DownloadProgressView(
task: task,
onCancel: {},
onPause: {},
onResume: {},
onRetry: {},
onRemove: {}
)
.environmentObject(NetworkManager())
.padding()
.frame(width: 600)
}
}
let task = NewDownloadTask(
sapCode: "PHSP",
version: "26.0.0",
language: "zh_CN",
displayName: "Adobe Photoshop",
directory: URL(fileURLWithPath: "/Users/Downloads/Install Photoshop_26.0-zh_CN.app"),
productsToDownload: [
ProductsToDownload(
sapCode: "PHSP",
version: "26.0.0",
buildGuid: "123",
applicationJson: ""
),
ProductsToDownload(
sapCode: "ACR",
version: "9.6.0",
buildGuid: "456",
applicationJson: ""
)
],
retryCount: 0,
createAt: Date(),
totalStatus: .downloading(DownloadStatus.DownloadInfo(
fileName: "AdobePhotoshop26-Core.zip",
currentPackageIndex: 0,
totalPackages: 8,
startTime: Date(),
estimatedTimeRemaining: nil
)),
totalProgress: 0.35,
totalDownloadedSize: 738_197_504,
totalSize: 2_147_483_648,
totalSpeed: 1_048_576
)
//
task.productsToDownload[0].packages = [
Package(
type: "core",
fullPackageName: "AdobePhotoshop26-Core.zip",
downloadSize: 1_073_741_824,
downloadURL: "/products/PHSP/AdobePhotoshop26-Core.zip"
),
Package(
type: "non-core",
fullPackageName: "AdobePhotoshop26-Support.zip",
downloadSize: 536_870_912,
downloadURL: "/products/PHSP/AdobePhotoshop26-Support.zip"
)
]
task.productsToDownload[1].packages = [
Package(
type: "core",
fullPackageName: "ACR-Core.zip",
downloadSize: 268_435_456,
downloadURL: "/products/ACR/ACR-Core.zip"
)
]
//
task.currentPackage = task.productsToDownload[0].packages[0]
task.currentPackage?.downloadedSize = 738_197_504
task.currentPackage?.progress = 0.35
task.currentPackage?.speed = 1_048_576
task.currentPackage?.status = .downloading
return PreviewWrapper(task: task)
}
#Preview("准备中") {
let task = NewDownloadTask(
sapCode: "PHSP",
version: "26.0.0",
language: "zh_CN",
displayName: "Adobe Photoshop",
directory: URL(fileURLWithPath: "/Users/Downloads/Install Photoshop_26.0-zh_CN.app"),
productsToDownload: [],
retryCount: 0,
createAt: Date(),
totalStatus: .preparing(DownloadStatus.PrepareInfo(
message: "正在准备下载...",
timestamp: Date(),
stage: .initializing
)),
totalProgress: 0,
totalDownloadedSize: 0,
totalSize: 2_147_483_648,
totalSpeed: 0
)
return DownloadProgressView(
task: task,
onCancel: {},
onPause: {},
onResume: {},
onRetry: {},
onRemove: {}
)
.environmentObject(NetworkManager())
.padding()
.frame(width: 600)
}
#Preview("已完成") {
let task = NewDownloadTask(
sapCode: "PHSP",
version: "26.0.0",
language: "zh_CN",
displayName: "Adobe Photoshop",
directory: URL(fileURLWithPath: "/Users/Downloads/Install Photoshop_26.0-zh_CN.app"),
productsToDownload: [
ProductsToDownload(
sapCode: "PHSP",
version: "26.0.0",
buildGuid: "123",
applicationJson: ""
)
],
retryCount: 0,
createAt: Date().addingTimeInterval(-3600),
totalStatus: .completed(DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: 3600,
totalSize: 2_147_483_648
)),
totalProgress: 1.0,
totalDownloadedSize: 2_147_483_648,
totalSize: 2_147_483_648,
totalSpeed: 0
)
//
task.productsToDownload[0].packages = [
Package(
type: "core",
fullPackageName: "AdobePhotoshop26-Core.zip",
downloadSize: 1_073_741_824,
downloadURL: "/products/PHSP/AdobePhotoshop26-Core.zip"
)
]
task.productsToDownload[0].packages[0].downloaded = true
task.productsToDownload[0].packages[0].progress = 1.0
task.productsToDownload[0].packages[0].status = .completed
return DownloadProgressView(
task: task,
onCancel: {},
onPause: {},
onResume: {},
onRetry: {},
onRemove: {}
)
.environmentObject(NetworkManager())
.padding()
.frame(width: 600)
}
#Preview("失败") {
let task = NewDownloadTask(
sapCode: "PHSP",
version: "26.0.0",
language: "zh_CN",
displayName: "Adobe Photoshop",
directory: URL(fileURLWithPath: "/Users/Downloads/Install Photoshop_26.0-zh_CN.app"),
productsToDownload: [
ProductsToDownload(
sapCode: "PHSP",
version: "26.0.0",
buildGuid: "123",
applicationJson: ""
)
],
retryCount: 3,
createAt: Date(),
totalStatus: .failed(DownloadStatus.FailureInfo(
message: "网络连接已断开",
error: NetworkError.noConnection,
timestamp: Date(),
recoverable: true
)),
totalProgress: 0.5,
totalDownloadedSize: 1_073_741_824,
totalSize: 2_147_483_648,
totalSpeed: 0
)
return DownloadProgressView(
task: task,
onCancel: {},
onPause: {},
onResume: {},
onRetry: {},
onRemove: {}
)
.environmentObject(NetworkManager())
.padding()
.frame(width: 600)
}