feat: 添加云盘与消息中心功能

This commit is contained in:
2026-07-09 22:37:43 +08:00
parent 8e356973bd
commit f20ec7f06c
91 changed files with 5437 additions and 89 deletions

View File

@ -0,0 +1,234 @@
//
// CloudDriveTransferViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `CloudStorageTransitScreen`
final class CloudDriveTransferViewController: BaseViewController {
private enum Tab: Int {
case upload
case download
}
private let manager: CloudTransferManager
private let segmentedControl = UISegmentedControl(items: ["上传", "下载"])
private let tableView = UITableView(frame: .zero, style: .plain)
private var selectedTab: Tab = .upload
///
init(manager: CloudTransferManager = .shared) {
self.manager = manager
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "传输管理"
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
segmentedControl.selectedSegmentIndex = selectedTab.rawValue
tableView.backgroundColor = AppColor.pageBackground
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
tableView.register(CloudTransferCell.self, forCellReuseIdentifier: CloudTransferCell.reuseIdentifier)
view.addSubview(segmentedControl)
view.addSubview(tableView)
}
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)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(segmentedControl.snp.bottom).offset(AppSpacing.sm)
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() }
}
}
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
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
112
}
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)
}
}
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
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 = "\(task.status.title) \(CloudDriveFormatter.fileSize(task.fileSize))"
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
}
private func setupUI() {
backgroundColor = .clear
selectionStyle = .none
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppRadius.sm
cardView.clipsToBounds = true
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
titleLabel.font = .app(.body)
titleLabel.textColor = AppColor.textPrimary
titleLabel.lineBreakMode = .byTruncatingMiddle
statusLabel.font = .app(.caption)
statusLabel.textColor = AppColor.textTertiary
progressView.progressTintColor = AppColor.primary
percentLabel.font = .app(.caption)
percentLabel.textColor = AppColor.textSecondary
percentLabel.textAlignment = .right
primaryButton.tintColor = AppColor.primary
cancelButton.tintColor = AppColor.textSecondary
cancelButton.setImage(UIImage(systemName: "xmark"), for: .normal)
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)
}
private func setupConstraints() {
cardView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(AppSpacing.xs)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
iconView.snp.makeConstraints { make in
make.leading.top.equalToSuperview().offset(AppSpacing.sm)
make.width.height.equalTo(48)
}
cancelButton.snp.makeConstraints { make in
make.trailing.top.equalToSuperview().inset(AppSpacing.xs)
make.width.height.equalTo(36)
}
primaryButton.snp.makeConstraints { make in
make.trailing.equalTo(cancelButton.snp.leading)
make.centerY.equalTo(cancelButton)
make.width.height.equalTo(36)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm)
make.top.equalTo(iconView)
make.trailing.equalTo(primaryButton.snp.leading).offset(-AppSpacing.xs)
}
statusLabel.snp.makeConstraints { make in
make.leading.trailing.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.xxs)
}
progressView.snp.makeConstraints { make in
make.leading.equalTo(iconView)
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.top.equalTo(iconView.snp.bottom).offset(AppSpacing.sm)
}
percentLabel.snp.makeConstraints { make in
make.top.equalTo(progressView.snp.bottom).offset(AppSpacing.xxs)
make.trailing.equalTo(progressView)
}
}
@objc private func primaryTapped() {
onPrimaryAction?()
}
@objc private func cancelTapped() {
onCancel?()
}
}