Files
leonpan-pc/app/view/widgets/share_folder_messageBox.py
2025-10-29 22:20:21 +08:00

464 lines
16 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
# 导入loguru日志库
from datetime import datetime
from loguru import logger
from PyQt6.QtCore import Qt, QThread, QUrl, pyqtSignal
from PyQt6.QtGui import QPixmap
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import (
QHBoxLayout,
QVBoxLayout,
QWidget,
)
from qfluentwidgets import Action, BodyLabel, BreadcrumbBar, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, \
MenuAnimationType, MessageBoxBase, RoundMenu, ScrollArea, setFont, SubtitleLabel
from qfluentwidgets import FluentIcon as FIF
from app.core import lang
from app.core import miaoStarsBasicApi
from app.core import signalBus
from app.view.components.file_card import SharedFolderFileCard
# 使用miaoStarsBasicApi中已经配置好的V4 API
UserSession = miaoStarsBasicApi.returnSession()
class APIWorker(QThread):
"""API 工作线程"""
finished = pyqtSignal(object, str) # 信号:传递响应数据和错误信息
def __init__(self, url_path, method="GET", data=None):
super().__init__()
self.url_path = url_path # 相对路径,如 "/share/list/{id}/path"
self.method = method
self.data = data
logger.debug(f"初始化API工作线程路径: {url_path}, 方法: {method}")
def run(self):
"""执行API请求使用miaoStarsBasicApi中已配置好的Cloudreve V4 API"""
try:
logger.info(f"开始API请求")
# 使用miaoStarsBasicApi进行请求
if self.method == "GET":
response = miaoStarsBasicApi.request(method="GET", url=self.url_path)
else:
response = miaoStarsBasicApi.request(method=self.method, url=self.url_path, json=self.data)
# Cloudreve V4 API 返回的是处理后的结果,不需要再解析响应
logger.success(f"API请求成功")
# 由于miaoStarsBasicApi.request已经处理了错误直接返回结果
if isinstance(response, dict) and "code" in response and response["code"] == 0:
self.finished.emit(response, "")
else:
error_msg = response.get("msg", "请求失败") if isinstance(response, dict) else "请求失败"
logger.error(f"API请求失败, 错误: {error_msg}")
self.finished.emit(None, error_msg)
except Exception as e:
error_msg = f"请求异常: {str(e)}"
logger.exception(f"API请求异常, 异常信息: {error_msg}")
self.finished.emit(None, error_msg)
class LinkageSwitching(ScrollArea):
"""文件卡片滚动区域组件"""
# 信号:文件卡片相关操作时发出
fileActionRequested = pyqtSignal(str, str) # (actionName, fileId)
def __init__(self, _id, paths, breadcrumbBar, parent=None):
super().__init__(parent)
self.paths = paths
self._id = _id
self.currentPath = paths
self.breadcrumbBar = breadcrumbBar
self.fileCardsDict = {} # 存储所有文件卡片
logger.debug(f"初始化文件卡片滚动区域,路径: {paths}")
self.setupUi()
def setupUi(self):
"""初始化UI"""
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)
self._onLoadDict("/")
def addFileCard(self, fileId, key, obj):
"""添加文件卡片
Args:
fileId: 文件的唯一标识符
obj: 文件数据对象
Returns:
创建的文件卡片对象
"""
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
# logger.debug(f"添加文件卡片: {fileId} - {obj.get('name', '未知')}")
fileCard = SharedFolderFileCard(
key,
obj["id"],
obj["name"],
obj["type"],
obj["path"],
obj["date"],
obj["size"],
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:
# logger.debug(f"移除文件卡片: {fileId}")
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 contextMenuEvent(self, e):
"""重写上下文菜单事件"""
logger.debug("触发上下文菜单事件")
menu = RoundMenu(parent=self)
# 添加操作
menu.addAction(
Action(FIF.SYNC, lang("刷新当前"), triggered=self._refreshFolderList)
)
menu.addSeparator()
# 显示菜单
menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
def _refreshFolderList(self):
logger.debug("刷新文件夹列表")
InfoBar.success(
"成功",
"刷新成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.parent(),
)
def _onLoadDict(self, paths):
"""加载目录数据"""
logger.info(f"加载目录数据: {paths}")
self.currentPath = paths
# 使用Cloudreve V4 API的分享列表路径
# 注意V4 API中分享列表可能使用不同的路径和参数格式
# 这里假设路径为 /share/list/{share_id}?path={path}
url_path = f"/share/list/{self._id}?path={paths}"
self.loadDataThread = APIWorker(url_path)
self.loadDataThread.finished.connect(self._dealDatas)
self.loadDataThread.start()
self.breadcrumbBar.setEnabled(False)
def _dealDatas(self, data, msg):
"""处理目录数据"""
logger.info("设置当前页策略")
if msg:
logger.error(f"加载目录数据失败: {msg}")
return
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()
self.breadcrumbBar.setEnabled(True)
for obj in self.objects:
try:
self.addFileCard(obj["id"], obj["key"], obj)
except:
self.addFileCard(obj["id"], obj["id"], obj)
class BasicInfoThread(QThread):
"""API 工作线程"""
finished = pyqtSignal(object, str) # 信号:传递响应数据和错误信息
def __init__(self, _id):
super().__init__()
self._id = _id
logger.debug(f"初始化API工作线程")
def run(self):
"""执行API请求使用miaoStarsBasicApi中已配置好的Cloudreve V4 API"""
try:
# 使用Cloudreve V4 API的URL格式
url_path = f"/share/{self._id}"
logger.info(f"开始API请求")
# 使用miaoStarsBasicApi进行请求
response = miaoStarsBasicApi.request(method="GET", url=url_path)
# 由于miaoStarsBasicApi.request已经处理了错误直接返回结果
if isinstance(response, dict) and "code" in response and response["code"] == 0:
logger.success(f"API请求成功")
self.finished.emit(response, "")
else:
error_msg = response.get("msg", "请求失败") if isinstance(response, dict) else "请求失败"
logger.error(f"API请求失败, 错误: {error_msg}")
self.finished.emit(None, error_msg)
except Exception as e:
error_msg = f"请求异常: {str(e)}"
logger.exception(f"API请求异常, 异常信息: {error_msg}")
self.finished.emit(None, error_msg)
class ShareFolderMessageBox(MessageBoxBase):
"""主文件管理界面"""
# 信号:面包屑导航项点击时发出,传递路径
breadcrumbItemClicked = pyqtSignal(str)
def __init__(self, _id, parent=None):
super().__init__(parent=parent)
self.setObjectName("ShareFolderMessageBox")
self.widget.setMinimumWidth(900)
self.widget.setMinimumHeight(600)
self.currentPath = "/"
self._id = _id
logger.debug("初始化分析文件管理")
self.folderTitleLabel = SubtitleLabel(self)
self.infomationLabel = BodyLabel(self)
self.authorAvatar = ImageLabel(":app/images/logo.png", self)
self.authorAvatar.setBorderRadius(20, 20, 20, 20)
self.authorAvatar.scaledToHeight(20)
self.authorAvatar.scaledToWidth(20)
self.authorNameLabel = BodyLabel(self)
self.breadcrumbBar = BreadcrumbBar(self)
self.basicInfoThread = BasicInfoThread(self._id)
self.basicInfoThread.finished.connect(self.handleApiResponse)
self.basicInfoThread.start()
self.networkManager = QNetworkAccessManager(self)
self.networkManager.finished.connect(self.onAvatarDownloaded)
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
logger.debug("设置分析文件管理界面UI")
# 初始化面包屑导航
self.breadcrumbBar.addItem("/", "/")
setFont(self.breadcrumbBar, 18)
self.breadcrumbBar.setSpacing(15)
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
# 初始化堆叠窗口
self.linkageSwitching = LinkageSwitching(
self._id, "/", self.breadcrumbBar, self
)
# 设置主布局
self.initLayout()
def initLayout(self):
"""初始化布局"""
self.viewLayout.setContentsMargins(10, 20, 10, 5)
self.viewLayout.addWidget(self.folderTitleLabel, 0, Qt.AlignmentFlag.AlignTop)
self.viewLayout.addWidget(self.infomationLabel, 0, Qt.AlignmentFlag.AlignTop)
self.viewLayout.addWidget(HorizontalSeparator(self))
self.authorLayout = QHBoxLayout()
self.authorLayout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.authorLayout.addWidget(self.authorAvatar, 0, Qt.AlignmentFlag.AlignLeft)
self.authorLayout.addWidget(self.authorNameLabel, 0, Qt.AlignmentFlag.AlignLeft)
self.viewLayout.addLayout(self.authorLayout)
self.viewLayout.addWidget(HorizontalSeparator(self))
# 添加所有组件到主布局
self.viewLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
self.viewLayout.addWidget(self.linkageSwitching)
def handleApiResponse(self, response_data):
response_data = response_data["data"]
self.folderTitleLabel.setText(response_data["source"]["name"])
infoLabel = f"创建时间: {self.format_date(response_data['create_date'])} | 浏览次数: {response_data['views']} | 下载次数: {response_data['downloads']}"
self.infomationLabel.setText(infoLabel)
self.authorAvatar.setText(response_data["creator"]["nick"])
self.authorNameLabel.setText(response_data["creator"]["nick"])
self.loadAvatarFromId(response_data["creator"]["key"])
def loadAvatarFromId(self, _id):
"""从网络URL加载头像"""
# 使用V4 API获取头像
url = f"/user/avatar/{_id}/l"
logger.info(f"开始从网络加载头像")
request = QNetworkRequest(QUrl(url))
self.networkManager.get(request)
def onAvatarDownloaded(self, reply):
"""处理头像下载完成"""
if reply.error() == QNetworkReply.NetworkError.NoError:
# 读取下载的数据
data = reply.readAll()
# 创建QPixmap并加载数据
pixmap = QPixmap()
if pixmap.loadFromData(data):
# 更新头像
self.authorAvatar.setImage(pixmap)
logger.info("网络头像加载成功")
else:
logger.error("头像数据格式不支持")
else:
logger.error(f"头像下载失败: {reply.errorString()}")
pixmap = QPixmap(":app/images/logo.png")
self.authorAvatar.setImage(pixmap)
logger.info("使用默认头像")
self.authorAvatar.scaledToHeight(20)
self.authorAvatar.scaledToWidth(20)
reply.deleteLater()
def format_size(self, size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def format_date(self, date_str):
"""格式化日期时间"""
try:
# 处理带小数秒的情况
if "." in date_str:
# 分割日期和小数秒部分
date_part, fractional_part = date_str.split(".", 1)
# 去除末尾的'Z'并截取前6位小数
fractional_sec = fractional_part.rstrip("Z")[:6]
# 重新组合日期字符串
normalized_date_str = f"{date_part}.{fractional_sec}Z"
date_time = datetime.strptime(
normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ"
)
else:
# 处理没有小数秒的情况
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
# 如果所有格式都失败,返回原始字符串
return date_str
return date_time.strftime("%Y-%m-%d %H:%M:%S")
def refreshCurrentDirectory(self):
"""刷新当前目录的文件卡片"""
logger.info(f"刷新当前目录: {self.currentPath}")
# 重新加载当前目录数据
self.linkageSwitching._onLoadDict(self.currentPath)
def clickChangeDir(self, name):
"""处理面包屑导航项点击事件"""
logger.info(f"面包屑导航项点击: {name}")
# 获取点击的路径
if name == "":
name = "/"
pathList = []
for item in self.breadcrumbBar.items:
if item.text == name:
pathList.append(item.text)
break
else:
pathList.append(item.text)
# 清理空路径
for i in pathList:
if i == "":
pathList.remove(i)
path = "/".join(pathList) if pathList else "/"
path = path[1:]
if path == "":
path = "/"
if path == self.currentPath:
logger.debug("路径未变化,跳过导航")
return
self.onChangeDir(path)
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接主文件管理界面信号")
# 连接搜索信号
signalBus.shareDirOpenSignal.connect(lambda x: self.onChangeDir(x))
# signalBus.refreshFolderListSignal.connect(self.refreshCurrentDirectory)
signalBus.shareFileDownloadSignal.connect(self.accept)
def onChangeDir(self, path):
"""处理目录变更"""
logger.info(f"变更目录: {path}")
self.linkageSwitching._onLoadDict(path)
if path == "":
self.currentPath = "/"
else:
self.currentPath = path
# 更新面包屑导航
display_name = path.split("/")[-1] if path != "/" else "/"
self.breadcrumbBar.addItem(display_name, display_name)