This commit is contained in:
2025-10-29 22:20:21 +08:00
commit 32b3b7b29a
111 changed files with 344425 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import pyqtSignal
from qfluentwidgets import LineEdit, MessageBoxBase, SubtitleLabel
from app.core import AddTagThread,lang
class AddTagMessageBox(MessageBoxBase):
successAddTagSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
def __init__(self, parent=None):
super().__init__(parent=parent)
self.widget.setMinimumWidth(250)
self.titleLabel = SubtitleLabel(lang("添加标签"), self)
self.nameLineEdit = LineEdit(self)
self.nameLineEdit.setPlaceholderText(lang("标签名称"))
self.expressionLineEdit = LineEdit(self)
self.expressionLineEdit.setPlaceholderText(lang("标签通配符"))
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.nameLineEdit)
self.viewLayout.addWidget(self.expressionLineEdit)
self.yesButton.setText(lang("添加"))
self.yesButton.clicked.connect(self.addTag)
self.cancelButton.setText(lang("取消"))
def addTag(self):
name = self.nameLineEdit.text()
expression = self.expressionLineEdit.text()
if not name or not expression:
return
self.addTagThread = AddTagThread(name, expression)
self.addTagThread.successSignal.connect(self.onAddTagSuccess)
self.addTagThread.errorSignal.connect(self.onAddTagError)
self.addTagThread.start()
def onAddTagSuccess(self, name, result):
self.accept()
self.successAddTagSignal.emit(name, result)
def onAddTagError(self, name, error_msg):
logger.error(f"添加标签失败: {name} - {error_msg}")

View File

@@ -0,0 +1,23 @@
# coding: utf-8
from pathlib import Path
from qfluentwidgets import HorizontalFlipView, MessageBoxBase, SubtitleLabel
class CustomBgMessageBox(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.titleLabel = SubtitleLabel(parent=self)
self.titleLabel.setText("内设壁纸")
self.viewLayout.addWidget(self.titleLabel)
self.imageChoice = HorizontalFlipView(parent=self)
self.imageChoice.setBorderRadius(8)
for i in range(0, 6):
self.imageChoice.addImage(f"app\\resource\\images\\bg{i}.png")
self.viewLayout.addWidget(self.imageChoice)
self.yesButton.setText("确定")
self.cancelButton.hide()
def returnImage(self):
return self.imageChoice.currentIndex()

View File

@@ -0,0 +1,363 @@
# coding:utf-8
import sys
from typing import Union
from PyQt6.QtCore import QRect, QSize, Qt
from PyQt6.QtGui import QColor, QIcon, QPainter, QPixmap
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets.common.animation import BackgroundAnimationWidget
from qfluentwidgets.common.config import qconfig
from qfluentwidgets.common.icon import FluentIconBase
from qfluentwidgets.common.router import qrouter
from qfluentwidgets.common.style_sheet import (
FluentStyleSheet,
isDarkTheme,
)
from qfluentwidgets.components.navigation import (
NavigationInterface,
NavigationItemPosition,
NavigationTreeWidget,
)
from qfluentwidgets.components.widgets.frameless_window import FramelessWindow
from qframelesswindow import TitleBar, TitleBarBase
from app.view.widgets.stacked_widget import StackedWidget
class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
"""Fluent window base class"""
def __init__(self, parent=None):
self._isMicaEnabled = False
self._lightBackgroundColor = QColor(240, 244, 249)
self._darkBackgroundColor = QColor(32, 32, 32)
self._backgroundPixmap = None # 存储背景图片
self._backgroundOpacity = 1.0 # 背景图片不透明度
super().__init__(parent=parent)
self.hBoxLayout = QHBoxLayout(self)
self.stackedWidget = StackedWidget(self)
self.navigationInterface = None
# initialize layout
self.hBoxLayout.setSpacing(0)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
FluentStyleSheet.FLUENT_WINDOW.apply(self.stackedWidget)
# enable mica effect on win11
self.setMicaEffectEnabled(True)
# show system title bar buttons on macOS
if sys.platform == "darwin":
self.setSystemTitleBarButtonVisible(True)
qconfig.themeChangedFinished.connect(self._onThemeChangedFinished)
def addSubInterface(
self,
interface: QWidget,
icon: Union[FluentIconBase, QIcon, str],
text: str,
position=NavigationItemPosition.TOP,
):
"""add sub interface"""
raise NotImplementedError
def removeInterface(self, interface: QWidget, isDelete=False):
"""remove sub interface
Parameters
----------
interface: QWidget
sub interface to be removed
isDelete: bool
whether to delete the sub interface
"""
raise NotImplementedError
def switchTo(self, interface: QWidget):
self.stackedWidget.setCurrentWidget(interface, popOut=False)
def _onCurrentInterfaceChanged(self, index: int):
widget = self.stackedWidget.widget(index)
self.navigationInterface.setCurrentItem(widget.objectName())
qrouter.push(self.stackedWidget, widget.objectName())
self._updateStackedBackground()
def _updateStackedBackground(self):
isTransparent = self.stackedWidget.currentWidget().property(
"isStackedTransparent"
)
if bool(self.stackedWidget.property("isTransparent")) == isTransparent:
return
self.stackedWidget.setProperty("isTransparent", isTransparent)
self.stackedWidget.setStyle(QApplication.style())
def setCustomBackgroundColor(self, light, dark):
"""set custom background color
Parameters
----------
light, dark: QColor | Qt.GlobalColor | str
background color in light/dark theme mode
"""
self._lightBackgroundColor = QColor(light)
self._darkBackgroundColor = QColor(dark)
self._updateBackgroundColor()
def setBackgroundImage(self, imagePath: str, opacity: float = 1.0):
"""设置背景图片
Parameters
----------
imagePath: str
背景图片路径
opacity: float
背景图片不透明度范围0.0-1.0
"""
self._backgroundPixmap = QPixmap(imagePath)
if self._backgroundPixmap.isNull():
print(f"无法加载背景图片: {imagePath}")
return
self._backgroundOpacity = max(0.0, min(1.0, opacity)) # 确保在0-1范围内
# 设置StackedWidget为透明以便显示背景图片
self.stackedWidget.setProperty("isTransparent", True)
self.stackedWidget.setStyle(QApplication.style())
self.update() # 触发重绘
def removeBackgroundImage(self):
"""移除背景图片"""
self._backgroundPixmap = None
# 恢复StackedWidget的默认背景
self.stackedWidget.setProperty("isTransparent", False)
self.stackedWidget.setStyle(QApplication.style())
self.update()
def _normalBackgroundColor(self):
if not self.isMicaEffectEnabled():
return (
self._darkBackgroundColor
if isDarkTheme()
else self._lightBackgroundColor
)
return QColor(0, 0, 0, 0)
def _onThemeChangedFinished(self):
if self.isMicaEffectEnabled():
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
def paintEvent(self, e):
# 创建绘制器
painter = QPainter(self)
# 如果有背景图片,先绘制背景图片
if self._backgroundPixmap and not self._backgroundPixmap.isNull():
# 设置不透明度
painter.setOpacity(self._backgroundOpacity)
# 缩放图片以适应窗口大小
scaled_pixmap = self._backgroundPixmap.scaled(
self.size(), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation
)
painter.drawPixmap(0, 0, scaled_pixmap)
# 然后调用父类的绘制方法
super().paintEvent(e)
def setMicaEffectEnabled(self, isEnabled: bool):
"""set whether the mica effect is enabled, only available on Win11"""
if sys.platform != "win32" or sys.getwindowsversion().build < 22000:
return
self._isMicaEnabled = isEnabled
if isEnabled:
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
# 启用Mica效果时移除背景图片
self.removeBackgroundImage()
else:
self.windowEffect.removeBackgroundEffect(self.winId())
self.setBackgroundColor(self._normalBackgroundColor())
def isMicaEffectEnabled(self):
return self._isMicaEnabled
def systemTitleBarRect(self, size: QSize) -> QRect:
"""Returns the system title bar rect, only works for macOS
Parameters
----------
size: QSize
original system title bar rect
"""
return QRect(
size.width() - 75, 0 if self.isFullScreen() else 9, 75, size.height()
)
def setTitleBar(self, titleBar):
super().setTitleBar(titleBar)
# hide title bar buttons on macOS
if (
sys.platform == "darwin"
and self.isSystemButtonVisible()
and isinstance(titleBar, TitleBarBase)
):
titleBar.minBtn.hide()
titleBar.maxBtn.hide()
titleBar.closeBtn.hide()
class FluentTitleBar(TitleBar):
"""Fluent title bar"""
def __init__(self, parent):
super().__init__(parent)
self.setFixedHeight(48)
self.hBoxLayout.removeWidget(self.minBtn)
self.hBoxLayout.removeWidget(self.maxBtn)
self.hBoxLayout.removeWidget(self.closeBtn)
# add window icon
self.iconLabel = QLabel(self)
self.iconLabel.setFixedSize(18, 18)
self.hBoxLayout.insertWidget(
0, self.iconLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
self.window().windowIconChanged.connect(self.setIcon)
# add title label
self.titleLabel = QLabel(self)
self.hBoxLayout.insertWidget(
1, self.titleLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
self.titleLabel.setObjectName("titleLabel")
self.window().windowTitleChanged.connect(self.setTitle)
self.vBoxLayout = QVBoxLayout()
self.buttonLayout = QHBoxLayout()
self.buttonLayout.setSpacing(0)
self.buttonLayout.setContentsMargins(0, 0, 0, 0)
self.buttonLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.buttonLayout.addWidget(self.minBtn)
self.buttonLayout.addWidget(self.maxBtn)
self.buttonLayout.addWidget(self.closeBtn)
self.vBoxLayout.addLayout(self.buttonLayout)
self.vBoxLayout.addStretch(1)
self.hBoxLayout.addLayout(self.vBoxLayout, 0)
FluentStyleSheet.FLUENT_WINDOW.apply(self)
def setTitle(self, title):
self.titleLabel.setText(title)
self.titleLabel.adjustSize()
def setIcon(self, icon):
self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18))
class CustomFluentWindow(CustomFluentWindowBase):
"""Fluent window"""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitleBar(FluentTitleBar(self))
self.navigationInterface = NavigationInterface(self, showReturnButton=True)
self.widgetLayout = QHBoxLayout()
# initialize layout
self.hBoxLayout.addWidget(self.navigationInterface)
self.hBoxLayout.addLayout(self.widgetLayout)
self.hBoxLayout.setStretchFactor(self.widgetLayout, 1)
self.widgetLayout.addWidget(self.stackedWidget)
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_)
self.titleBar.raise_()
def addSubInterface(
self,
interface: QWidget,
icon: Union[FluentIconBase, QIcon, str],
text: str,
position=NavigationItemPosition.TOP,
parent=None,
isTransparent=False,
) -> NavigationTreeWidget:
"""add sub interface, the object name of `interface` should be set already
before calling this method
Parameters
----------
interface: QWidget
the subinterface to be added
icon: FluentIconBase | QIcon | str
the icon of navigation item
text: str
the text of navigation item
position: NavigationItemPosition
the position of navigation item
parent: QWidget
the parent of navigation item
isTransparent: bool
whether to use transparent background
"""
if not interface.objectName():
raise ValueError("The object name of `interface` can't be empty string.")
if parent and not parent.objectName():
raise ValueError("The object name of `parent` can't be empty string.")
interface.setProperty("isStackedTransparent", isTransparent)
self.stackedWidget.addWidget(interface)
# add navigation item
routeKey = interface.objectName()
item = self.navigationInterface.addItem(
routeKey=routeKey,
icon=icon,
text=text,
onClick=lambda: self.switchTo(interface),
position=position,
tooltip=text,
parentRouteKey=parent.objectName() if parent else None,
)
# initialize selected item
if self.stackedWidget.count() == 1:
self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged)
self.navigationInterface.setCurrentItem(routeKey)
qrouter.setDefaultRouteKey(self.stackedWidget, routeKey)
self._updateStackedBackground()
return item
def removeInterface(self, interface, isDelete=False):
self.navigationInterface.removeWidget(interface.objectName())
self.stackedWidget.removeWidget(interface)
interface.hide()
if isDelete:
interface.deleteLater()
def resizeEvent(self, e):
self.titleBar.move(46, 0)
self.titleBar.resize(self.width() - 46, self.titleBar.height())

View File

@@ -0,0 +1,42 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea
from app.view.components.file_deal_cards import DownloadCard
class DownloadScrollWidget(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
self.__initWidget()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("DownloadScrollWidget")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
def addDownloadTask(self, suffix, fileName, _id):
self.vBoxLayout.addWidget(
DownloadCard(
suffix,
fileName,
_id,
self.scrollWidget,
)
)

View File

@@ -0,0 +1,117 @@
from loguru import logger
from PyQt6.QtCore import QRegularExpression, Qt, pyqtSignal
from PyQt6.QtGui import (
QRegularExpressionValidator,
)
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
CheckBox,
LineEdit,
PasswordLineEdit,
PushButton,
)
from app.core import CaptchaThread,cfg, qconfig
class LoginWidget(QWidget):
loginSignal = pyqtSignal(dict)
def __init__(self, parent=None):
logger.debug("初始化登录组件")
super().__init__(parent)
self.setObjectName("LoginWidget")
self.setWindowTitle("LeonPan")
self.emailLineEdit = LineEdit(self)
self.emailLineEdit.setPlaceholderText("请输入邮箱")
self.passwordLineEdit = PasswordLineEdit(self)
self.passwordLineEdit.setPlaceholderText("请输入密码")
self.rememberMeCheckBox = CheckBox("记住我", self)
self.rememberMeCheckBox.checkStateChanged.connect(
lambda: qconfig.set(cfg.rememberMe, self.rememberMeCheckBox.isChecked())
)
self.loginButton = PushButton("登录", self)
self.loginButton.setDisabled(False)
self.verificationCodeLabel = QLabel(self)
self.verificationCodeLabel.setFixedSize(120, 35)
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
self.verificationCodeLabel.mousePressEvent = (
self.refreshVerificationCode
) # 绑定点击事件
self.verificationCodeLineEdit = LineEdit(self)
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
self.verificationLayout = QHBoxLayout()
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
self.verificationLayout.addWidget(self.verificationCodeLabel)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addSpacing(10)
self.vBoxLayout.addWidget(self.emailLineEdit)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.passwordLineEdit)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addLayout(self.verificationLayout)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(
self.rememberMeCheckBox, 0, Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.loginButton)
email_regex = QRegularExpression(
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
# TODO: 内测时用的邮箱匹配
# email_regex = QRegularExpression(r"^[a-zA-Z0-9_.+-]+@miaostars\.cn$")
validator = QRegularExpressionValidator(email_regex, self)
self.emailLineEdit.setValidator(validator)
self.emailLineEdit.textChanged.connect(self.checkEmail)
self.refreshVerificationCode()
self.rememberMe()
logger.debug("登录组件初始化完成")
def rememberMe(self):
logger.debug("检查记住我选项")
if qconfig.get(cfg.rememberMe):
logger.debug("已启用记住我功能,填充保存的邮箱和密码")
self.emailLineEdit.setText(qconfig.get(cfg.email))
self.passwordLineEdit.setText(qconfig.get(cfg.activationCode))
self.rememberMeCheckBox.setChecked(True)
else:
logger.debug("已禁用记住我功能,清空保存的邮箱和密码")
self.emailLineEdit.clear()
self.passwordLineEdit.clear()
def checkEmail(self, text):
# 检查当前输入是否通过验证器
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
if state == QRegularExpressionValidator.State.Acceptable:
logger.debug("邮箱格式验证通过")
self.loginButton.setDisabled(False)
else:
self.loginButton.setDisabled(True)
def refreshVerificationCode(self, event=None):
logger.debug("刷新验证码")
self.verificationCodeLabel.setEnabled(False)
self.captchaThread = CaptchaThread()
self.captchaThread.captchaReady.connect(self._showVerificationCode)
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
self.captchaThread.start()
def _showVerificationCode(self, pixmap):
logger.debug("显示验证码")
self.verificationCodeLabel.setEnabled(True)
self.verificationCodeLabel.setPixmap(pixmap)
def _showCaptchaFailed(self, message):
logger.debug(f"验证码刷新失败:{message}")
self.verificationCodeLabel.setEnabled(True)
self.verificationCodeLineEdit.clear()

View File

@@ -0,0 +1,142 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
LineEdit,
MessageBoxBase,
SubtitleLabel,
)
from app.core import CreateFolderThread, signalBus
class NewFolderMessageBox(MessageBoxBase):
"""新建文件夹对话框"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self._setupUi()
self._connectSignals()
# 线程引用,防止被垃圾回收
self.createFolderThread = None
def _setupUi(self):
"""设置UI界面"""
self.titleLabel = SubtitleLabel("新建文件夹", self)
self.nameLineEdit = LineEdit(self)
self.nameLineEdit.setPlaceholderText("请输入文件夹名称")
self.nameLineEdit.setClearButtonEnabled(True)
# 设置对话框属性
self.widget.setMinimumWidth(400)
self.yesButton.setText("新建")
self.cancelButton.setText("取消")
# 添加组件到布局
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.nameLineEdit)
# 初始时禁用确认按钮
self.yesButton.setEnabled(False)
def _connectSignals(self):
"""连接信号槽"""
self.yesButton.clicked.connect(self._onCreateClicked)
self.nameLineEdit.textChanged.connect(self._onTextChanged)
self.nameLineEdit.returnPressed.connect(self._onReturnPressed)
def _onTextChanged(self, text):
"""文本框内容变化时的处理"""
# 检查文件夹名称是否有效
is_valid = bool(text.strip()) and not any(char in text for char in '/\\:*?"<>|')
self.yesButton.setEnabled(is_valid)
if not is_valid and text.strip():
self.nameLineEdit.setToolTip('文件夹名称不能包含 /\\:*?"<>| 等特殊字符')
else:
self.nameLineEdit.setToolTip("")
def _onReturnPressed(self):
"""回车键处理"""
if self.yesButton.isEnabled():
self._onCreateClicked()
def _onCreateClicked(self):
"""创建文件夹按钮点击处理"""
folder_name = self.nameLineEdit.text().strip()
if not folder_name:
return
# 禁用按钮防止重复点击
self._setUiEnabled(False)
self.yesButton.setText("创建中...")
# 创建并启动线程
self.createFolderThread = CreateFolderThread(folder_name)
self.createFolderThread.successSignal.connect(self._onCreateSuccess)
self.createFolderThread.errorSignal.connect(self._onCreateError)
self.createFolderThread.start()
def _setUiEnabled(self, enabled):
"""设置UI启用状态"""
self.yesButton.setEnabled(enabled)
self.cancelButton.setEnabled(enabled)
self.nameLineEdit.setEnabled(enabled)
def _onCreateSuccess(self):
"""创建成功处理"""
self._showInfoBar("success", "操作成功", "新建文件夹成功")
signalBus.refreshFolderListSignal.emit()
"""线程完成时的清理工作"""
self.yesButton.setText("新建")
self._setUiEnabled(True)
self.accept()
def _onCreateError(self, error_msg):
"""创建失败处理"""
self._showInfoBar("error", "操作失败", error_msg)
# 不关闭对话框,让用户有机会修改后重试
self.nameLineEdit.setFocus()
self.nameLineEdit.selectAll()
"""线程完成时的清理工作"""
self.yesButton.setText("新建")
self._setUiEnabled(True)
def _showInfoBar(self, type_, title, content):
"""显示信息栏"""
if type_ == "success":
InfoBar.success(
title=title,
content=content,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=2000,
parent=self.window(),
)
else:
InfoBar.error(
title=title,
content=content,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000, # 错误信息显示稍长时间
parent=self.window(),
)
def showEvent(self, event):
"""显示事件处理"""
super().showEvent(event)
self.nameLineEdit.setFocus()
def closeEvent(self, event):
"""关闭事件处理"""
# 确保线程安全退出
if self.createFolderThread and self.createFolderThread.isRunning():
self.createFolderThread.quit()
self.createFolderThread.wait(1000) # 等待1秒
super().closeEvent(event)

View File

@@ -0,0 +1,297 @@
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
QHBoxLayout,
QWidget,
)
from qfluentwidgets import (Action, InfoBar, InfoBarPosition, LineEdit, MenuAnimationType, PillPushButton,
PrimarySplitPushButton, PushButton, RoundMenu, ScrollArea)
from app.core import DeleteTagThread, userConfig, lang
from app.view.widgets.add_tag_messageBox import AddTagMessageBox
class TagsScrollArea(ScrollArea):
"""标签滚动区域组件,支持动态添加和移除标签,支持单选模式"""
# 信号:标签被点击时发出,传递标签文本
tagClicked = pyqtSignal(str, str)
TAG_TYPES = {"video": "视频", "doc": "文档", "image": "图片", "audio": "音乐"}
def __init__(self, parent=None):
super().__init__(parent)
self.tagsDict = {} # 存储所有标签按钮
self.currentCheckedTag = None # 当前选中的标签ID
self.setupUi()
logger.debug("初始化标签滚动区域组件")
def setupUi(self):
"""初始化UI"""
self.widgets = QWidget()
self.layouts = QHBoxLayout(self.widgets)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.setMaximumWidth(400)
# 设置布局属性
self.layouts.setContentsMargins(0, 0, 0, 0)
self.layouts.setSpacing(10)
# 初始化默认标签
self.initDefaultTags()
# 设置样式
self.widgets.setStyleSheet("background-color: transparent; border: none;")
self.setStyleSheet("background-color: transparent; border: none;")
# 设置滚动策略
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# 安全地获取 tags 字段,如果不存在则使用空列表
self.tags = userConfig.userData.get("data", {}).get("tags", [])
for tag in self.tags:
self.addTag(tag["id"], tag["name"])
self.tagsDict[tag["id"]] = tag["name"]
def initDefaultTags(self):
"""初始化默认标签"""
logger.debug("初始化默认标签")
for tagId, tagText in self.TAG_TYPES.items():
self.addTag(tagId, tagText)
def addTag(self, tagId, text):
"""添加一个新标签
Args:
tagId: 标签的唯一标识符
text: 标签显示的文本
Returns:
创建的标签按钮对象
"""
self.tagsDict[tagId] = text
logger.debug(f"添加新标签: {tagId} - {text}")
tagBtn = PillPushButton(text, self.widgets)
tagBtn.setObjectName(f"tag_{tagId}")
tagBtn.setCheckable(True) # 设置为可选中状态
tagBtn.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu
) # 启用自定义上下文菜单
# 连接点击信号,实现单选逻辑
tagBtn.clicked.connect(
lambda checked, tid=tagId: self.onTagClicked(tid, checked)
)
# 连接右键菜单信号
tagBtn.customContextMenuRequested.connect(
lambda pos, tid=tagId: self.onTagRightClicked(tid, pos)
)
self.layouts.addWidget(tagBtn)
return tagBtn
def onTagClicked(self, tagId, checked):
"""处理标签点击事件,实现单选逻辑"""
if checked:
# 如果当前点击的标签被选中,取消之前选中的标签
if self.currentCheckedTag and self.currentCheckedTag != tagId:
# 找到之前选中的标签并取消选中
previousTagBtn = self.findChild(
QWidget, f"tag_{self.currentCheckedTag}"
)
if previousTagBtn:
previousTagBtn.setChecked(False)
# 更新当前选中的标签
self.currentCheckedTag = tagId
if tagId in ["video", "doc", "image", "audio"]:
self.tagClicked.emit("internalTag", tagId)
else:
self.tagClicked.emit("externalTag", tagId)
logger.debug(f"选中标签: {tagId}")
else:
# 如果取消选中当前标签,清空当前选中的标签
if self.currentCheckedTag == tagId:
self.currentCheckedTag = None
logger.debug(f"取消选中标签: {tagId}")
# 发出标签点击信号
def onTagRightClicked(self, tagId, pos):
"""处理标签右键点击事件"""
logger.debug(f"标签被右键点击: {tagId}")
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
global_pos = tagBtn.mapToGlobal(pos)
if tagBtn.text() in ["视频", "文档", "图片", "音乐"]:
return
menu = RoundMenu(parent=self)
menu.addAction(Action(lang("删除"), triggered=lambda: self.deleteTag(tagId)))
menu.exec(global_pos, aniType=MenuAnimationType.DROP_DOWN)
def deleteTag(self, tagId):
self.deleteTagThread = DeleteTagThread(tagId)
self.deleteTagThread.successDeleteSignal.connect(
lambda: self._onTagDeleteError(tagId)
)
self.deleteTagThread.errorSignal.connect(self._onTagDeleteError)
self.deleteTagThread.start()
def _onTagDeleteError(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def _onTagDeleteError(self, tagId):
self.removeTag(tagId)
InfoBar.success(
"成功",
"标签删除成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def removeTag(self, tagId):
"""移除指定标签"""
if tagId in self.tagsDict:
logger.debug(f"移除标签: {tagId}")
# 如果移除的是当前选中的标签,清空选中状态
if self.currentCheckedTag == tagId:
self.currentCheckedTag = None
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
self.layouts.removeWidget(tagBtn)
tagBtn.deleteLater()
del self.tagsDict[tagId]
else:
logger.warning(f"尝试移除不存在的标签: {tagId}")
def getCheckedTag(self):
"""获取当前选中的标签ID"""
return self.currentCheckedTag
def setCheckedTag(self, tagId):
"""设置指定标签为选中状态"""
if tagId in self.tagsDict:
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
tagBtn.setChecked(True)
# onTagClicked 方法会自动处理单选逻辑
else:
logger.warning(f"尝试选中不存在的标签: {tagId}")
def clearChecked(self):
"""清除所有选中状态"""
if self.currentCheckedTag:
tagBtn = self.findChild(QWidget, f"tag_{self.currentCheckedTag}")
if tagBtn:
tagBtn.setChecked(False)
self.currentCheckedTag = None
class TagWidget(QWidget):
"""标签管理组件"""
def __init__(self, parent=None):
super().__init__(parent)
self.tagScrollArea = TagsScrollArea(self)
self.addPushButton = PushButton(lang("添加标签"), self)
logger.debug("初始化标签管理组件")
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.tagScrollArea)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(self.addPushButton, Qt.AlignmentFlag.AlignRight)
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接标签管理组件信号")
self.addPushButton.clicked.connect(self.addTag)
def addTag(self):
w = AddTagMessageBox(self.window())
w.successAddTagSignal.connect(self.onAddTagSuccess)
if w.exec():
...
def onAddTagSuccess(self, name, result):
"""处理标签添加成功事件"""
InfoBar.success(
"成功",
"标签添加成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.tagScrollArea.addTag(result["data"], name)
class SearchWidget(QWidget):
"""搜索组件"""
# 信号:搜索请求时发出,传递搜索关键词
searchRequested = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.searchLineEdit = LineEdit(self)
self.searchButton = PrimarySplitPushButton(lang("仓内搜索"), self)
self.menu = RoundMenu(parent=self)
self.menu.addAction(
Action(
lang("仓内搜索"),
triggered=lambda: self.changeButtonText(lang("仓内搜索")),
)
)
self.menu.addAction(
Action(
lang("站内搜索"),
triggered=lambda: self.changeButtonText(lang("站内搜索")),
)
)
self.searchButton.setFlyout(self.menu)
logger.debug("初始化搜索组件")
self.setupUi()
def changeButtonText(self, text):
self.searchButton.setText(text)
def setupUi(self):
"""初始化UI"""
self.searchLineEdit.setPlaceholderText(lang("搜索文件"))
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setContentsMargins(0, 24, 0, 0)
self.hBoxLayout.addWidget(self.searchLineEdit)
self.hBoxLayout.addWidget(self.searchButton)

View File

@@ -0,0 +1,91 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import (
BreadcrumbBar,
setFont,
)
from app.view.components.linkage_switching import OwnFileLinkageSwitching
class OwnFileScrollWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentPath = "/"
self.breadcrumbBar = BreadcrumbBar(self)
self.ownFileLinkageSwitching = OwnFileLinkageSwitching("/", self)
self.__initWidget()
def __initWidget(self):
self.setObjectName("OwnFileScrollWidget")
self.breadcrumbBar.addItem("/", "/")
setFont(self.breadcrumbBar, 18)
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
self.breadcrumbBar.setSpacing(15)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setSpacing(5)
self.vBoxLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addWidget(self.ownFileLinkageSwitching)
def loadDict(self, paths):
self.ownFileLinkageSwitching.loadDict(paths)
def onChangeDir(self, path):
"""处理目录变更"""
logger.info(f"变更目录: {path}")
if path == "":
self.currentPath = "/"
else:
self.currentPath = path
self.ownFileLinkageSwitching.loadDict(path)
# 更新面包屑导航
displayName = path.split("/")[-1] if path != "/" else "/"
self.breadcrumbBar.addItem(displayName, displayName)
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 refreshCurrentDirectory(self):
"""刷新当前目录的文件卡片"""
logger.info(f"刷新当前目录: {self.currentPath}")
self.loadDict(self.currentPath)

View File

@@ -0,0 +1,162 @@
# coding: utf-8
# flip bookmark
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QIntValidator
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, LineEdit, PushButton, ToolButton)
class PageFlipWidget(CardWidget):
# 定义页码变化信号
pageChangeSignal = pyqtSignal(int)
"""
page : 总页码,默认为10
currentPage : 当前页码
numberButtonList : 页码按钮列表
currentButtonNumbet : 当前显示的按钮数字列表
"""
def __init__(self, parent=None, page=10):
super().__init__(parent)
self.page = page
self.currentPage = 1
self.numberButtonList = []
self.currentButtonNumber = []
# 定义翻页按钮
self.leftPageButton = ToolButton(self)
self.rightPageButton = ToolButton(self)
# 定义跳转页面组件
self.pageLineEdit = LineEdit(self)
self.allPageLabel = BodyLabel(self)
self.turnPageButton = PushButton(self)
self.__initWidget()
# 动态设置按钮
self._addButton()
def __initWidget(self):
# 组件设置
self.leftPageButton.setIcon(FluentIcon.PAGE_LEFT)
self.leftPageButton.setFixedSize(40, 40)
self.leftPageButton.clicked.connect(self.backPage)
self.rightPageButton.setIcon(FluentIcon.PAGE_RIGHT)
self.rightPageButton.setFixedSize(40, 40)
self.rightPageButton.clicked.connect(self.forwardPage)
self.pageLineEdit.setText("1")
self.pageLineEdit.setValidator(QIntValidator())
self.pageLineEdit.editingFinished.connect(self._validator)
self.pageLineEdit.setFixedWidth(45)
self.allPageLabel.setText(f"/{self.page}")
self.turnPageButton.setText(self.tr("jump"))
self.turnPageButton.setFixedWidth(60)
self.turnPageButton.clicked.connect(self.turnPage)
self.__initLayout()
def __initLayout(self):
# 布局设置
self.layouts = QHBoxLayout(self)
self.layouts.addWidget(self.leftPageButton)
self.layouts.addWidget(self.rightPageButton)
self.layouts.addSpacing(30)
self.layouts.addWidget(self.pageLineEdit)
self.layouts.addWidget(self.allPageLabel)
self.layouts.addWidget(self.turnPageButton)
# 向后翻页
def forwardPage(self):
if self.page > 5:
if int(self.numberButtonList[-1].text()) < self.page:
for i in self.numberButtonList:
i.setText(str(int(i.text()) + 1))
for i in range(len(self.currentButtonNumber)):
self.currentButtonNumber[i] += 1
if self.currentPage < self.page:
self.currentPage += 1
self.pageLineEdit.setText(str(self.currentPage))
self.pageChangeSignal.emit(self.currentPage)
# 向前翻页
def backPage(self):
if int(self.numberButtonList[0].text()) > 1:
for i in self.numberButtonList:
i.setText(str(int(i.text()) - 1))
for i in range(len(self.currentButtonNumber)):
self.currentButtonNumber[i] -= 1
if self.currentPage > 1:
self.currentPage -= 1
self.pageLineEdit.setText(str(self.currentPage))
self.pageChangeSignal.emit(self.currentPage)
# 跳转页面
def turnPage(self, page: int):
"""
page : 目标跳转页
"""
page = int(self.pageLineEdit.text())
numberList = [1, 2, 3, 4, 5]
if page not in numberList:
while True:
numberList = [x + 1 for x in numberList]
if page in numberList and max(numberList) <= self.page:
break
self.currentButtonNumber = numberList
for i in self.numberButtonList:
i.setText(str(self.currentButtonNumber[self.numberButtonList.index(i)]))
self.pageChangeSignal.emit(page)
self.currentPage = page
# 输入框判断器,规定只可输入数字,并且数字不能超过规定范围
def _validator(self):
page = int(self.pageLineEdit.text())
if page <= 0:
page = 1
elif page > self.page:
page = self.page
self.pageLineEdit.setText(str(page))
# 动态添加按钮
def _addButton(self):
if self.page >= 5:
for i in range(1, 6):
numberButton = PushButton(str(i), self)
numberButton.setFixedSize(40, 40)
numberButton.clicked.connect(self._pageChanged)
self.layouts.insertWidget(i, numberButton)
self.numberButtonList.append(numberButton)
self.currentButtonNumber = [1, 2, 3, 4, 5]
else:
for i in range(1, self.page + 1):
numberButton = PushButton(str(i), self)
numberButton.setFixedSize(40, 40)
numberButton.clicked.connect(self._pageChanged)
self.layouts.insertWidget(i, numberButton)
self.numberButtonList.append(numberButton)
self.currentButtonNumber.append(i)
# 页面翻页时发出信号
def _pageChanged(self, checked):
sender = self.sender()
if sender:
button_text = sender.text()
self.pageChangeSignal.emit(int(button_text))
self.pageLineEdit.setText(button_text)
self.currentPage = int(button_text)

View File

@@ -0,0 +1,165 @@
# coding: utf-8
import logging
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QListWidgetItem
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
ListWidget,
MessageBoxBase,
SubtitleLabel,
)
from app.core import ChangePolicyThread, GetPoliciesThread, policyConfig, signalBus
class PolicyChooseMessageBox(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentPath = "/"
self.policyDict = {}
self.isLoading = True
self.originalTitle = "选择存储策略"
self.setClosableOnMaskClicked(True)
self.setupUI()
# 开始获取策略列表
self.getPoliciesThread = GetPoliciesThread()
self.getPoliciesThread.successGetSignal.connect(self.refreshPolicyList)
self.getPoliciesThread.errorSignal.connect(self._handleGetPoliciesError)
self.getPoliciesThread.start()
# 初始化更改策略线程(但不启动)
self.changePolicyThread = None
def setupUI(self):
"""设置UI界面"""
self.titleLabel = SubtitleLabel(self.originalTitle, self)
self.policyListWidget = ListWidget(self)
# 添加加载提示
self.loadingItem = QListWidgetItem("正在加载策略列表...")
self.policyListWidget.addItem(self.loadingItem)
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.policyListWidget)
# 隐藏确定取消按钮组
self.buttonGroup.hide()
def connectSignals(self):
"""连接信号与槽"""
self.policyListWidget.currentTextChanged.connect(self.onPolicyChanged)
self.policyListWidget.itemClicked.connect(self.selfClicked)
def selfClicked(self, listWidget: QListWidgetItem):
if listWidget.text() == policyConfig.returnPolicy()["name"]:
QTimer.singleShot(100, self.accept)
def onPolicyChanged(self, text):
"""处理策略更改"""
if not text or self.isLoading or text == "正在加载策略列表...":
return
policy_id = self.policyDict.get(text)
if not policy_id:
return
# 如果已经有更改线程在运行,先停止它
if self.changePolicyThread and self.changePolicyThread.isRunning():
self.changePolicyThread.quit()
self.changePolicyThread.wait()
# 创建并启动新的更改线程
self.changePolicyThread = ChangePolicyThread(self.currentPath, policy_id)
self.changePolicyThread.successChangedSignal.connect(
self._handlePolicyChangeSuccess
)
self.changePolicyThread.errorSignal.connect(self._handlePolicyChangeError)
self.changePolicyThread.start()
# 更新UI状态 - 只更改标题
self._setLoadingState(True, f"正在切换到策略: {text}")
def refreshPolicyList(self, policiesList):
"""刷新策略列表"""
self.isLoading = False
self.policyListWidget.clear()
self.policyDict.clear()
currentPolicy = policyConfig.returnPolicy()
currentIndex = 0
for i, policy in enumerate(policiesList):
self.policyListWidget.addItem(QListWidgetItem(policy["name"]))
self.policyDict[policy["name"]] = policy["id"]
if policy["id"] == currentPolicy["id"]:
currentIndex = i
# 设置当前选中项
if self.policyListWidget.count() > 0:
self.policyListWidget.setCurrentRow(currentIndex)
self.currentPath = policyConfig.returnCurrentPath()
self.connectSignals()
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)
def _handleGetPoliciesError(self, error_msg):
"""处理获取策略列表错误"""
self.policyListWidget.clear()
errorItem = QListWidgetItem(f"加载失败: {error_msg}")
self.policyListWidget.addItem(errorItem)
self.isLoading = False
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)
def _handlePolicyChangeSuccess(self):
"""处理策略更改成功"""
self._setLoadingState(False)
# 显示成功提示
if self.parent():
InfoBar.success(
title="操作成功",
content="存储策略已成功更改",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=2000,
parent=self.window(),
)
signalBus.refreshFolderListSignal.emit()
QTimer.singleShot(1000, self.accept)
def _handlePolicyChangeError(self, error_msg):
"""处理策略更改错误"""
self._setLoadingState(False)
# 显示错误提示
if self.parent():
InfoBar.error(
title="操作失败",
content=f"更改策略时出错: {error_msg}",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000,
parent=self.window(),
)
QTimer.singleShot(1000, self.reject)
def _setLoadingState(self, loading, message=None):
"""设置加载状态"""
if loading:
self.policyListWidget.setEnabled(False)
if message:
logging.info(message)
else:
self.policyListWidget.setEnabled(True)
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)

View File

@@ -0,0 +1,292 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
IndeterminateProgressBar,
MessageBoxBase,
PlainTextEdit,
PushButton,
)
from app.core import (ImageLoaderThread, TextLoaderThread, UpdateFileContentThread)
from app.core.services.text_speech import LocalSpeechController
from app.view.components.empty_card import EmptyCard
# 图片预览类
def createThumbnail(pixmap, max_size=200):
"""创建快速缩略图"""
if pixmap.isNull():
return pixmap
# 使用快速缩放算法
return pixmap.scaled(
max_size,
max_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.FastTransformation,
)
class OptimizedPreviewBox(MessageBoxBase):
def __init__(self, parent=None, url=None):
super().__init__(parent=parent)
self.widget.setMinimumSize(500, 500)
self.original_pixmap = None
self.current_scale = 1.0
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 图片显示标签
self.previewLabel = ImageLabel(self)
self.previewLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter)
# 使用优化的图片加载线程
self.imageLoaderThread = ImageLoaderThread(url)
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
self.imageLoaderThread.errorOccurred.connect(self.handleError)
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
# 延迟启动加载避免阻塞UI初始化
from PyQt6.QtCore import QTimer
QTimer.singleShot(100, self.startLoading)
def startLoading(self):
"""开始加载图片"""
self.imageLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
self.loadingCard.setText(f"正在加载图片... {progress}%")
def setPreviewImg(self, img: QPixmap):
"""设置预览图片"""
self.loadingCard.hide()
self.original_pixmap = img
# 立即显示缩略图
thumbnail = createThumbnail(img)
self.previewLabel.setPixmap(thumbnail)
# 然后异步加载高质量版本
self.adjustImageSize()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
if self.original_pixmap and not self.original_pixmap.isNull():
# 使用定时器延迟调整,避免频繁调整
from PyQt6.QtCore import QTimer
QTimer.singleShot(50, self.adjustImageSize)
def adjustImageSize(self):
"""根据窗口大小动态调整图片尺寸"""
if not self.original_pixmap or self.original_pixmap.isNull():
return
# 获取可用显示区域大小
margin = 80
available_width = self.width() - margin * 2
available_height = self.height() - margin * 2
# 获取原始图片尺寸
original_width = self.original_pixmap.width()
original_height = self.original_pixmap.height()
# 计算缩放比例
width_ratio = available_width / original_width
height_ratio = available_height / original_height
scale_ratio = min(width_ratio, height_ratio, 1.0)
# 只在需要时重新缩放
if abs(scale_ratio - self.current_scale) > 0.05: # 变化超过5%才重新缩放
self.current_scale = scale_ratio
new_width = int(original_width * scale_ratio)
new_height = int(original_height * scale_ratio)
# 使用平滑缩放
scaled_pixmap = self.original_pixmap.scaled(
new_width,
new_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self.previewLabel.setPixmap(scaled_pixmap)
def handleError(self, msg):
"""处理加载错误"""
self.loadingCard.error()
self.previewLabel.hide()
# 文本文档预览类
class PreviewTextBox(MessageBoxBase):
"""文本预览对话框"""
def __init__(self, parent=None, url=None, _id=None):
super().__init__(parent=parent)
self.updateTxtThread = None
self.widget.setMinimumSize(600, 400)
self._id = _id
self.isChanged = False
self.speech_controller = LocalSpeechController(self)
self.textSpeakButton = PushButton("朗读文本", self)
self.textSpeakButton.hide()
self.isSpeaking = False
self.textSpeakButton.clicked.connect(self.playTextSpeech)
self.viewLayout.addWidget(
self.textSpeakButton,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
# 创建文本编辑框
self.textEdit = PlainTextEdit(self)
self.textEdit.hide()
self.textEdit.setLineWrapMode(PlainTextEdit.LineWrapMode.NoWrap) # 不自动换行
# 设置等宽字体,便于阅读代码或日志
from PyQt6.QtGui import QFont
font = QFont("微软雅黑", 10) # 等宽字体
self.textEdit.setFont(font)
self.viewLayout.addWidget(self.textEdit)
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 使用文本加载线程
self.textLoaderThread = TextLoaderThread(url)
self.textLoaderThread.textLoaded.connect(self.setTextContent)
self.textLoaderThread.errorOccurred.connect(self.handleError)
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
self.yesButton.hide()
# 创建保存按钮
self.saveButton = PushButton("保存修改", self)
# 创建进度条
self.saveProgressBar = IndeterminateProgressBar(self)
self.saveProgressBar.setFixedHeight(4)
self.saveProgressBar.hide()
# 添加按钮和进度条到布局
self.buttonLayout.insertWidget(
0, self.saveButton, 1, Qt.AlignmentFlag.AlignVCenter
)
self.buttonLayout.insertWidget(
1, self.saveProgressBar, 1, Qt.AlignmentFlag.AlignVCenter
)
self.saveButton.setEnabled(False)
self.saveButton.clicked.connect(self.saveText)
self.cancelButton.setText("返回")
# 延迟启动加载避免阻塞UI初始化
QTimer.singleShot(100, self.startLoading)
def saveText(self):
logger.info("保存用户修改")
# 显示进度条并禁用按钮
self.saveProgressBar.show()
self.saveButton.setEnabled(False)
self.saveTextThread = UpdateFileContentThread(
self._id,
self.textEdit.toPlainText(),
)
self.saveTextThread.successUpdated.connect(self._successSave)
self.saveTextThread.errorUpdated.connect(self._errorSave)
self.saveTextThread.start()
def _successSave(self):
InfoBar.success(
"成功",
"修改保存成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条
self.saveProgressBar.hide()
QTimer.singleShot(700, self.accept)
def _errorSave(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条并重新启用按钮
self.saveProgressBar.hide()
self.saveButton.setEnabled(True)
def playTextSpeech(self):
"""播放文本语音"""
if not self.isSpeaking:
text = self.textEdit.toPlainText()
if text and len(text.strip()) > 0:
self.speech_controller.play_text(text)
self.isSpeaking = True
self.textSpeakButton.setText("暂停朗读")
else:
self.speech_controller.stop_playback()
self.isSpeaking = False
self.textSpeakButton.setText("朗读文本")
def startLoading(self):
"""开始加载文本"""
self.textLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
self.loadingCard.setText(f"正在加载文本... {progress}%")
def setTextContent(self, content):
"""设置文本内容"""
self.loadingCard.hide()
self.textEdit.show()
self.textSpeakButton.show()
self.saveButton.setEnabled(True)
# 限制显示的内容长度,避免性能问题
max_display_length = 100000 # 最多显示10万个字符
if len(content) > max_display_length:
content = (
content[:max_display_length]
+ f"\n\n... (内容过长,已截断前{max_display_length}个字符,完整内容请下载文件查看)"
)
self.textEdit.setPlainText(content)
def handleError(self, error_msg):
"""处理加载错误"""
self.loadingCard.error()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
# 文本预览框会自动适应大小,无需特殊处理

View File

@@ -0,0 +1,86 @@
from loguru import logger
from PyQt6.QtCore import QRegularExpression, Qt
from PyQt6.QtGui import (
QRegularExpressionValidator,
)
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
LineEdit,
PasswordLineEdit,
PrimaryPushButton,
)
from app.core import CaptchaThread
class RegisterWidget(QWidget):
def __init__(self, parent=None):
logger.debug("初始化注册组件")
super().__init__(parent)
self.setObjectName("RegisterWidget")
self.emailLineEdit = LineEdit(self)
self.emailLineEdit.setPlaceholderText("请输入注册邮箱")
self.passwordLineEdit = PasswordLineEdit(self)
self.passwordLineEdit.setPlaceholderText("请输入密码")
self.confirmPasswordLineEdit = PasswordLineEdit(self)
self.confirmPasswordLineEdit.setPlaceholderText("请确认您的密码")
self.verificationCodeLabel = QLabel(self)
self.verificationCodeLabel.setFixedSize(120, 35)
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
self.verificationCodeLabel.mousePressEvent = (
self.refreshVerificationCode
) # 绑定点击事件
self.verificationCodeLineEdit = LineEdit(self)
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
self.verificationLayout = QHBoxLayout()
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
self.verificationLayout.addWidget(self.verificationCodeLabel)
self.registerButton = PrimaryPushButton("注册", self)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.setSpacing(15)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.emailLineEdit)
self.vBoxLayout.addWidget(self.passwordLineEdit)
self.vBoxLayout.addWidget(self.confirmPasswordLineEdit)
self.vBoxLayout.addLayout(self.verificationLayout)
self.vBoxLayout.addWidget(self.registerButton)
email_regex = QRegularExpression(
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
validator = QRegularExpressionValidator(email_regex, self)
self.emailLineEdit.setValidator(validator)
self.emailLineEdit.textChanged.connect(self.checkeEmail)
logger.debug("注册组件初始化完成")
self.refreshVerificationCode()
def checkeEmail(self, text):
# 检查当前输入是否通过验证器
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
if state == QRegularExpressionValidator.Acceptable:
logger.debug("注册邮箱格式验证通过")
self.registerButton.setDisabled(False)
else:
self.registerButton.setDisabled(True)
def refreshVerificationCode(self, event=None):
logger.debug("刷新验证码")
self.captchaThread = CaptchaThread()
self.captchaThread.captchaReady.connect(self._showVerificationCode)
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
self.captchaThread.start()
def _showVerificationCode(self, pixmap):
logger.debug("显示验证码")
self.verificationCodeLabel.setPixmap(pixmap)
def _showCaptchaFailed(self, message):
logger.debug(f"验证码刷新失败:{message}")
self.verificationCodeLineEdit.clear()

View File

@@ -0,0 +1,147 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer, QUrl
from PyQt6.QtGui import QPixmap
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import (BodyLabel, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, MessageBoxBase,
SubtitleLabel)
from app.core import formatDate, formatSize, GetShareFileInfoThread, signalBus
class ShareFileMessageBox(MessageBoxBase):
def __init__(self, _id, fileIcon=None, suffix="", parent=None):
super().__init__(parent=parent)
self.widget.setFixedWidth(350)
self.suffix = suffix
self._id = _id
self.fileTypeImageLabel = ImageLabel(parent=self)
self.fileTypeImageLabel.setImage(fileIcon)
self.fileTypeImageLabel.scaledToHeight(60)
self.fileTypeImageLabel.scaledToWidth(60)
self.fileNameLabel = SubtitleLabel(parent=self)
self.fileSizeLabel = BodyLabel(parent=self)
self.fileInformationLabel = BodyLabel(parent=self)
self.fileInformationLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.userImageLabel = ImageLabel(":app/images/logo.png", parent=self)
self.userImageLabel.setBorderRadius(20, 20, 20, 20)
self.userImageLabel.setFixedSize(30, 30)
self.userImageLabel.scaledToHeight(30)
self.userImageLabel.scaledToWidth(30)
self.userNameLabel = SubtitleLabel(parent=self)
self.userLayout = QHBoxLayout()
self.userLayout.addWidget(self.userImageLabel)
self.userLayout.addWidget(self.userNameLabel)
self.viewLayout.addWidget(
self.fileTypeImageLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileSizeLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileInformationLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(HorizontalSeparator(parent=self))
self.viewLayout.addLayout(self.userLayout)
self.yesButton.setText("下载")
self.yesButton.clicked.connect(self.downloadFile)
self.cancelButton.setText("取消")
self.apiWorker = GetShareFileInfoThread(_id)
self.apiWorker.shareFileInfoSignal.connect(self.handleApiResponse)
self.apiWorker.errorSignal.connect(self.handleError)
self.apiWorker.start()
self.networkManager = QNetworkAccessManager(self)
self.networkManager.finished.connect(self.onAvatarDownloaded)
def downloadFile(self):
signalBus.addDownloadFileTask.emit(
f"share.{self.suffix}",
self.fileNameLabel.text(),
f"undefined/undefined.{self._id}",
)
self.accept()
def handleApiResponse(self, response_data):
response_data = response_data["data"]
self.fileNameLabel.setText(response_data["source"]["name"])
self.fileSizeLabel.setText(
f"大小: {formatSize(response_data['source']['size'])}"
)
infoLabel = f"创建时间: {formatDate(response_data['create_date'])}\n浏览次数: {response_data['views']}\n下载次数: {response_data['downloads']}"
self.fileInformationLabel.setText(infoLabel)
self.userNameLabel.setText(response_data["creator"]["nick"])
self.loadAvatarFromId(response_data["creator"]["key"])
def handleError(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
QTimer.singleShot(1000, self.accept)
def loadAvatarFromId(self, _id):
"""从网络URL加载头像"""
# 使用V4 API获取头像 - 假设格式变为/api/v4/user/avatar/{_id}/l
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.userImageLabel.setImage(pixmap)
logger.info("网络头像加载成功")
else:
logger.error("头像数据格式不支持")
else:
logger.error(f"头像下载失败: {reply.errorString()}")
pixmap = QPixmap(":app/images/logo.png")
self.userImageLabel.setImage(pixmap)
logger.info("使用默认头像")
self.userImageLabel.scaledToHeight(30)
self.userImageLabel.scaledToWidth(30)
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"

View File

@@ -0,0 +1,463 @@
# 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)

View File

@@ -0,0 +1,94 @@
# coding: utf-8
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import ComboBox, PushButton
from qfluentwidgets import FluentIcon as FIF
from app.view.components.linkage_switching import ShareLinkageSwitching
from app.view.widgets.page_flip_widget import PageFlipWidget
#
class ShareSearchScrollWidget(QWidget):
returnSignal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentKeyword = ""
self.currentPage = 1
self.vBoxLayout = QVBoxLayout(self)
self.topLayout = QHBoxLayout()
self.returnButton = PushButton(
FIF.RETURN,
"返回",
self,
)
self.searchMethod = ComboBox(self)
self.searchMethod.addItems(["创建时间", "下载次数", "浏览次数"])
self.searchMethod.currentTextChanged.connect(
lambda: self.shareSearch(self.currentKeyword, self.currentPage)
)
self.sortMethod = ComboBox(self)
self.sortMethod.addItems(["从大到小", "从小到大"])
self.sortMethod.currentTextChanged.connect(
lambda: self.shareSearch(self.currentKeyword, self.currentPage)
)
self.returnButton.clicked.connect(self.clear)
self.searchScrolledArea = ShareLinkageSwitching(self)
self.searchScrolledArea.totalItemsSignal.connect(self.updatePageFlip)
self.pageFlipWidget = None
self.topLayout.addWidget(self.returnButton, 0, Qt.AlignmentFlag.AlignLeft)
self.topLayout.addWidget(self.searchMethod, 1, Qt.AlignmentFlag.AlignRight)
self.topLayout.addWidget(self.sortMethod, 0, Qt.AlignmentFlag.AlignRight)
self.vBoxLayout.addLayout(self.topLayout)
self.vBoxLayout.addWidget(self.searchScrolledArea)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
QTimer.singleShot(1000, lambda: self.shareSearch("A", 1))
def updatePageFlip(self, total):
if not self.pageFlipWidget:
pages = total // 18 if total % 18 == 0 else total // 18 + 1
self.pageFlipWidget = PageFlipWidget(self, pages)
self.vBoxLayout.addWidget(
self.pageFlipWidget,
0,
Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
self.pageFlipWidget.pageChangeSignal.connect(
lambda page: self.shareSearch(self.currentKeyword, page)
)
def shareSearch(self, keyword, page):
if self.currentKeyword != keyword:
self.currentKeyword = keyword
if self.currentPage != page:
self.currentPage = page
orderByDict = {
"创建时间": "created_at",
"下载次数": "downloads",
"浏览次数": "views",
}
sortDict = {
"从大到小": "DESC",
"从小到大": "ASC",
}
self.searchScrolledArea.search(
keyword,
orderByDict[self.searchMethod.currentText()],
sortDict[self.sortMethod.currentText()],
page,
)
def clear(self):
self.searchScrolledArea.clearFileCards()
self.returnSignal.emit()

View File

@@ -0,0 +1,66 @@
# coding:utf-8
from PyQt6.QtCore import Qt, pyqtSignal, QEasingCurve
from PyQt6.QtWidgets import QFrame, QHBoxLayout, QAbstractScrollArea
from qfluentwidgets.components.widgets.stacked_widget import PopUpAniStackedWidget
class StackedWidget(QFrame):
""" Stacked widget """
currentChanged = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.hBoxLayout = QHBoxLayout(self)
self.view = PopUpAniStackedWidget(self)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
self.hBoxLayout.addWidget(self.view)
self.view.currentChanged.connect(self.currentChanged)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
def isAnimationEnabled(self) -> bool:
return self.view.isAnimationEnabled
def setAnimationEnabled(self, isEnabled: bool):
"""set whether the pop animation is enabled"""
self.view.setAnimationEnabled(isEnabled)
def addWidget(self, widget):
""" add widget to view """
self.view.addWidget(widget)
def removeWidget(self, widget):
""" remove widget from view """
self.view.removeWidget(widget)
def widget(self, index: int):
return self.view.widget(index)
def setCurrentWidget(self, widget, popOut=True):
if isinstance(widget, QAbstractScrollArea):
widget.verticalScrollBar().setValue(0)
if not popOut:
self.view.setCurrentWidget(widget, duration=300)
else:
self.view.setCurrentWidget(
widget, True, False, 200, QEasingCurve.Type.InQuad)
def setCurrentIndex(self, index, popOut=True):
self.setCurrentWidget(self.view.widget(index), popOut)
def currentIndex(self):
return self.view.currentIndex()
def currentWidget(self):
return self.view.currentWidget()
def indexOf(self, widget):
return self.view.indexOf(widget)
def count(self):
return self.view.count()

View File

@@ -0,0 +1,38 @@
# encoding:utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QSystemTrayIcon
from qfluentwidgets import Action, InfoBar, InfoBarPosition, MessageBox, SystemTrayMenu
class SystemTrayIcon(QSystemTrayIcon):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setIcon(parent.windowIcon())
self.setToolTip("-六棱光界-")
self.menu = SystemTrayMenu(parent=parent)
self.menu.addActions(
[
Action("🙂 显示界面", triggered=self.showSofware),
Action("🙃 退出软件", triggered=self.exitSoftware),
]
)
self.setContextMenu(self.menu)
def exitSoftware(self):
self.parent().window().showNormal()
InfoBar.info("提示","你正在进行退出操作,请返回软件",Qt.Orientation.Horizontal,True,5000,InfoBarPosition.BOTTOM_RIGHT,InfoBar.desktopView())
w = MessageBox(
title="提示",
content="确定退出软件吗?正在进行的任务将会取消",
parent=self.parent().window(),
)
w.yesButton.setText("确定退出")
w.cancelButton.setText("算了我想想")
if w.exec():
self.parent().window().close()
def showSofware(self):
self.parent().window().showNormal()

View File

@@ -0,0 +1,42 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea
from app.view.components.file_deal_cards import UploadCard
class UploadScrollWidget(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
self.__initWidget()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.setViewportMargins(0, 100, 0, 20)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("UploadScrollWidget")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
def addUploadTask(self, filePath):
self.vBoxLayout.addWidget(
UploadCard(
"file",
filePath,
self.scrollWidget,
)
)

View File

@@ -0,0 +1,41 @@
# coding: utf-8
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
QVBoxLayout,
QWidget,
)
from qfluentwidgets import FluentIcon as FIF
from qfluentwidgets import (
PushButton,
)
from app.view.components.linkage_switching import SearchLinkageSwitching
class WareSearchScrollWidget(QWidget):
returnSignal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.vBoxLayout = QVBoxLayout(self)
self.returnButton = PushButton(
FIF.RETURN,
"返回",
self,
)
self.returnButton.clicked.connect(self.clear)
self.searchScrolledArea = SearchLinkageSwitching(self)
self.vBoxLayout.addWidget(
self.returnButton, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
)
self.vBoxLayout.addWidget(self.searchScrolledArea)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
def wareSearch(self, searchType, searchContent):
self.searchScrolledArea.search(searchType, searchContent)
def clear(self):
self.searchScrolledArea.clearFileCards()
self.returnSignal.emit()