init
This commit is contained in:
45
app/view/widgets/add_tag_messageBox.py
Normal file
45
app/view/widgets/add_tag_messageBox.py
Normal 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}")
|
||||
23
app/view/widgets/custom_background_messageBox.py
Normal file
23
app/view/widgets/custom_background_messageBox.py
Normal 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()
|
||||
363
app/view/widgets/custom_fluent_window.py
Normal file
363
app/view/widgets/custom_fluent_window.py
Normal 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())
|
||||
42
app/view/widgets/download_widget.py
Normal file
42
app/view/widgets/download_widget.py
Normal 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,
|
||||
)
|
||||
)
|
||||
117
app/view/widgets/login_widget.py
Normal file
117
app/view/widgets/login_widget.py
Normal 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()
|
||||
142
app/view/widgets/new_folder_messageBox.py
Normal file
142
app/view/widgets/new_folder_messageBox.py
Normal 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)
|
||||
297
app/view/widgets/ownFiled_widgets.py
Normal file
297
app/view/widgets/ownFiled_widgets.py
Normal 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)
|
||||
91
app/view/widgets/ownfile_scroll_widget.py
Normal file
91
app/view/widgets/ownfile_scroll_widget.py
Normal 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)
|
||||
162
app/view/widgets/page_flip_widget.py
Normal file
162
app/view/widgets/page_flip_widget.py
Normal 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)
|
||||
165
app/view/widgets/policy_messageBox.py
Normal file
165
app/view/widgets/policy_messageBox.py
Normal 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)
|
||||
292
app/view/widgets/preview_box.py
Normal file
292
app/view/widgets/preview_box.py
Normal 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)
|
||||
# 文本预览框会自动适应大小,无需特殊处理
|
||||
86
app/view/widgets/register_widget.py
Normal file
86
app/view/widgets/register_widget.py
Normal 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()
|
||||
147
app/view/widgets/share_file_messageBox.py
Normal file
147
app/view/widgets/share_file_messageBox.py
Normal 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"
|
||||
463
app/view/widgets/share_folder_messageBox.py
Normal file
463
app/view/widgets/share_folder_messageBox.py
Normal 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)
|
||||
94
app/view/widgets/share_search_widgets.py
Normal file
94
app/view/widgets/share_search_widgets.py
Normal 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()
|
||||
66
app/view/widgets/stacked_widget.py
Normal file
66
app/view/widgets/stacked_widget.py
Normal 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()
|
||||
38
app/view/widgets/system_trayMenu.py
Normal file
38
app/view/widgets/system_trayMenu.py
Normal 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()
|
||||
42
app/view/widgets/upload_widget.py
Normal file
42
app/view/widgets/upload_widget.py
Normal 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,
|
||||
)
|
||||
)
|
||||
41
app/view/widgets/ware_search_widgets.py
Normal file
41
app/view/widgets/ware_search_widgets.py
Normal 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()
|
||||
Reference in New Issue
Block a user