同步相册云盘界面并完善相关交互
This commit is contained in:
@ -6,21 +6,34 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 云盘传输管理页,对齐 Android `CloudStorageTransitScreen`。
|
||||
/// 云盘传输管理页,严格对齐 Android `CloudStorageTransitScreen`。
|
||||
final class CloudDriveTransferViewController: BaseViewController {
|
||||
private enum Tab: Int {
|
||||
case upload
|
||||
case download
|
||||
}
|
||||
|
||||
private enum Section {
|
||||
case main
|
||||
}
|
||||
|
||||
private let manager: CloudTransferManager
|
||||
private let segmentedControl = UISegmentedControl(items: ["上传", "下载"])
|
||||
private let navigationDivider = UIView()
|
||||
private let tabContainer = UIView()
|
||||
private let uploadTabButton = UIButton(type: .system)
|
||||
private let downloadTabButton = UIButton(type: .system)
|
||||
private let tabIndicator = UIView()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, String>!
|
||||
private var selectedTab: Tab = .upload
|
||||
private var previousStandardAppearance: UINavigationBarAppearance?
|
||||
private var previousScrollEdgeAppearance: UINavigationBarAppearance?
|
||||
private var previousCompactAppearance: UINavigationBarAppearance?
|
||||
private var previousNavigationTintColor: UIColor?
|
||||
|
||||
/// 初始化传输管理页。
|
||||
init(manager: CloudTransferManager = .shared) {
|
||||
self.manager = manager
|
||||
init(manager: CloudTransferManager? = nil) {
|
||||
self.manager = manager ?? .shared
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@ -30,81 +43,217 @@ final class CloudDriveTransferViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "传输管理"
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "传输管理"
|
||||
titleLabel.textColor = UIColor(hex: 0x333333)
|
||||
titleLabel.font = .systemFont(ofSize: 18)
|
||||
navigationItem.titleView = titleLabel
|
||||
|
||||
let backButton = UIButton(type: .system)
|
||||
backButton.setImage(
|
||||
UIImage(systemName: "chevron.left")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)),
|
||||
for: .normal
|
||||
)
|
||||
backButton.tintColor = .black
|
||||
backButton.accessibilityLabel = "返回"
|
||||
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
||||
backButton.snp.makeConstraints { make in make.width.height.equalTo(44) }
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
segmentedControl.selectedSegmentIndex = selectedTab.rawValue
|
||||
navigationDivider.backgroundColor = UIColor(hex: 0xEEEEEE)
|
||||
tabContainer.backgroundColor = .white
|
||||
configureTabButton(uploadTabButton, title: "上传", action: #selector(uploadTabTapped))
|
||||
configureTabButton(downloadTabButton, title: "下载", action: #selector(downloadTabTapped))
|
||||
tabIndicator.backgroundColor = AppColor.primary
|
||||
|
||||
tableView.backgroundColor = AppColor.pageBackground
|
||||
tableView.separatorStyle = .none
|
||||
tableView.dataSource = self
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.contentInsetAdjustmentBehavior = .never
|
||||
tableView.delegate = self
|
||||
tableView.register(CloudTransferCell.self, forCellReuseIdentifier: CloudTransferCell.reuseIdentifier)
|
||||
|
||||
view.addSubview(segmentedControl)
|
||||
view.addSubview(navigationDivider)
|
||||
view.addSubview(tabContainer)
|
||||
tabContainer.addSubview(uploadTabButton)
|
||||
tabContainer.addSubview(downloadTabButton)
|
||||
tabContainer.addSubview(tabIndicator)
|
||||
view.addSubview(tableView)
|
||||
|
||||
configureDataSource()
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
segmentedControl.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.sm)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(36)
|
||||
navigationDivider.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
tabContainer.snp.makeConstraints { make in
|
||||
make.top.equalTo(navigationDivider.snp.bottom)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(48)
|
||||
}
|
||||
uploadTabButton.snp.makeConstraints { make in
|
||||
make.top.leading.bottom.equalToSuperview()
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
downloadTabButton.snp.makeConstraints { make in
|
||||
make.top.trailing.bottom.equalToSuperview()
|
||||
make.width.equalTo(uploadTabButton)
|
||||
}
|
||||
tabIndicator.snp.makeConstraints { make in
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(2)
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
make.leading.equalToSuperview()
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(segmentedControl.snp.bottom).offset(AppSpacing.sm)
|
||||
make.top.equalTo(tabContainer.snp.bottom)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
segmentedControl.addTarget(self, action: #selector(tabChanged), for: .valueChanged)
|
||||
manager.onTasksChange = { [weak self] in
|
||||
Task { @MainActor in self?.tableView.reloadData() }
|
||||
Task { @MainActor in self?.applySnapshot(animated: true) }
|
||||
}
|
||||
updateTabs(animated: false)
|
||||
applySnapshot(animated: false)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
applyCloudNavigationBarAppearance()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
restoreNavigationBarAppearance()
|
||||
}
|
||||
|
||||
private var visibleTasks: [CloudTransferTask] {
|
||||
selectedTab == .upload ? manager.uploadTasks : manager.downloadTasks
|
||||
}
|
||||
|
||||
@objc private func tabChanged() {
|
||||
selectedTab = Tab(rawValue: segmentedControl.selectedSegmentIndex) ?? .upload
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudDriveTransferViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
visibleTasks.count
|
||||
private func configureTabButton(_ button: UIButton, title: String, action: Selector) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
button.addTarget(self, action: action, for: .touchUpInside)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
112
|
||||
private func applyCloudNavigationBarAppearance() {
|
||||
guard let navigationBar = navigationController?.navigationBar else { return }
|
||||
previousStandardAppearance = navigationBar.standardAppearance
|
||||
previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance
|
||||
previousCompactAppearance = navigationBar.compactAppearance
|
||||
previousNavigationTintColor = navigationBar.tintColor
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithOpaqueBackground()
|
||||
appearance.backgroundColor = .white
|
||||
appearance.shadowColor = .clear
|
||||
navigationBar.standardAppearance = appearance
|
||||
navigationBar.scrollEdgeAppearance = appearance
|
||||
navigationBar.compactAppearance = appearance
|
||||
navigationBar.tintColor = .black
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CloudTransferCell.reuseIdentifier, for: indexPath) as! CloudTransferCell
|
||||
let task = visibleTasks[indexPath.row]
|
||||
cell.apply(task: task)
|
||||
cell.onPrimaryAction = { [weak self] in
|
||||
guard let self else { return }
|
||||
if task.kind == .upload {
|
||||
task.status == .uploading ? self.manager.pauseUpload(id: task.id) : self.manager.resumeUpload(id: task.id)
|
||||
} else {
|
||||
task.status == .downloading ? self.manager.pauseDownload(id: task.id) : self.manager.resumeDownload(id: task.id)
|
||||
private func restoreNavigationBarAppearance() {
|
||||
guard let navigationBar = navigationController?.navigationBar else { return }
|
||||
if let previousStandardAppearance {
|
||||
navigationBar.standardAppearance = previousStandardAppearance
|
||||
}
|
||||
navigationBar.scrollEdgeAppearance = previousScrollEdgeAppearance
|
||||
navigationBar.compactAppearance = previousCompactAppearance
|
||||
navigationBar.tintColor = previousNavigationTintColor
|
||||
}
|
||||
|
||||
private func configureDataSource() {
|
||||
dataSource = UITableViewDiffableDataSource<Section, String>(tableView: tableView) { [weak self] tableView, indexPath, taskID in
|
||||
guard let self,
|
||||
let task = self.visibleTasks.first(where: { $0.id == taskID }),
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: CloudTransferCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? CloudTransferCell else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
cell.apply(task: task)
|
||||
cell.onPrimaryAction = { [weak self] in
|
||||
guard let self else { return }
|
||||
switch task.kind {
|
||||
case .upload:
|
||||
task.status == .uploading ? self.manager.pauseUpload(id: task.id) : self.manager.resumeUpload(id: task.id)
|
||||
case .download:
|
||||
task.status == .downloading ? self.manager.pauseDownload(id: task.id) : self.manager.resumeDownload(id: task.id)
|
||||
}
|
||||
}
|
||||
cell.onCancel = { [weak self] in
|
||||
guard let self else { return }
|
||||
task.kind == .upload ? self.manager.cancelUpload(id: task.id) : self.manager.cancelDownload(id: task.id)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
cell.onCancel = { [weak self] in
|
||||
guard let self else { return }
|
||||
task.kind == .upload ? self.manager.cancelUpload(id: task.id) : self.manager.cancelDownload(id: task.id)
|
||||
dataSource.defaultRowAnimation = .fade
|
||||
}
|
||||
|
||||
private func applySnapshot(animated: Bool) {
|
||||
let previousIDs = Set(dataSource.snapshot().itemIdentifiers)
|
||||
let taskIDs = visibleTasks.map(\.id)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(taskIDs)
|
||||
snapshot.reconfigureItems(taskIDs.filter(previousIDs.contains))
|
||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
||||
}
|
||||
|
||||
private func updateTabs(animated: Bool) {
|
||||
let uploadSelected = selectedTab == .upload
|
||||
uploadTabButton.setTitleColor(uploadSelected ? AppColor.primary : UIColor(hex: 0x333333), for: .normal)
|
||||
downloadTabButton.setTitleColor(uploadSelected ? UIColor(hex: 0x333333) : AppColor.primary, for: .normal)
|
||||
uploadTabButton.titleLabel?.font = .systemFont(ofSize: 14, weight: uploadSelected ? .medium : .regular)
|
||||
downloadTabButton.titleLabel?.font = .systemFont(ofSize: 14, weight: uploadSelected ? .regular : .medium)
|
||||
tabIndicator.snp.updateConstraints { make in
|
||||
make.leading.equalToSuperview().offset(uploadSelected ? 0 : view.bounds.width / 2)
|
||||
}
|
||||
return cell
|
||||
guard animated else {
|
||||
tabContainer.layoutIfNeeded()
|
||||
return
|
||||
}
|
||||
UIView.animate(withDuration: 0.2) { self.tabContainer.layoutIfNeeded() }
|
||||
}
|
||||
|
||||
private func selectTab(_ tab: Tab) {
|
||||
guard selectedTab != tab else { return }
|
||||
selectedTab = tab
|
||||
updateTabs(animated: true)
|
||||
applySnapshot(animated: true)
|
||||
tableView.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
|
||||
@objc private func backTapped() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func uploadTabTapped() {
|
||||
selectTab(.upload)
|
||||
}
|
||||
|
||||
@objc private func downloadTabTapped() {
|
||||
selectTab(.download)
|
||||
}
|
||||
}
|
||||
|
||||
/// 云盘传输任务 Cell。
|
||||
extension CloudDriveTransferViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
119
|
||||
}
|
||||
}
|
||||
|
||||
/// 云盘传输任务 Cell,严格对齐 Android 上传/下载任务卡片。
|
||||
private final class CloudTransferCell: UITableViewCell {
|
||||
static let reuseIdentifier = "CloudTransferCell"
|
||||
|
||||
@ -137,90 +286,124 @@ private final class CloudTransferCell: UITableViewCell {
|
||||
onCancel = nil
|
||||
}
|
||||
|
||||
/// 应用传输任务状态。
|
||||
func apply(task: CloudTransferTask) {
|
||||
iconView.image = CloudDriveAsset.image(
|
||||
named: task.fileType == 1 ? "icon_cloud_storage_file_type_video" : "icon_cloud_storage_file_type_photo",
|
||||
fallbackSystemName: task.fileType == 1 ? "video.fill" : "photo.fill"
|
||||
)
|
||||
titleLabel.text = task.fileName
|
||||
statusLabel.text = "\(task.status.title) \(CloudDriveFormatter.fileSize(task.fileSize))"
|
||||
statusLabel.text = statusText(for: task)
|
||||
progressView.progress = Float(task.progress) / 100
|
||||
percentLabel.text = "\(task.progress)%"
|
||||
|
||||
let isRunning = task.status == .uploading || task.status == .downloading
|
||||
primaryButton.setImage(UIImage(systemName: isRunning ? "pause.fill" : "play.fill"), for: .normal)
|
||||
primaryButton.isHidden = task.status == .completed
|
||||
let canResume = task.status == .paused || task.status == .failed
|
||||
primaryButton.setImage(
|
||||
UIImage(systemName: isRunning ? "pause.fill" : "play.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20)),
|
||||
for: .normal
|
||||
)
|
||||
primaryButton.isHidden = !(isRunning || canResume)
|
||||
}
|
||||
|
||||
private func statusText(for task: CloudTransferTask) -> String {
|
||||
switch task.kind {
|
||||
case .upload:
|
||||
let status: String
|
||||
switch task.status {
|
||||
case .uploading: status = "上传中"
|
||||
case .paused: status = "已暂停"
|
||||
case .failed: status = "失败"
|
||||
default: status = ""
|
||||
}
|
||||
let size = CloudDriveFormatter.fileSize(task.fileSize)
|
||||
return status.isEmpty ? size : "\(status) \(size)"
|
||||
case .download:
|
||||
switch task.status {
|
||||
case .downloading: return "下载中"
|
||||
case .paused: return "已暂停"
|
||||
case .failed: return "失败"
|
||||
case .completed: return "已完成"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = AppRadius.sm
|
||||
cardView.layer.cornerRadius = 8
|
||||
cardView.clipsToBounds = true
|
||||
|
||||
iconView.tintColor = AppColor.primary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
titleLabel.font = .app(.body)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.font = .systemFont(ofSize: 15)
|
||||
titleLabel.textColor = UIColor(hex: 0x333333)
|
||||
titleLabel.lineBreakMode = .byTruncatingMiddle
|
||||
statusLabel.font = .app(.caption)
|
||||
statusLabel.textColor = AppColor.textTertiary
|
||||
statusLabel.font = .systemFont(ofSize: 12)
|
||||
statusLabel.textColor = .gray
|
||||
progressView.progressTintColor = AppColor.primary
|
||||
percentLabel.font = .app(.caption)
|
||||
percentLabel.textColor = AppColor.textSecondary
|
||||
progressView.trackTintColor = UIColor(hex: 0xE5E7EB)
|
||||
progressView.transform = CGAffineTransform(scaleX: 1, y: 2)
|
||||
percentLabel.font = .systemFont(ofSize: 11)
|
||||
percentLabel.textColor = UIColor(hex: 0x4B5563)
|
||||
percentLabel.textAlignment = .right
|
||||
|
||||
primaryButton.tintColor = AppColor.primary
|
||||
cancelButton.tintColor = AppColor.textSecondary
|
||||
cancelButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
primaryButton.tintColor = UIColor(hex: 0x333333)
|
||||
cancelButton.tintColor = UIColor(hex: 0x333333)
|
||||
cancelButton.setImage(
|
||||
UIImage(systemName: "xmark")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20)),
|
||||
for: .normal
|
||||
)
|
||||
primaryButton.accessibilityLabel = "暂停或继续"
|
||||
cancelButton.accessibilityLabel = "取消任务"
|
||||
primaryButton.addTarget(self, action: #selector(primaryTapped), for: .touchUpInside)
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(iconView)
|
||||
cardView.addSubview(titleLabel)
|
||||
cardView.addSubview(statusLabel)
|
||||
cardView.addSubview(primaryButton)
|
||||
cardView.addSubview(cancelButton)
|
||||
cardView.addSubview(progressView)
|
||||
cardView.addSubview(percentLabel)
|
||||
[iconView, titleLabel, statusLabel, primaryButton, cancelButton, progressView, percentLabel]
|
||||
.forEach(cardView.addSubview)
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview().inset(AppSpacing.xs)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.top.equalToSuperview().offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(12)
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.top.equalToSuperview().offset(AppSpacing.sm)
|
||||
make.leading.top.equalToSuperview().offset(12)
|
||||
make.width.height.equalTo(48)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.trailing.top.equalToSuperview().inset(AppSpacing.xs)
|
||||
make.width.height.equalTo(36)
|
||||
make.trailing.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(8)
|
||||
make.width.height.equalTo(44)
|
||||
}
|
||||
primaryButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(cancelButton.snp.leading)
|
||||
make.centerY.equalTo(cancelButton)
|
||||
make.width.height.equalTo(36)
|
||||
make.width.height.equalTo(44)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm)
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(12)
|
||||
make.top.equalTo(iconView)
|
||||
make.trailing.equalTo(primaryButton.snp.leading).offset(-AppSpacing.xs)
|
||||
make.trailing.equalTo(primaryButton.snp.leading).offset(-4)
|
||||
}
|
||||
statusLabel.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.xxs)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
}
|
||||
progressView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView)
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||
make.top.equalTo(iconView.snp.bottom).offset(AppSpacing.sm)
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.trailing.equalToSuperview().inset(12)
|
||||
make.top.equalTo(iconView.snp.bottom).offset(12)
|
||||
make.height.equalTo(4)
|
||||
}
|
||||
percentLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(progressView.snp.bottom).offset(AppSpacing.xxs)
|
||||
make.top.equalTo(progressView.snp.bottom).offset(4)
|
||||
make.trailing.equalTo(progressView)
|
||||
make.bottom.equalToSuperview().inset(12)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user