feat: 添加云盘与消息中心功能
This commit is contained in:
@ -0,0 +1,353 @@
|
||||
//
|
||||
// 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<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?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user