feat: 添加云盘与消息中心功能

This commit is contained in:
2026-07-09 22:37:43 +08:00
parent 8e356973bd
commit f20ec7f06c
91 changed files with 5437 additions and 89 deletions

View File

@ -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?()
}
}