530 lines
24 KiB
Swift
530 lines
24 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|