Files
leonpan-pc/app/view/components/file_card.py
2025-11-02 19:17:20 +08:00

601 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)