feat: 添加云盘与消息中心功能
This commit is contained in:
529
suixinkan/UI/CloudDrive/CloudDriveListViewController.swift
Normal file
529
suixinkan/UI/CloudDrive/CloudDriveListViewController.swift
Normal file
@ -0,0 +1,529 @@
|
||||
//
|
||||
// CloudDriveListViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
/// 相册云盘列表页,对齐 Android `CloudStorageListScreen`。
|
||||
final class CloudDriveListViewController: BaseViewController {
|
||||
private let viewModel = CloudDriveListViewModel()
|
||||
private let api: any CloudDriveServing
|
||||
private let transferManager: CloudTransferManager
|
||||
|
||||
private let pathScrollView = UIScrollView()
|
||||
private let pathStackView = UIStackView()
|
||||
private let searchContainer = UIView()
|
||||
private let searchIconView = UIImageView(image: CloudDriveAsset.image(named: "icon_cloud_storage_file_search", fallbackSystemName: "magnifyingglass"))
|
||||
private let searchField = UITextField()
|
||||
private let sortButton = UIButton(type: .system)
|
||||
private let filterButton = UIButton(type: .system)
|
||||
private let displayModeButton = UIButton(type: .system)
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Int, CloudFile>!
|
||||
private let uploadButton = AppButton(title: "上传")
|
||||
private let bottomBar = UIView()
|
||||
|
||||
/// 初始化相册云盘列表页。
|
||||
init(
|
||||
api: (any CloudDriveServing)? = nil,
|
||||
transferManager: CloudTransferManager = .shared
|
||||
) {
|
||||
self.api = api ?? NetworkServices.shared.cloudDriveAPI
|
||||
self.transferManager = transferManager
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "相册云盘"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_transit", fallbackSystemName: "arrow.up.arrow.down.circle"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(openTransferManager)
|
||||
)
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = .white
|
||||
|
||||
pathScrollView.showsHorizontalScrollIndicator = false
|
||||
pathStackView.axis = .horizontal
|
||||
pathStackView.alignment = .center
|
||||
pathStackView.spacing = AppSpacing.xs
|
||||
pathScrollView.addSubview(pathStackView)
|
||||
|
||||
searchContainer.backgroundColor = AppColor.inputBackground
|
||||
searchContainer.layer.cornerRadius = AppRadius.xs
|
||||
searchIconView.tintColor = AppColor.textTertiary
|
||||
searchField.placeholder = "搜索文件名称"
|
||||
searchField.font = .app(.body)
|
||||
searchField.textColor = AppColor.textPrimary
|
||||
searchField.returnKeyType = .search
|
||||
searchField.delegate = self
|
||||
searchContainer.addSubview(searchIconView)
|
||||
searchContainer.addSubview(searchField)
|
||||
|
||||
configureFilterButton(sortButton)
|
||||
configureFilterButton(filterButton)
|
||||
displayModeButton.tintColor = AppColor.textPrimary
|
||||
displayModeButton.backgroundColor = .white
|
||||
displayModeButton.layer.cornerRadius = AppRadius.xs
|
||||
|
||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())
|
||||
collectionView.backgroundColor = .white
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.delegate = self
|
||||
collectionView.register(CloudDriveFileCell.self, forCellWithReuseIdentifier: CloudDriveFileCell.reuseIdentifier)
|
||||
dataSource = UICollectionViewDiffableDataSource<Int, CloudFile>(collectionView: collectionView) { [weak self] collectionView, indexPath, file in
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: CloudDriveFileCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! CloudDriveFileCell
|
||||
cell.apply(file: file, mode: self?.viewModel.displayMode ?? .grid)
|
||||
cell.onDetails = { [weak self] in self?.showControlSheet(for: file) }
|
||||
return cell
|
||||
}
|
||||
|
||||
bottomBar.backgroundColor = .white
|
||||
bottomBar.layer.borderColor = AppColor.border.cgColor
|
||||
bottomBar.layer.borderWidth = 0.5
|
||||
|
||||
view.addSubview(pathScrollView)
|
||||
view.addSubview(searchContainer)
|
||||
view.addSubview(sortButton)
|
||||
view.addSubview(filterButton)
|
||||
view.addSubview(displayModeButton)
|
||||
view.addSubview(collectionView)
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(uploadButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
pathScrollView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(52)
|
||||
}
|
||||
pathStackView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.height.equalToSuperview()
|
||||
}
|
||||
searchContainer.snp.makeConstraints { make in
|
||||
make.top.equalTo(pathScrollView.snp.bottom).offset(2)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(36)
|
||||
}
|
||||
searchIconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(AppSpacing.sm)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(16)
|
||||
}
|
||||
searchField.snp.makeConstraints { make in
|
||||
make.leading.equalTo(searchIconView.snp.trailing).offset(AppSpacing.sm)
|
||||
make.top.bottom.trailing.equalToSuperview().inset(AppSpacing.xs)
|
||||
}
|
||||
sortButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(searchContainer.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.equalToSuperview().offset(AppSpacing.md)
|
||||
make.height.equalTo(36)
|
||||
}
|
||||
filterButton.snp.makeConstraints { make in
|
||||
make.top.width.height.equalTo(sortButton)
|
||||
make.leading.equalTo(sortButton.snp.trailing).offset(AppSpacing.sm)
|
||||
}
|
||||
displayModeButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(sortButton)
|
||||
make.leading.equalTo(filterButton.snp.trailing).offset(AppSpacing.sm)
|
||||
make.trailing.equalToSuperview().inset(6)
|
||||
make.width.height.equalTo(36)
|
||||
}
|
||||
sortButton.snp.makeConstraints { make in
|
||||
make.width.equalTo(filterButton)
|
||||
}
|
||||
bottomBar.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
uploadButton.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppSpacing.md)
|
||||
}
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(sortButton.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(bottomBar.snp.top)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
|
||||
displayModeButton.addTarget(self, action: #selector(displayModeTapped), for: .touchUpInside)
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
viewModel.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
transferManager.onUploadCompleted = { [weak self] folderId in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.handleUploadCompleted(parentFolderId: folderId, api: self.api) }
|
||||
}
|
||||
transferManager.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
configureMenus()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
Task { await viewModel.refresh(api: api, showLoading: true) }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
rebuildBreadcrumb()
|
||||
searchField.text = viewModel.searchText
|
||||
sortButton.setTitle(viewModel.sortType.title, for: .normal)
|
||||
filterButton.setTitle(viewModel.filterType.title, for: .normal)
|
||||
let displayIconName = viewModel.displayMode == .grid ? "icon_cloud_storage_show_type_list" : "icon_cloud_storage_show_type_table"
|
||||
let displayFallback = viewModel.displayMode == .grid ? "list.bullet" : "square.grid.2x2"
|
||||
displayModeButton.setImage(CloudDriveAsset.image(named: displayIconName, fallbackSystemName: displayFallback), for: .normal)
|
||||
sortButton.setImage(CloudDriveAsset.image(named: "icon_cloud_storage_list_sort", fallbackSystemName: "arrow.up.arrow.down"), for: .normal)
|
||||
filterButton.setImage(CloudDriveAsset.image(named: "icon_cloud_storage_file_filter", fallbackSystemName: "line.3.horizontal.decrease.circle"), for: .normal)
|
||||
collectionView.setCollectionViewLayout(makeLayout(), animated: false)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, CloudFile>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(viewModel.files)
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
|
||||
if viewModel.isLoading && viewModel.files.isEmpty {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
private func rebuildBreadcrumb() {
|
||||
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
let homeButton = UIButton(type: .system)
|
||||
homeButton.setImage(CloudDriveAsset.image(named: "icon_cloud_storage_go_home", fallbackSystemName: "house.fill"), for: .normal)
|
||||
homeButton.tintColor = AppColor.textPrimary
|
||||
homeButton.addTarget(self, action: #selector(homeTapped), for: .touchUpInside)
|
||||
homeButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
pathStackView.addArrangedSubview(homeButton)
|
||||
|
||||
for (index, item) in viewModel.pathStack.enumerated() {
|
||||
if index > 0 {
|
||||
let arrow = UIImageView(image: CloudDriveAsset.image(named: "icon_cloud_storage_current_path_arrow", fallbackSystemName: "chevron.right"))
|
||||
arrow.tintColor = AppColor.textTertiary
|
||||
arrow.snp.makeConstraints { make in
|
||||
make.width.equalTo(8)
|
||||
}
|
||||
pathStackView.addArrangedSubview(arrow)
|
||||
}
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(item.name, for: .normal)
|
||||
button.titleLabel?.font = .app(index == viewModel.pathStack.count - 1 ? .body : .caption)
|
||||
button.setTitleColor(index == viewModel.pathStack.count - 1 ? .black : AppColor.textSecondary, for: .normal)
|
||||
button.tag = index
|
||||
button.addTarget(self, action: #selector(pathTapped(_:)), for: .touchUpInside)
|
||||
pathStackView.addArrangedSubview(button)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureMenus() {
|
||||
sortButton.menu = UIMenu(children: CloudDriveSortType.allCases.map { type in
|
||||
UIAction(title: type.title) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.chooseSortType(type, api: self.api) }
|
||||
}
|
||||
})
|
||||
sortButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
filterButton.menu = UIMenu(children: CloudDriveFilterType.allCases.map { type in
|
||||
UIAction(title: type.title) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.chooseFilterType(type, api: self.api) }
|
||||
}
|
||||
})
|
||||
filterButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
private func configureFilterButton(_ button: UIButton) {
|
||||
button.backgroundColor = AppColor.inputBackground
|
||||
button.layer.cornerRadius = AppRadius.xs
|
||||
button.titleLabel?.font = .app(.caption)
|
||||
button.setTitleColor(AppColor.textPrimary, for: .normal)
|
||||
button.contentHorizontalAlignment = .left
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
button.tintColor = AppColor.textPrimary
|
||||
button.semanticContentAttribute = .forceLeftToRight
|
||||
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: AppSpacing.xs)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: AppSpacing.sm, bottom: 0, right: AppSpacing.sm)
|
||||
}
|
||||
|
||||
private func makeLayout() -> UICollectionViewCompositionalLayout {
|
||||
UICollectionViewCompositionalLayout { [weak self] _, environment in
|
||||
let mode = self?.viewModel.displayMode ?? .grid
|
||||
switch mode {
|
||||
case .grid:
|
||||
let columns = 2
|
||||
let spacing: CGFloat = 15
|
||||
let horizontalInset: CGFloat = 12
|
||||
let availableWidth = environment.container.effectiveContentSize.width - horizontalInset * 2
|
||||
let itemWidth = (availableWidth - spacing) / CGFloat(columns)
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth), heightDimension: .absolute(itemWidth * 0.82))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemWidth * 0.82))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: columns)
|
||||
group.interItemSpacing = .fixed(spacing)
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: horizontalInset, bottom: 12, trailing: horizontalInset)
|
||||
section.interGroupSpacing = spacing
|
||||
return section
|
||||
case .list:
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(72))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: AppSpacing.md, bottom: AppSpacing.md, trailing: AppSpacing.md)
|
||||
return section
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showControlSheet(for file: CloudFile) {
|
||||
viewModel.setControlFile(file)
|
||||
let sheet = CloudDriveActionSheetViewController(title: "请选择", items: [
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "删除",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_file_delete", fallbackSystemName: "trash"),
|
||||
isDestructive: true
|
||||
) { [weak self] in self?.confirmDelete() },
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "下载",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_file_download", fallbackSystemName: "arrow.down.circle"),
|
||||
isDestructive: false
|
||||
) { [weak self] in self?.download(file) },
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "移动",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_file_move", fallbackSystemName: "folder"),
|
||||
isDestructive: false
|
||||
) { [weak self] in self?.showMoveDialog() },
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "详情",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_file_item_details", fallbackSystemName: "info.circle"),
|
||||
isDestructive: false
|
||||
) { [weak self] in self?.showDetailDialog(file) }
|
||||
])
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
private func confirmDelete() {
|
||||
showAlert(title: "删除确认", message: "删除后无法恢复,确认删除吗?") { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.deleteCurrentFile(api: self.api) }
|
||||
}
|
||||
}
|
||||
|
||||
private func download(_ file: CloudFile) {
|
||||
guard file.isSelectableMedia else {
|
||||
showToast("当前类型不支持下载")
|
||||
return
|
||||
}
|
||||
guard !file.fileUrl.isEmpty else {
|
||||
showToast("文件地址不存在")
|
||||
return
|
||||
}
|
||||
transferManager.addDownloadTask(fileURL: file.fileUrl, fileName: file.name, fileType: file.type, fileSize: file.fileSize)
|
||||
}
|
||||
|
||||
private func showMoveDialog() {
|
||||
let items = viewModel.moveTargetFolders().map { folder in
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: folder.name,
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_file_type_folder", fallbackSystemName: "folder"),
|
||||
isDestructive: false
|
||||
) { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.moveCurrentFile(targetFolderId: folder.id, api: self.api) }
|
||||
}
|
||||
}
|
||||
present(CloudDriveActionSheetViewController(title: "移动到", items: items), animated: true)
|
||||
}
|
||||
|
||||
private func showDetailDialog(_ file: CloudFile) {
|
||||
let sheet = CloudDriveDetailSheetViewController(file: file) { [weak self] name in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.saveCurrentFileName(name, api: self.api) }
|
||||
}
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
@objc private func uploadTapped() {
|
||||
let sheet = CloudDriveActionSheetViewController(title: "选择上传类型", items: [
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "上传图片",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_add_photo", fallbackSystemName: "photo"),
|
||||
isDestructive: false
|
||||
) { [weak self] in self?.beginPick(type: 1) },
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "上传视频",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_add_video", fallbackSystemName: "video"),
|
||||
isDestructive: false
|
||||
) { [weak self] in self?.beginPick(type: 2) },
|
||||
CloudDriveActionSheetViewController.Item(
|
||||
title: "新建文件夹",
|
||||
image: CloudDriveAsset.image(named: "icon_cloud_storage_add_folder", fallbackSystemName: "folder.badge.plus"),
|
||||
isDestructive: false
|
||||
) { [weak self] in self?.showCreateFolderDialog() }
|
||||
])
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
private func beginPick(type: Int) {
|
||||
Task {
|
||||
guard await viewModel.checkCanUpload(api: api) else { return }
|
||||
await MainActor.run {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||
configuration.selectionLimit = 1
|
||||
configuration.filter = type == 1 ? .images : .videos
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self
|
||||
picker.view.tag = type
|
||||
present(picker, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showCreateFolderDialog() {
|
||||
guard viewModel.pathStack.count < 5 else {
|
||||
showToast("不能创建更深的文件夹")
|
||||
return
|
||||
}
|
||||
let alert = UIAlertController(title: "新建文件夹", message: nil, preferredStyle: .alert)
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = "请输入文件夹名称"
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self, weak alert] _ in
|
||||
guard let self else { return }
|
||||
let name = alert?.textFields?.first?.text ?? ""
|
||||
Task { await self.viewModel.addFolder(name: name, api: self.api) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func displayModeTapped() {
|
||||
viewModel.toggleDisplayMode()
|
||||
}
|
||||
|
||||
@objc private func openTransferManager() {
|
||||
navigationController?.pushViewController(CloudDriveTransferViewController(manager: transferManager), animated: true)
|
||||
}
|
||||
|
||||
@objc private func homeTapped() {
|
||||
Task { await viewModel.goHome(api: api) }
|
||||
}
|
||||
|
||||
@objc private func pathTapped(_ sender: UIButton) {
|
||||
Task { await viewModel.navigateToPathIndex(sender.tag, api: api) }
|
||||
}
|
||||
|
||||
private func openPreview(file: CloudFile) {
|
||||
navigationController?.pushViewController(CloudDrivePreviewViewController(file: file), animated: true)
|
||||
}
|
||||
|
||||
private func stagePickedFile(result: PHPickerResult, fileType: Int) {
|
||||
let provider = result.itemProvider
|
||||
let typeIdentifier = fileType == 2 ? UTType.image.identifier : UTType.movie.identifier
|
||||
provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [weak self] url, error in
|
||||
guard let self else { return }
|
||||
if let error {
|
||||
Task { @MainActor in self.showToast(error.localizedDescription) }
|
||||
return
|
||||
}
|
||||
guard let url else { return }
|
||||
do {
|
||||
let stagedURL = try Self.copyPickedFileToStage(url: url, suggestedName: provider.suggestedName, fileType: fileType)
|
||||
let size = (try? FileManager.default.attributesOfItem(atPath: stagedURL.path)[.size] as? NSNumber)?.int64Value ?? 0
|
||||
Task { @MainActor in
|
||||
self.transferManager.addUploadTask(
|
||||
localFileURL: stagedURL,
|
||||
fileName: stagedURL.lastPathComponent,
|
||||
fileType: fileType,
|
||||
fileSize: size,
|
||||
parentFolderId: self.viewModel.currentFolderID
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
Task { @MainActor in self.showToast(error.localizedDescription) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func copyPickedFileToStage(url: URL, suggestedName: String?, fileType: Int) throws -> URL {
|
||||
let directory = FileManager.default.temporaryDirectory.appendingPathComponent("cloud_drive_uploads", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let ext = url.pathExtension.isEmpty ? (fileType == 2 ? "jpg" : "mp4") : url.pathExtension
|
||||
let baseName = CloudTransferFileName.sanitized(suggestedName ?? "cloud_upload")
|
||||
let fileName = baseName.lowercased().hasSuffix(".\(ext.lowercased())") ? baseName : "\(baseName).\(ext)"
|
||||
let destination = directory.appendingPathComponent("\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))_\(fileName)")
|
||||
if FileManager.default.fileExists(atPath: destination.path) {
|
||||
try FileManager.default.removeItem(at: destination)
|
||||
}
|
||||
try FileManager.default.copyItem(at: url, to: destination)
|
||||
return destination
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudDriveListViewController: UITextFieldDelegate {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
viewModel.updateSearchText(textField.text ?? "")
|
||||
textField.resignFirstResponder()
|
||||
Task { await viewModel.refresh(api: api, showLoading: true) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudDriveListViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let file = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
Task {
|
||||
if let preview = await viewModel.handleFileTap(file, api: api) {
|
||||
await MainActor.run { self.openPreview(file: preview) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let offsetY = scrollView.contentOffset.y
|
||||
let contentHeight = scrollView.contentSize.height
|
||||
let frameHeight = scrollView.frame.size.height
|
||||
if offsetY > contentHeight - frameHeight - 120 {
|
||||
let lastIndex = max(0, viewModel.files.count - 1)
|
||||
Task { await viewModel.loadMoreIfNeeded(lastVisibleIndex: lastIndex, api: api) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudDriveListViewController: PHPickerViewControllerDelegate {
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
let type = picker.view.tag
|
||||
picker.dismiss(animated: true)
|
||||
guard let result = results.first else { return }
|
||||
stagePickedFile(result: result, fileType: type == 2 ? 1 : 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user