Files
suixinkan_uikit/suixinkan/UI/CloudDrive/CloudDriveTransferViewController.swift

418 lines
16 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// CloudDriveTransferViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// 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 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? = nil) {
self.manager = manager ?? .shared
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
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
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.showsVerticalScrollIndicator = false
tableView.contentInsetAdjustmentBehavior = .never
tableView.delegate = self
tableView.register(CloudTransferCell.self, forCellReuseIdentifier: CloudTransferCell.reuseIdentifier)
view.addSubview(navigationDivider)
view.addSubview(tabContainer)
tabContainer.addSubview(uploadTabButton)
tabContainer.addSubview(downloadTabButton)
tabContainer.addSubview(tabIndicator)
view.addSubview(tableView)
configureDataSource()
}
override func setupConstraints() {
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(tabContainer.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindActions() {
manager.onTasksChange = { [weak self] in
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
}
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)
}
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
}
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
}
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)
}
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)
}
}
extension CloudDriveTransferViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
119
}
}
/// Cell Android /
private final class CloudTransferCell: UITableViewCell {
static let reuseIdentifier = "CloudTransferCell"
var onPrimaryAction: (() -> Void)?
var onCancel: (() -> Void)?
private let cardView = UIView()
private let iconView = UIImageView()
private let titleLabel = UILabel()
private let statusLabel = UILabel()
private let primaryButton = UIButton(type: .system)
private let cancelButton = UIButton(type: .system)
private let progressView = UIProgressView(progressViewStyle: .default)
private let percentLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
setupConstraints()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
onPrimaryAction = nil
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 = statusText(for: task)
progressView.progress = Float(task.progress) / 100
percentLabel.text = "\(task.progress)%"
let isRunning = task.status == .uploading || task.status == .downloading
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 = 8
cardView.clipsToBounds = true
iconView.contentMode = .scaleAspectFit
titleLabel.font = .systemFont(ofSize: 15)
titleLabel.textColor = UIColor(hex: 0x333333)
titleLabel.lineBreakMode = .byTruncatingMiddle
statusLabel.font = .systemFont(ofSize: 12)
statusLabel.textColor = .gray
progressView.progressTintColor = AppColor.primary
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 = 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)
[iconView, titleLabel, statusLabel, primaryButton, cancelButton, progressView, percentLabel]
.forEach(cardView.addSubview)
}
private func setupConstraints() {
cardView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.leading.trailing.equalToSuperview().inset(12)
make.bottom.equalToSuperview()
}
iconView.snp.makeConstraints { make in
make.leading.top.equalToSuperview().offset(12)
make.width.height.equalTo(48)
}
cancelButton.snp.makeConstraints { make in
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(44)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(12)
make.top.equalTo(iconView)
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(4)
}
progressView.snp.makeConstraints { make in
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(4)
make.trailing.equalTo(progressView)
make.bottom.equalToSuperview().inset(12)
}
}
@objc private func primaryTapped() {
onPrimaryAction?()
}
@objc private func cancelTapped() {
onCancel?()
}
}