# coding: utf-8 import re from loguru import logger from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QPixmap from PyQt6.QtWidgets import QHBoxLayout from qfluentwidgets import Action, BodyLabel, CardWidget, ImageLabel, InfoBar, InfoBarPosition, MenuAnimationType, \ MessageBox, PushButton, RoundMenu, StrongBodyLabel from qfluentwidgets import FluentIcon as FIF from app.core import (DeleteFileThread, RenameFileThread, formatDate, formatSize, getFileIcon, lang, signalBus) from app.view.widgets.share_file_messageBox import ShareFileMessageBox class FileCard(CardWidget): def __init__(self, _id, fileName, fileType, path, date, size, parent=None): super().__init__(parent) self._id = _id self.fileName = fileName self.fileSize = size self.changeTime = date self.fileType = fileType self.filePath = path self.setFixedHeight(50) self.iconLabel = ImageLabel(self) self.fileNameLabel = StrongBodyLabel(self.fileName, self) self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self) self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self) self.hBoxLayout = QHBoxLayout(self) self.hBoxLayout.addWidget(self.iconLabel) self.hBoxLayout.addWidget(self.fileNameLabel) self.hBoxLayout.addWidget(self.fileSizeLabel) self.hBoxLayout.addWidget(self.changeTimeLabel) self.hBoxLayout.setStretch(0, 1) self.hBoxLayout.setStretch(1, 2) self.hBoxLayout.setStretch(2, 1) self.hBoxLayout.setContentsMargins(10, 10, 10, 10) self.hBoxLayout.setSpacing(10) self.loadIcon() if self.fileType == "dir": self.clicked.connect(self.dirClicked) self.suffix = fileName.split(".")[-1].lower() def mousePressEvent(self, event): """重写鼠标点击事件,区分左右键""" if event.button() == Qt.MouseButton.RightButton: # 使用globalPosition()获取全局位置并转换为适合菜单显示的坐标 global_pos = event.globalPosition().toPoint() if self.fileType == "file": self.showFileContextMenu(global_pos) else: self.showFolderContextMenu(global_pos) else: super().mousePressEvent(event) def loadIcon(self): icon_name = getFileIcon(self.fileType, self.fileName) self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}")) self.iconLabel.scaledToHeight(25) self.iconLabel.scaledToWidth(25) def dirClicked(self): if self.filePath == "/": paths = "/" + self.fileName else: paths = f"{self.filePath}/{self.fileName}" signalBus.dirOpenSignal.emit(paths) def selfPreview(self): if self.fileType == "file" and self.suffix in [ "jpg", "png", "jpeg", "bmp", "gif", ]: signalBus.imagePreviewSignal.emit(self._id) if self.fileType == "file" and self.suffix in ["txt", "py", "md", "js", "html"]: signalBus.txtPreviewSignal.emit(self._id) def downloadFile(self): if self.fileType == "file": # 构建Cloudreve V4 API所需的正确路径格式 # 确保不会出现重复的前缀和文件名 if self.filePath == "/": # 根目录情况 full_path = f"cloudreve://my/{self.fileName}" else: # 子目录情况,确保正确拼接路径 # 清理路径,避免重复的斜杠和前缀 clean_path = self.filePath.lstrip("/") # 确保路径中不包含cloudreve://my前缀 clean_path = clean_path.replace("cloudreve://my/", "") full_path = f"cloudreve://my/{clean_path}/{self.fileName}" # 确保路径格式正确,移除重复的前缀 full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my") # 更健壮地处理重复文件名的情况 # 分割路径并去重 path_parts = full_path.split('/') if len(path_parts) > 1: # 检查最后一个部分是否是文件名 if path_parts[-1] == self.fileName: # 检查倒数第二个部分是否也是文件名 if len(path_parts) > 2 and path_parts[-2] == self.fileName: # 移除重复的文件名部分 path_parts.pop(-2) full_path = '/'.join(path_parts) signalBus.addDownloadFileTask.emit( f"own.{self.suffix}", self.fileName, full_path ) def showFileContextMenu(self, pos): """显示上下文菜单""" menu = RoundMenu(parent=self) menu.addActions( [ Action( FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile() ), Action( FIF.PROJECTOR, lang("预览"), triggered=lambda: self.selfPreview() ), Action( FIF.EDIT, lang("重命名"), triggered=lambda: self.renameSelf() ), ] ) menu.addSeparator() menu.addActions( [ Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()), ] ) menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN) def showFolderContextMenu(self, pos): """显示上下文菜单""" menu = RoundMenu(parent=self) menu.addActions( [ Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()), Action( FIF.EDIT, lang("重命名"), triggered=lambda: self.renameSelf() ), ] ) menu.addSeparator() menu.addActions( [ Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()), ] ) menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN) def deleteSelf(self): w = MessageBox( "确认删除", f"你确定要删除{self.fileName}吗?\n删除后不可恢复噢!", parent=self.window(), ) if w.exec(): # 构建Cloudreve V4 API所需的正确路径格式 # 确保路径格式规范,避免Path not exist错误 # 只使用路径中的目录部分,不包含文件名,确保不会重复 dir_path = self.filePath if dir_path == "/": # 根目录情况 full_path = f"cloudreve://my/{self.fileName}" else: # 子目录情况,确保只使用目录路径 # 清理路径,移除协议前缀 clean_dir = dir_path if clean_dir.startswith("cloudreve://my/"): clean_dir = clean_dir[15:] # 移除 "cloudreve://my/" elif clean_dir.startswith("/"): clean_dir = clean_dir[1:] # 移除前导斜杠 # 确保路径中间没有多余的斜杠 clean_dir = '/'.join([part for part in clean_dir.split('/') if part]) # 构建完整URI,只包含目录路径和文件名一次 full_path = f"cloudreve://my/{clean_dir}/{self.fileName}" # 检查并移除可能的重复文件名部分 # 避免同时包含编码形式和原始形式的文件名 import re # 查找可能的编码形式+原始形式的重复模式 path_parts = full_path.split('/') if len(path_parts) > 3: # 检查最后两个部分是否可能是编码和原始形式的关系 potential_encoding = path_parts[-2] original = path_parts[-1] # 如果倒数第二个部分看起来像URL编码且最后一个部分是中文 if re.match(r'^%[0-9A-Fa-f]{2}%[0-9A-Fa-f]{2}%[0-9A-Fa-f]{2}%[0-9A-Fa-f]{2}$', potential_encoding) and any('\u4e00'-'\u9fff' in c for c in original): # 移除倒数第二个部分,避免重复 path_parts.pop(-2) full_path = '/'.join(path_parts) # 最终清理 full_path = full_path.replace("//", "/") # 移除多余的斜杠 # 记录构建的路径用于调试 print(f"[DEBUG] 重命名路径: {full_path}") self.deleteThread = DeleteFileThread(full_path, self.fileType) self.deleteThread.successDelete.connect(self.deleteSuccess) self.deleteThread.errorDelete.connect(self.deleteError) self.deleteThread.start() else: InfoBar.info( "提示", "删除已取消", Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) def deleteSuccess(self): InfoBar.success( "成功", "成功删除", Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) signalBus.refreshFolderListSignal.emit() def deleteError(self, error_msg): InfoBar.error( "失败", "删除失败", Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) logger.error(f"删除文件失败:{error_msg}") def renameSelf(self): """重命名文件或文件夹""" # 使用输入对话框获取新名称 from qfluentwidgets import LineEdit, MessageBoxBase, SubtitleLabel from PyQt6.QtCore import Qt import re # 创建自定义对话框 class RenameMessageBox(MessageBoxBase): """自定义重命名对话框""" def __init__(self, parent=None, initial_name=""): super().__init__(parent) self.titleLabel = SubtitleLabel(f"{lang("请输入新名称")}:") self.lineEdit = LineEdit() self.lineEdit.setText(initial_name) # 选择文件名部分(不包括扩展名) if "." in initial_name: name_part = initial_name.rsplit(".", 1)[0] self.lineEdit.setSelection(0, len(name_part)) else: self.lineEdit.selectAll() # 将组件添加到布局中 self.viewLayout.addWidget(self.titleLabel) self.viewLayout.addWidget(self.lineEdit) # 设置对话框的最小宽度 self.widget.setMinimumWidth(400) # 设置窗口标题 self.setWindowTitle(lang("重命名")) # 创建并显示对话框 dlg = RenameMessageBox(self.window(), self.fileName) if dlg.exec(): new_name = dlg.lineEdit.text().strip() # 验证文件名 if not new_name: InfoBar.warning( lang("警告"), lang("文件名不能为空"), Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) return # 检查文件名是否包含无效字符 if re.search(r'[<>"/\\|?*]', new_name): InfoBar.warning( lang("警告"), lang("文件名包含无效字符"), Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) return # 检查文件名是否与当前目录中的其他文件重复 # 获取当前目录下的所有文件名称 current_folder = self.filePath # 从父窗口获取文件列表(假设父窗口有fileList属性) parent_window = self.window() if hasattr(parent_window, 'fileList'): # 遍历文件列表,检查是否有相同名称的文件(排除当前正在重命名的文件) for file_item in parent_window.fileList: # 跳过当前正在重命名的文件 if hasattr(file_item, 'fileName') and file_item.fileName == self.fileName: continue # 检查是否有同名文件 if hasattr(file_item, 'fileName') and file_item.fileName == new_name: InfoBar.warning( lang("警告"), lang("文件名已存在"), Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) return # 构建Cloudreve V4 API所需的正确路径格式 # 确保路径格式规范,避免Path not exist错误 # 只使用路径中的目录部分,不包含文件名 if self.filePath == "/": # 根目录情况,URI只包含根目录路径 full_path = "cloudreve://my" # 直接使用正确的双斜杠格式 else: # 子目录情况,确保只使用目录路径,不包含文件名 # 清理路径,移除协议前缀 clean_path = self.filePath if clean_path.startswith("cloudreve://my/"): clean_path = clean_path[15:] # 移除 "cloudreve://my/" elif clean_path.startswith("/"): clean_path = clean_path[1:] # 移除前导斜杠 # 构建完整URI,只包含目录路径 full_path = f"cloudreve://my/{clean_path}" # 直接使用正确的双斜杠格式 # 强制确保协议前缀格式正确,使用双斜杠 # 直接替换所有可能的单斜杠格式 full_path = full_path.replace("cloudreve:/my", "cloudreve://my") full_path = full_path.replace("cloudreve:/", "cloudreve://") # 再次确认格式完全正确 if not full_path.startswith("cloudreve://"): # 如果格式仍然不正确,重新构建整个URI path_part = full_path.replace("cloudreve:", "").lstrip("/") full_path = f"cloudreve://{path_part}" # 不再需要处理文件名重复,因为URI中只包含目录路径 # 最终清理 full_path = full_path.replace("//", "/") # 移除多余的斜杠 # 记录构建的路径用于调试 print(f"[DEBUG] 重命名路径: {full_path}") # 调用重命名线程 self.renameThread = RenameFileThread(full_path, new_name, self.fileType) self.renameThread.successRename.connect(self.renameSuccess) self.renameThread.errorRename.connect(self.renameError) self.renameThread.start() def renameSuccess(self): InfoBar.success( lang("成功"), lang("重命名成功"), Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) # 刷新文件夹列表 signalBus.refreshFolderListSignal.emit() def renameError(self, error_msg): InfoBar.error( lang("失败"), f"{lang("重命名失败")}: {error_msg}", Qt.Orientation.Horizontal, True, 1000, InfoBarPosition.TOP_RIGHT, self.window(), ) logger.error(f"重命名文件失败:{error_msg}") def contextMenuEvent(self, e): """重写上下文菜单事件,确保只有右键点击才会触发""" pass def mouseDoubleClickEvent(self, event): """重写鼠标双击事件,双击文件夹时进入文件夹""" if self.fileType == "dir": self.dirClicked() # 阻止事件继续传播,避免可能的干扰 event.accept() else: # 非文件夹时调用父类处理 super().mouseDoubleClickEvent(event) class ShareFileCard(CardWidget): def __init__(self, data, parent=None): super().__init__(parent) self._id = data["key"] self.fileName = data["source"]["name"] self.fileSize = data["source"]["size"] self.changeTime = data["create_date"] self.fileType = "dir" if data["is_dir"] else "file" self.preview = data["preview"] self.passWord = data["password"] self.remainDownloads = data["remain_downloads"] self.downloads = data["downloads"] self.score = data["score"] self.views = data["views"] self.expireTime = data["expire"] self.setFixedHeight(50) self.iconLabel = ImageLabel(self) self.fileNameLabel = StrongBodyLabel(self.fileName, self) self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self) self.viewButton = PushButton("查看", self) self.viewButton.clicked.connect(self.viewFile) self.hBoxLayout = QHBoxLayout(self) self.hBoxLayout.addWidget(self.iconLabel) self.hBoxLayout.addWidget(self.fileNameLabel) self.hBoxLayout.addWidget(self.changeTimeLabel) self.hBoxLayout.addWidget(self.viewButton) self.hBoxLayout.setStretch(0, 1) self.hBoxLayout.setStretch(1, 2) self.hBoxLayout.setStretch(2, 1) self.hBoxLayout.setContentsMargins(10, 10, 10, 10) self.hBoxLayout.setSpacing(10) self.loadIcon() self.suffix = self.fileName.split(".")[-1].lower() def loadIcon(self): icon_name = getFileIcon(self.fileType, self.fileName) self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}")) self.iconLabel.scaledToHeight(25) self.iconLabel.scaledToWidth(25) def viewFile(self): if self.fileType == "file": w = ShareFileMessageBox( self._id, self.iconLabel.pixmap(), self.suffix, self.window() ) if w.exec(): ... else: signalBus.shareFolderViewSignal.emit(self._id) class SharedFolderFileCard(CardWidget): shareFileDownloadSignal = pyqtSignal() # 共享文件下载信号 def __init__(self, key, _id, fileName, fileType, path, date, size, parent=None): super().__init__(parent) self._id = _id self.key = key self.fileName = fileName self.fileSize = size self.changeTime = date self.fileType = fileType self.filePath = path self.setFixedHeight(50) self.iconLabel = ImageLabel(self) self.fileNameLabel = StrongBodyLabel(self.fileName, self) self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self) self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self) self.hBoxLayout = QHBoxLayout(self) self.hBoxLayout.addWidget(self.iconLabel) self.hBoxLayout.addWidget(self.fileNameLabel) self.hBoxLayout.addWidget(self.fileSizeLabel) self.hBoxLayout.addWidget(self.changeTimeLabel) self.hBoxLayout.setStretch(0, 1) self.hBoxLayout.setStretch(1, 2) self.hBoxLayout.setStretch(2, 1) self.hBoxLayout.setContentsMargins(10, 10, 10, 10) self.hBoxLayout.setSpacing(10) self.loadIcon() self.suffix = self.fileName.split(".")[-1].lower() if self.fileType == "dir": # 连接左键点击信号 self.clicked.connect(self.dirClicked) def mousePressEvent(self, event): """重写鼠标点击事件,区分左右键""" if event.button() == Qt.MouseButton.RightButton: # 右键点击,显示上下文菜单 if self.fileType == "file": self.showFileContextMenu(event.globalPos()) else: self.showFolderContextMenu(event.globalPos()) else: # 左键或其他按钮点击,调用父类处理 super().mousePressEvent(event) def loadIcon(self): icon_name = getFileIcon(self.fileType, self.fileName) self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}")) self.iconLabel.scaledToHeight(25) self.iconLabel.scaledToWidth(25) def dirClicked(self): if self.filePath == "/": paths = "/" + self.fileName else: paths = f"{self.filePath}/{self.fileName}" signalBus.shareDirOpenSignal.emit(paths) def downloadFile(self): if self.fileType == "file": signalBus.addDownloadFileTask.emit( f"share.{self.suffix}", self.fileName, f"{self.filePath}/{self.fileName}.{self.key}", ) signalBus.shareFileDownloadSignal.emit() def showFileContextMenu(self, pos): """显示上下文菜单""" menu = RoundMenu(parent=self) menu.addActions( [ Action( FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile() ), ] ) menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN) def showFolderContextMenu(self, pos): """显示上下文菜单""" menu = RoundMenu(parent=self) menu.addActions( [ Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()), ] ) menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN) def mouseDoubleClickEvent(self, event): """重写鼠标双击事件,双击文件夹时进入文件夹""" if self.fileType == "dir": self.dirClicked() # 阻止事件继续传播,避免可能的干扰 event.accept() else: # 非文件夹时调用父类处理 super().mouseDoubleClickEvent(event)