Files
suixinkan_uikit/suixinkan/Features/CloudDrive/ViewModels/CloudDriveListViewModel.swift

371 lines
12 KiB
Swift
Raw 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.

//
// CloudDriveListViewModel.swift
// suixinkan
//
import Foundation
///
enum CloudDriveDisplayMode: Sendable, Equatable {
case grid
case list
}
///
enum CloudDriveSortType: Int, CaseIterable, Sendable {
case createdAscending = 1
case createdDescending = 2
/// Android
var title: String {
switch self {
case .createdAscending:
"创建时间顺序"
case .createdDescending:
"创建时间倒序"
}
}
}
///
enum CloudDriveFilterType: Int, CaseIterable, Sendable {
case all = 0
case video = 1
case image = 2
case folder = 99
/// Android
static let androidMenuOrder: [CloudDriveFilterType] = [.all, .image, .video, .folder]
/// Android
var menuTitle: String {
switch self {
case .all:
"全部"
case .video:
"视频"
case .image:
"图片"
case .folder:
"文件夹"
}
}
/// Android
var displayTitle: String {
switch self {
case .all:
"全部"
case .video:
"视频"
case .image:
"文件"
case .folder:
"文件夹"
}
}
}
private struct CloudDriveCacheKey: Hashable {
let folderId: Int
let name: String
let type: Int
let orderBy: Int
}
private struct CloudDriveFolderCache {
let list: [CloudFile]
let total: Int
let page: Int
}
/// ViewModel Android `CloudStorageListViewModel`
final class CloudDriveListViewModel {
private(set) var pathStack: [CloudPathItem] = [CloudPathItem(id: 0, name: "云盘")]
private(set) var files: [CloudFile] = []
private(set) var searchText = ""
private(set) var filterType: CloudDriveFilterType = .all
private(set) var sortType: CloudDriveSortType = .createdDescending
private(set) var displayMode: CloudDriveDisplayMode = .grid
private(set) var isLoading = false
private(set) var isRefreshing = false
private(set) var canLoadMore = false
private(set) var currentControlFile: CloudFile?
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private var currentPage = 1
private var totalCount = 0
private let pageSize = 20
private var folderCache: [CloudDriveCacheKey: CloudDriveFolderCache] = [:]
private var dirtyFolders: Set<Int> = []
/// ID
var currentFolderID: Int {
pathStack.last?.id ?? 0
}
///
func updateSearchText(_ text: String) {
searchText = text
notifyStateChange()
}
/// /
func toggleDisplayMode() {
displayMode = displayMode == .grid ? .list : .grid
notifyStateChange()
}
///
func chooseSortType(_ type: CloudDriveSortType, api: any CloudDriveServing) async {
sortType = type
await refresh(api: api, showLoading: true)
}
///
func chooseFilterType(_ type: CloudDriveFilterType, api: any CloudDriveServing) async {
filterType = type
await refresh(api: api, showLoading: true)
}
///
func refresh(api: any CloudDriveServing, showLoading: Bool = false) async {
currentPage = 1
isRefreshing = true
notifyStateChange()
await loadPage(api: api, reset: true, showLoading: showLoading)
isRefreshing = false
notifyStateChange()
}
///
func loadMoreIfNeeded(lastVisibleIndex: Int, api: any CloudDriveServing) async {
guard canLoadMore, !isLoading, lastVisibleIndex >= files.count - 6 else { return }
currentPage += 1
await loadPage(api: api, reset: false, showLoading: false)
}
/// UI
func handleFileTap(_ file: CloudFile, api: any CloudDriveServing) async -> CloudFile? {
if file.isFolder {
searchText = ""
pathStack.append(CloudPathItem(id: file.id, name: file.name))
await refresh(api: api, showLoading: true)
return nil
}
guard file.isSelectableMedia else { return nil }
return file
}
///
func setControlFile(_ file: CloudFile) {
currentControlFile = file
notifyStateChange()
}
/// false UI pop
func navigateBack(api: any CloudDriveServing) async -> Bool {
guard pathStack.count > 1 else { return false }
pathStack.removeLast()
await restoreOrReload(api: api)
return true
}
///
func goHome(api: any CloudDriveServing) async {
pathStack = [pathStack.first ?? CloudPathItem(id: 0, name: "云盘")]
await restoreOrReload(api: api)
}
///
func navigateToPathIndex(_ index: Int, api: any CloudDriveServing) async {
guard index >= 0, index < pathStack.count else { return }
pathStack = Array(pathStack.prefix(index + 1))
await restoreOrReload(api: api)
}
///
func checkCanUpload(api: any CloudDriveServing) async -> Bool {
do {
let response = try await api.checkUploadPermission()
if !response.canUpload {
onShowMessage?(response.reason)
}
return response.canUpload
} catch {
onShowMessage?(error.localizedDescription)
return false
}
}
///
func addFolder(name: String, api: any CloudDriveServing) async {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty else {
onShowMessage?("名称不能为空")
return
}
guard pathStack.count < 5 else {
onShowMessage?("不能创建更深的文件夹")
return
}
await performLoading {
try await api.createCloudFolder(parentFolderId: currentFolderID, name: trimmedName)
invalidateFolderCache(currentFolderID)
await refresh(api: api, showLoading: false)
}
}
///
func deleteCurrentFile(api: any CloudDriveServing) async {
guard let file = currentControlFile else { return }
await performLoading {
try await api.deleteCloudFiles(
CloudFileDeleteRequest(list: [CloudFileOperationItem(id: file.id, type: file.type)])
)
invalidateFolderCache(currentFolderID)
await refresh(api: api, showLoading: false)
}
}
///
func moveCurrentFile(targetFolderId: Int, api: any CloudDriveServing) async {
guard let file = currentControlFile else { return }
await performLoading {
try await api.moveCloudFiles(
CloudFileMoveRequest(
targetFolderId: targetFolderId,
list: [CloudFileOperationItem(id: file.id, type: file.type)]
)
)
invalidateFolderCache(currentFolderID)
invalidateFolderCache(targetFolderId)
await refresh(api: api, showLoading: false)
}
}
///
func saveCurrentFileName(_ name: String, api: any CloudDriveServing) async {
guard let file = currentControlFile else { return }
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty else {
onShowMessage?("名称不能为空")
return
}
guard trimmedName != file.name else { return }
await performLoading {
if file.isFolder {
try await api.modifyCloudFolderName(fileId: file.id, name: trimmedName)
} else {
try await api.modifyCloudFileName(fileId: file.id, fileName: trimmedName)
}
invalidateFolderCache(currentFolderID)
await refresh(api: api, showLoading: false)
}
}
///
func handleUploadCompleted(parentFolderId: Int, api: any CloudDriveServing) async {
if parentFolderId == currentFolderID {
invalidateFolderCache(parentFolderId)
await refresh(api: api, showLoading: false)
} else {
invalidateFolderCache(parentFolderId)
}
}
/// Android +
func moveTargetFolders() -> [CloudFile] {
let root = CloudFile(id: 0, name: "云盘", type: 99)
let folders = files.filter { $0.isFolder }
return ([root] + folders).reduce(into: [CloudFile]()) { result, item in
guard !result.contains(where: { $0.id == item.id }) else { return }
result.append(item)
}
}
private func loadPage(api: any CloudDriveServing, reset: Bool, showLoading: Bool) async {
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
let response = try await api.cloudFileList(
parentFolderId: currentFolderID,
name: searchText,
type: filterType.rawValue,
orderBy: sortType.rawValue,
page: currentPage,
pageSize: pageSize
)
totalCount = response.total
files = reset ? response.list : files + response.list
canLoadMore = files.count < totalCount
folderCache[currentKey()] = CloudDriveFolderCache(list: files, total: totalCount, page: currentPage)
} catch is CancellationError {
return
} catch {
if reset {
files = []
}
canLoadMore = false
onShowMessage?(error.localizedDescription)
}
}
private func restoreOrReload(api: any CloudDriveServing) async {
if dirtyFolders.remove(currentFolderID) != nil {
await refresh(api: api, showLoading: true)
return
}
if let cached = folderCache[currentKey()] {
files = cached.list
totalCount = cached.total
currentPage = cached.page
canLoadMore = files.count < totalCount
notifyStateChange()
} else {
await refresh(api: api, showLoading: true)
}
}
private func performLoading(_ operation: () async throws -> Void) async {
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
do {
try await operation()
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription)
}
}
private func currentKey() -> CloudDriveCacheKey {
CloudDriveCacheKey(
folderId: currentFolderID,
name: searchText,
type: filterType.rawValue,
orderBy: sortType.rawValue
)
}
private func invalidateFolderCache(_ folderId: Int) {
folderCache = folderCache.filter { $0.key.folderId != folderId }
dirtyFolders.insert(folderId)
}
private func notifyStateChange() {
onStateChange?()
}
}