// // 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 对齐文案。 var title: 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 = [] /// 当前文件夹 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?() } }