# coding: utf-8 from loguru import logger from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtWidgets import QFileDialog, QVBoxLayout, QWidget from qfluentwidgets import ( Action, InfoBar, InfoBarPosition, MenuAnimationType, RoundMenu, ScrollArea, ) from qfluentwidgets import FluentIcon as FIF from app.core import (lang, ListFileThread, ListSearchThread, ListShareThread, policyConfig, signalBus) from app.view.components.file_card import FileCard, ShareFileCard from app.view.widgets.new_folder_messageBox import NewFolderMessageBox from app.view.widgets.policy_messageBox import PolicyChooseMessageBox class LinkageSwitchingBase(ScrollArea): """文件卡片滚动区域组件基""" def __init__(self, parent=None): super().__init__(parent) self.fileCardsDict = {} # 存储所有文件卡片 self.widgets = QWidget() self.layouts = QVBoxLayout(self.widgets) self.layouts.setAlignment(Qt.AlignmentFlag.AlignTop) self.setWidget(self.widgets) self.setWidgetResizable(True) self.layouts.setContentsMargins(5, 5, 5, 0) self.layouts.setSpacing(5) self.widgets.setStyleSheet("background-color: transparent; border: none;") self.setStyleSheet("background-color: transparent; border: none;") self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) def addFileCard(self, fileId, data): """ 添加文件卡片 Args: fileId: 文件的唯一标识符 data: 文件数据对象 Returns: 创建的文件卡片对象 """ if fileId in self.fileCardsDict: logger.warning(f"文件卡片已存在: {fileId}") return self.fileCardsDict[fileId] # 安全地获取对象属性,提供默认值以避免KeyError fileId = data.get("id", "") fileName = data.get("name", "未知文件") # Cloudreve V4 API使用数字1表示文件夹,0表示文件 fileType_num = data.get("type", 0) # 将数字类型转换为字符串表示 fileType = "folder" if fileType_num == 1 else "file" filePath = data.get("path", "") # 使用created_at或updated_at作为日期 fileDate = data.get("created_at", data.get("date", "")) fileSize = data.get("size", 0) # 构建符合Cloudreve V4 API要求的正确URI格式 if filePath == "/" or filePath == "": # 根目录情况 full_uri = f"cloudreve://my/{fileName}" else: # 子目录情况,确保正确拼接路径 # 清理路径,避免重复的斜杠和前缀 clean_path = filePath.lstrip("/") # 确保路径中不包含cloudreve://my前缀 clean_path = clean_path.replace("cloudreve://my/", "") full_uri = f"cloudreve://my/{clean_path}/{fileName}" # 确保路径格式正确,移除重复的前缀 full_uri = full_uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my") # 更健壮地处理重复文件名的情况 # 分割路径并去重 path_parts = full_uri.split('/') if len(path_parts) > 1: # 检查最后一个部分是否是文件名 if path_parts[-1] == fileName: # 检查倒数第二个部分是否也是文件名 if len(path_parts) > 2 and path_parts[-2] == fileName: # 移除重复的文件名部分 path_parts.pop(-2) full_uri = '/'.join(path_parts) logger.debug(f"构建文件URI: {full_uri}") fileCard = FileCard( full_uri, # 传递完整的URI而不是文件ID fileName, fileType, filePath, fileDate, fileSize, self, ) fileCard.setObjectName(f"fileCard_{fileId}") self.fileCardsDict[fileId] = fileCard self.layouts.addWidget(fileCard) return fileCard def removeFileCard(self, fileId): """移除文件卡片""" if fileId in self.fileCardsDict: fileCard = self.fileCardsDict[fileId] self.layouts.removeWidget(fileCard) fileCard.deleteLater() del self.fileCardsDict[fileId] else: logger.warning(f"尝试移除不存在的文件卡片: {fileId}") def clearFileCards(self): """清除所有文件卡片""" logger.debug("清除所有文件卡片") fileIds = list(self.fileCardsDict.keys()) for fileId in fileIds: self.removeFileCard(fileId) def refreshFolderList(self): logger.debug("刷新文件夹列表") InfoBar.success( "成功", "刷新成功", Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) signalBus.refreshFolderListSignal.emit() # 个人文件浏览区域 class OwnFileLinkageSwitching(LinkageSwitchingBase): """个人件卡片滚动区域组件""" def __init__(self, paths, parent=None): super(OwnFileLinkageSwitching, self).__init__(parent) self.currentPath = paths self.fileCardsDict = {} # 存储所有文件卡片 self.loadDict("/") def contextMenuEvent(self, e): """菜单事件""" logger.debug("触发上下文菜单事件") menu = RoundMenu(parent=self) menu.addAction( Action(FIF.SYNC, lang("刷新当前"), triggered=self.refreshFolderList) ) menu.addSeparator() menu.addAction( Action(FIF.ADD, lang("新建文件夹"), triggered=self._createFolder) ) menu.addSeparator() menu.addAction(Action(FIF.UP, lang("上传文件"), triggered=self._uploadFile)) menu.addSeparator() menu.addAction( Action(FIF.CLOUD, lang("设置存储策略"), triggered=self._choosePolicy) ) menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN) def _choosePolicy(self): w = PolicyChooseMessageBox(self.window()) if w.exec(): ... def _createFolder(self): w = NewFolderMessageBox(self.window()) if w.exec(): ... def _uploadFile(self): file_name, _ = QFileDialog.getOpenFileName( self.window(), "选择文件", "", "所有文件 (*)" ) if file_name: signalBus.addUploadFileTask.emit(file_name) def loadDict(self, paths): """加载目录数据""" logger.info(f"加载目录数据: {paths}") policyConfig.setCurrentPath(paths) self.currentPath = paths self.loadDataThread = ListFileThread(paths) self.loadDataThread.listDictSignal.connect(self.dealData) self.loadDataThread.errorSignal.connect(self._errorLoadDict) self.loadDataThread.start() def dealData(self, data): """处理目录数据""" self.clearFileCards() logger.info("设置当前页策略") # 安全地访问策略信息,考虑data["data"]可能是列表的情况 if isinstance(data, dict) and "data" in data: data_content = data["data"] if isinstance(data_content, dict): # Cloudreve V4 API格式处理 if "storage_policy" in data_content: policyConfig.setPolicy(data_content["storage_policy"]) elif "policy" in data_content: policyConfig.setPolicy(data_content["policy"]) elif isinstance(data_content, list) and data_content: # 如果data_content是列表,尝试从第一个元素获取策略 logger.warning("data['data']是列表而不是字典,可能需要调整API响应处理") # 处理data["data"]可能是列表或字典的情况 data_content = data.get("data", {}) if isinstance(data_content, list): # 如果是列表,直接使用列表作为objects logger.info(f"成功加载目录数据,对象数量: {len(data_content)}") self.objects = data_content elif isinstance(data_content, dict): # Cloudreve V4 API格式处理,先检查files字段 if "files" in data_content: # Cloudreve V4 API 使用files字段存储文件列表 logger.info(f"成功加载目录数据,对象数量: {len(data_content['files'])}") self.objects = data_content["files"] elif "objects" in data_content: # 向后兼容旧版API logger.info(f"成功加载目录数据,对象数量: {len(data_content['objects'])}") self.objects = data_content["objects"] else: logger.error("目录数据格式错误:字典中没有files或objects字段") return else: logger.error("目录数据格式错误") return for obj in self.objects: self.addFileCard(obj["id"], obj) def _errorLoadDict(self, error_msg): """处理加载目录数据失败""" logger.error(f"加载目录数据失败: {error_msg}") InfoBar.error( "错误", f"加载目录数据失败: {error_msg}", Qt.Orientation.Horizontal, True, -1, InfoBarPosition.TOP_RIGHT, self.window(), ) self.loadDict("/") # 搜索文件浏览区域 class SearchLinkageSwitching(LinkageSwitchingBase): """文件卡片滚动区域组件""" def __init__(self, parent=None): super(SearchLinkageSwitching, self).__init__(parent) self.fileCardsDict = {} # 存储所有文件卡片 def search(self, searchType, searchContent): """加载数据""" self.loadDataThread = ListSearchThread(searchContent, searchType) self.loadDataThread.listDictSignal.connect(self._dealData) self.loadDataThread.errorSignal.connect(self._error) self.loadDataThread.start() def _dealData(self, data): """处理数据""" if not data or "data" not in data or "objects" not in data["data"]: logger.error("数据格式错误") return logger.info(f"成功加载数据,对象数量: {len(data['data']['objects'])}") self.objects = data["data"]["objects"] self.clearFileCards() for obj in self.objects: self.addFileCard(obj["id"], obj) def _error(self, msg): """处理错误""" logger.error(f"加载数据失败: {msg}") InfoBar.error( "错误", msg, Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) # 分享文件浏览区域 class ShareLinkageSwitching(LinkageSwitchingBase): """文件卡片滚动区域组件""" totalItemsSignal = pyqtSignal(int) # 信号:传递总数量 def __init__(self, parent=None): super().__init__(parent) self.fileCardsDict = {} # 存储所有文件卡片 logger.debug(f"初始化搜索卡片滚动区域") def addFileCard(self, fileId, obj): if fileId in self.fileCardsDict: logger.warning(f"文件卡片已存在: {fileId}") return self.fileCardsDict[fileId] fileCard = ShareFileCard( obj, self, ) fileCard.setObjectName(f"fileCard_{fileId}") self.fileCardsDict[fileId] = fileCard self.layouts.addWidget(fileCard) return fileCard def search(self, keyword, orderBy, order, page): """加载数据""" self.loadDataThread = ListShareThread(keyword, orderBy, order, page) self.loadDataThread.listDictSignal.connect(self._dealData) self.loadDataThread.errorSignal.connect(self._error) self.loadDataThread.start() def _dealData(self, data): """处理数据""" if not data or "data" not in data: logger.error("数据格式错误:缺少data字段") return # 处理data["data"]可能是列表或字典的情况 data_content = data["data"] if isinstance(data_content, list): logger.warning("data['data']是列表而不是字典,将直接使用列表数据") self.objects = data_content elif isinstance(data_content, dict): # 尝试从字典中获取对象列表,按照Cloudreve V4 API格式处理 if "files" in data_content: # Cloudreve V4 API 使用files字段存储文件列表 self.objects = data_content["files"] elif "items" in data_content: self.objects = data_content["items"] elif "objects" in data_content: self.objects = data_content["objects"] else: logger.error("数据格式错误:字典中没有files、items或objects字段") return else: logger.error(f"数据格式错误:data['data']类型为{type(data_content).__name__},应为列表或字典") return logger.info(f"成功加载数据,对象数量: {len(self.objects)}") # 尝试获取总数,如果不存在则不发送信号 if isinstance(data_content, dict) and "total" in data_content: self.totalItemsSignal.emit(data_content["total"]) self.clearFileCards() for obj in self.objects: # 使用obj中可能存在的不同键名 file_id = obj.get("key", obj.get("id", None)) file_path = obj.get("path", None) if file_id: self.addFileCard(file_id, obj) def _error(self, error): """处理错误""" logger.error(f"加载数据失败: {error}") InfoBar.error( "错误", f"加载数据失败: {error}", Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), )