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,300 @@
# coding: utf-8
import os
import sys
import requests
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtWidgets import (
QLabel,
QMessageBox,
QSizePolicy,
QSpacerItem,
QVBoxLayout,
QWidget,
)
from qfluentwidgets import (ComboBoxSettingCard, FluentIcon, ImageLabel, MessageBox, PrimaryPushSettingCard,
SettingCardGroup, SwitchSettingCard)
from app.core import cfg, lang, qconfig, signalBus
from app.core.utils.version import version
class AppInfoInterface(QWidget):
"""
APP信息页面
包含语言切换功能
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("appInfoInterface")
self.initUI()
self.connectSignalToSlot()
# 移除初始化时的自动检查,改为由登录成功信号触发
def initUI(self):
# 创建主布局
mainLayout = QVBoxLayout(self)
mainLayout.setContentsMargins(30, 30, 30, 30)
mainLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.titleImageLabel = ImageLabel(":app/images/title.jpg", self)
self.titleImageLabel.scaledToHeight(130)
mainLayout.addWidget(self.titleImageLabel, 0, Qt.AlignmentFlag.AlignHCenter)
# 添加标题
self.titleLabel = QLabel(lang("应用信息"))
self.titleLabel.setStyleSheet("QLabel { font-size: 24px; font-weight: bold; }")
mainLayout.addWidget(self.titleLabel)
# 创建设置卡组
self.languageGroup = SettingCardGroup(lang("语言设置"), self)
# 语言选择设置卡
self.languageCard = ComboBoxSettingCard(
title=lang("语言设置"),
icon=FluentIcon.LANGUAGE,
texts=["中文", "English"],
configItem=cfg.language,
parent=self.languageGroup,
)
# 将设置卡添加到组
self.languageGroup.addSettingCard(self.languageCard)
# 将设置卡组添加到主布局
mainLayout.addWidget(self.languageGroup)
# 创建更新设置卡组
self.updateGroup = SettingCardGroup(lang("更新设置"), self)
# 自动更新设置开关
self.autoUpdateSwitch = SwitchSettingCard(
title=lang("开启自动更新"),
icon=FluentIcon.UPDATE,
configItem=cfg.checkUpdateAtStartUp,
parent=self.updateGroup,
)
# 手动检查更新设置卡
self.checkUpdateCard = PrimaryPushSettingCard(
title=lang("检查更新"),
text=lang("检查是否有新版本可用"),
icon=FluentIcon.UPDATE,
parent=self.updateGroup,
)
# 当前版本信息
self.versionLabel = QLabel(f"{lang('当前版本')}: {version}")
self.versionLabel.setStyleSheet(
"QLabel { font-size: 14px; color: #666; margin-top: 10px; margin-left: 10px; }"
)
# 将设置卡添加到组
self.updateGroup.addSettingCard(self.autoUpdateSwitch)
self.updateGroup.addSettingCard(self.checkUpdateCard)
self.updateGroup.layout().addWidget(self.versionLabel)
# 将更新设置卡组添加到主布局
mainLayout.addWidget(self.updateGroup)
# 添加空白占位符
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
mainLayout.addItem(spacer)
# 底部空间
bottomSpacer = QSpacerItem(20, 100, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
mainLayout.addItem(bottomSpacer)
def connectSignalToSlot(self):
# 连接语言变更信号
signalBus.languageChanged.connect(self.updateUI)
# 连接ComboBox的当前文本变更信号
self.languageCard.comboBox.currentTextChanged.connect(self.onLanguageChanged)
# 连接检查更新按钮信号
self.checkUpdateCard.clicked.connect(self.manualCheckUpdate)
# 自动更新开关的信号已通过configItem自动连接无需额外处理
# 连接登录成功信号,在用户登录后执行自动检查更新
signalBus.loginSuccessSignal.connect(self.onLoginSuccess)
def checkUpdate(self):
"""检查应用更新"""
try:
# 发送请求获取应用信息
url = "https://leon.miaostars.com/api.php?t=getappinfo&id=23"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON响应
data = response.json()
if data.get("status") == "success":
app_data = data.get("data", {})
versions = app_data.get("versions", [])
if versions:
# 获取最新版本
latest_version = versions[0].get("version", "")
# 比较版本号 - 只要版本不同就提示更新
if latest_version and latest_version != version:
# 有新版本
changelog = versions[0].get("changelog", "")
api_file_path = versions[0].get("file_path", "")
# 确保使用完整的URL如果路径不包含协议则添加域名前缀
if api_file_path:
# 检查是否是完整的URL
if not (
api_file_path.startswith("http://")
or api_file_path.startswith("https://")
):
# 添加域名前缀,确保链接完整
file_path = (
f"https://leon.miaostars.com/{api_file_path}"
)
else:
file_path = api_file_path
else:
# 使用默认下载链接
file_path = "https://leon.miaostars.com/app.php?id=23"
# 使用QFluentWidgets的MessageBox提示用户更新
msg_box = MessageBox(
lang("发现新版本"),
f"{lang('当前版本')}: {version}\n{lang('最新版本')}: {latest_version}\n\n{lang('更新内容')}:\n{changelog}",
self,
)
msg_box.yesButton.setText(lang("立即更新"))
msg_box.cancelButton.setText(lang("稍后更新"))
# QFluentWidgets的MessageBox.exec()返回True表示用户点击了确认按钮
if msg_box.exec():
# 添加下载更新的逻辑
# 例如:打开浏览器访问下载链接
if file_path:
import webbrowser
webbrowser.open(file_path)
return True
# 没有新版本或请求失败
return False
except Exception as e:
print(f"检查更新失败: {e}")
# 如果是手动检查更新,则显示错误提示
if hasattr(self, "is_manual_check") and self.is_manual_check:
error_box = MessageBox(
lang("检查更新失败"),
f"{lang('无法连接到更新服务器,请稍后再试。')}\n{str(e)}",
self,
)
error_box.cancelButton.setVisible(False)
error_box.exec()
return False
def manualCheckUpdate(self):
"""手动检查更新"""
self.is_manual_check = True
has_update = self.checkUpdate()
if not has_update and self.is_manual_check:
# 如果是手动检查且没有更新
no_update_box = MessageBox(
lang("已是最新版本"),
f"{lang('当前版本')} {version} {lang('已是最新版本。')}",
self,
)
no_update_box.cancelButton.setVisible(False)
no_update_box.exec()
self.is_manual_check = False
def autoCheckUpdate(self):
"""自动检查更新"""
print(f"自动检查更新已触发,配置状态: {cfg.checkUpdateAtStartUp.value}")
# 在单独的线程中执行避免阻塞UI
class UpdateCheckThread(QThread):
update_available = pyqtSignal(bool)
def run(self):
try:
print("开始检查更新...")
url = "https://leon.miaostars.com/api.php?t=getappinfo&id=23"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
if data.get("status") == "success":
app_data = data.get("data", {})
versions = app_data.get("versions", [])
if versions:
latest_version = versions[0].get("version", "")
print(f"当前版本: {version}, 最新版本: {latest_version}")
if latest_version and latest_version != version:
print("发现新版本,准备显示更新提示")
self.update_available.emit(True)
return
except Exception as e:
print(f"自动检查更新出错: {e}")
self.update_available.emit(False)
# 创建并启动线程
self.update_thread = UpdateCheckThread()
self.update_thread.update_available.connect(self.onAutoUpdateAvailable)
self.update_thread.start()
def onAutoUpdateAvailable(self, available):
"""自动检查更新结果处理"""
print(f"自动检查更新结果: {'有更新' if available else '无更新'}")
if available:
# 自动检查到更新时再次调用checkUpdate显示提示
self.is_manual_check = False
self.checkUpdate()
def onLoginSuccess(self):
"""用户登录成功后的处理"""
print(f"用户登录成功,检查是否开启自动更新: {cfg.checkUpdateAtStartUp.value}")
# 在用户登录成功后,根据配置决定是否自动检查更新
if cfg.checkUpdateAtStartUp.value:
self.autoCheckUpdate()
def onLanguageChanged(self, text):
# 语言变更处理
# 从选项映射获取对应的语言代码
lang_map = {"中文": "zh", "English": "en"}
lang_code = lang_map.get(text, "zh")
# 保存到配置
qconfig.set(cfg.language, lang_code)
# 显示重启提示
reply = QMessageBox.question(
self,
lang("语言变更"),
lang("语言已变更,是否立即重启应用以应用新语言?"),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply == QMessageBox.Yes:
# 重启应用
self.restartApplication()
def updateUI(self):
# 更新UI文本
self.titleLabel.setText(lang("应用信息"))
self.hintLabel.setText(lang("此页面正在建设中..."))
# 注意SettingCardGroup可能没有setTitle方法需要根据实际API调整
def restartApplication(self):
"""重启应用程序"""
# 保存配置
qconfig.save()
# 获取当前Python解释器路径和脚本路径
python = sys.executable
script = os.path.abspath(sys.argv[0])
# 退出当前进程
sys.exit()
# 注意在实际应用中这里应该使用subprocess重新启动应用但为了安全考虑
# 这里仅退出当前进程,让用户手动重启

View File

@@ -0,0 +1,49 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout
from qfluentwidgets import CardWidget, ImageLabel, SubtitleLabel
class EmptyCard(CardWidget):
def __init__(self, parent=None, text=None):
super().__init__(parent=parent)
self.setMinimumWidth(200)
self.setBorderRadius(10)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.iconLabel = ImageLabel(self)
self.iconLabel.setImage(":app/images/empty.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel = SubtitleLabel(self)
self.titleLabel.setText(text)
self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.vBoxLayout.addWidget(self.iconLabel, 0, Qt.AlignmentFlag.AlignHCenter)
self.vBoxLayout.addWidget(self.titleLabel)
def setText(self, text):
self.titleLabel.setText(text)
self.update()
def load(self):
self.iconLabel.setImage(":app/images/load.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("加载中...")
def error(self):
self.iconLabel.setImage(":app/images/error.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("加载失败,请重试")
def empty(self):
self.iconLabel.setImage(":app/images/empty.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("这里空空如也")

View File

@@ -0,0 +1,362 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import Action, BodyLabel, CardWidget, ImageLabel, InfoBar, InfoBarPosition, MenuAnimationType, \
MessageBox, PushButton, RoundMenu, StrongBodyLabel
from qfluentwidgets import FluentIcon as FIF
from app.core import (DeleteFileThread, formatDate, formatSize, getFileIcon, lang, signalBus)
from app.view.widgets.share_file_messageBox import ShareFileMessageBox
class FileCard(CardWidget):
def __init__(self, _id, fileName, fileType, path, date, size, parent=None):
super().__init__(parent)
self._id = _id
self.fileName = fileName
self.fileSize = size
self.changeTime = date
self.fileType = fileType
self.filePath = path
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.fileSizeLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
if self.fileType == "dir":
self.clicked.connect(self.dirClicked)
self.suffix = fileName.split(".")[-1].lower()
def mousePressEvent(self, event):
"""重写鼠标点击事件,区分左右键"""
if event.button() == Qt.MouseButton.RightButton:
# 使用globalPosition()获取全局位置并转换为适合菜单显示的坐标
global_pos = event.globalPosition().toPoint()
if self.fileType == "file":
self.showFileContextMenu(global_pos)
else:
self.showFolderContextMenu(global_pos)
else:
super().mousePressEvent(event)
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def dirClicked(self):
if self.filePath == "/":
paths = "/" + self.fileName
else:
paths = f"{self.filePath}/{self.fileName}"
signalBus.dirOpenSignal.emit(paths)
def selfPreview(self):
if self.fileType == "file" and self.suffix in [
"jpg",
"png",
"jpeg",
"bmp",
"gif",
]:
signalBus.imagePreviewSignal.emit(self._id)
if self.fileType == "file" and self.suffix in ["txt", "py", "md"]:
signalBus.txtPreviewSignal.emit(self._id)
def downloadFile(self):
if self.fileType == "file":
# 构建Cloudreve V4 API所需的正确路径格式
# 确保不会出现重复的前缀和文件名
if self.filePath == "/":
# 根目录情况
full_path = f"cloudreve://my/{self.fileName}"
else:
# 子目录情况,确保正确拼接路径
# 清理路径,避免重复的斜杠
clean_path = self.filePath.lstrip("/")
full_path = f"cloudreve://my/{clean_path}/{self.fileName}"
# 确保路径格式正确,没有重复的部分
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
# 确保没有重复的文件名
if f"/{self.fileName}/{self.fileName}" in full_path:
full_path = full_path.replace(f"/{self.fileName}/{self.fileName}", f"/{self.fileName}")
signalBus.addDownloadFileTask.emit(
f"own.{self.suffix}", self.fileName, full_path
)
def showFileContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(
FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile()
),
Action(
FIF.PROJECTOR, lang("预览"), triggered=lambda: self.selfPreview()
),
]
)
menu.addSeparator()
menu.addActions(
[
Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def showFolderContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
]
)
menu.addSeparator()
menu.addActions(
[
Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def deleteSelf(self):
w = MessageBox(
"确认删除",
f"你确定要删除{self.fileName}吗?\n删除后不可恢复噢!",
parent=self.window(),
)
if w.exec():
self.deleteThread = DeleteFileThread(self._id, self.fileType)
self.deleteThread.successDelete.connect(self.deleteSuccess)
self.deleteThread.errorDelete.connect(self.deleteError)
self.deleteThread.start()
else:
InfoBar.info(
"提示",
"删除已取消",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def deleteSuccess(self):
InfoBar.success(
"成功",
"成功删除",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
signalBus.refreshFolderListSignal.emit()
def deleteError(self, error_msg):
InfoBar.error(
"失败",
"删除失败",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
logger.error(f"删除文件失败:{error_msg}")
def contextMenuEvent(self, e):
"""重写上下文菜单事件,确保只有右键点击才会触发"""
pass
class ShareFileCard(CardWidget):
def __init__(self, data, parent=None):
super().__init__(parent)
self._id = data["key"]
self.fileName = data["source"]["name"]
self.fileSize = data["source"]["size"]
self.changeTime = data["create_date"]
self.fileType = "dir" if data["is_dir"] else "file"
self.preview = data["preview"]
self.passWord = data["password"]
self.remainDownloads = data["remain_downloads"]
self.downloads = data["downloads"]
self.score = data["score"]
self.views = data["views"]
self.expireTime = data["expire"]
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.viewButton = PushButton("查看", self)
self.viewButton.clicked.connect(self.viewFile)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.addWidget(self.viewButton)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
self.suffix = self.fileName.split(".")[-1].lower()
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def viewFile(self):
if self.fileType == "file":
w = ShareFileMessageBox(
self._id, self.iconLabel.pixmap(), self.suffix, self.window()
)
if w.exec():
...
else:
signalBus.shareFolderViewSignal.emit(self._id)
class SharedFolderFileCard(CardWidget):
shareFileDownloadSignal = pyqtSignal() # 共享文件下载信号
def __init__(self, key, _id, fileName, fileType, path, date, size, parent=None):
super().__init__(parent)
self._id = _id
self.key = key
self.fileName = fileName
self.fileSize = size
self.changeTime = date
self.fileType = fileType
self.filePath = path
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.fileSizeLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
self.suffix = self.fileName.split(".")[-1].lower()
if self.fileType == "dir":
# 连接左键点击信号
self.clicked.connect(self.dirClicked)
def mousePressEvent(self, event):
"""重写鼠标点击事件,区分左右键"""
if event.button() == Qt.MouseButton.RightButton:
# 右键点击,显示上下文菜单
if self.fileType == "file":
self.showFileContextMenu(event.globalPos())
else:
self.showFolderContextMenu(event.globalPos())
else:
# 左键或其他按钮点击,调用父类处理
super().mousePressEvent(event)
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def dirClicked(self):
if self.filePath == "/":
paths = "/" + self.fileName
else:
paths = f"{self.filePath}/{self.fileName}"
signalBus.shareDirOpenSignal.emit(paths)
def downloadFile(self):
if self.fileType == "file":
signalBus.addDownloadFileTask.emit(
f"share.{self.suffix}",
self.fileName,
f"{self.filePath}/{self.fileName}.{self.key}",
)
signalBus.shareFileDownloadSignal.emit()
def showFileContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(
FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile()
),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def showFolderContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)

View File

@@ -0,0 +1,263 @@
# coding: utf-8
import os
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, ImageLabel, InfoBar, InfoBarPosition, PrimaryToolButton,
ProgressBar, SubtitleLabel)
from app.core import (DownloadShareThread, DownloadThread, formatSize, getFileIcon, signalBus, UploadThread)
class UploadCard(CardWidget):
def __init__(self, fileType, filePath, parent=None):
super().__init__(parent=parent)
self.fileType = fileType
self.filePath = filePath
self.fileName = os.path.basename(filePath)
self.setFixedHeight(75)
self.hBoxLayout = QHBoxLayout(self)
self.infomationLayout = QVBoxLayout()
self.iconLabel = ImageLabel(self)
self.fileNameLabel = SubtitleLabel(self.fileName, self)
self.currentStatusLabel = BodyLabel("等待中...", self)
self.progressBar = ProgressBar(self)
self.progressBar.setValue(0)
self.cancelButton = PrimaryToolButton(FluentIcon.DELETE, self)
self.retryButton = PrimaryToolButton(FluentIcon.RETURN, self)
self.retryButton.clicked.connect(self.retryUpload)
self.cancelButton.clicked.connect(self.cancelUpload)
self.hBoxLayout.addWidget(
self.iconLabel, 0, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignLeft
)
self.hBoxLayout.addSpacing(5)
self.infomationLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(
self.currentStatusLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(self.progressBar)
self.hBoxLayout.addLayout(self.infomationLayout)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.retryButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
self.hBoxLayout.addWidget(
self.cancelButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
self.setIcon()
self.uploadThread = None
self.startUpload()
def startUpload(self):
self.retryButton.setEnabled(False)
self.uploadThread = UploadThread(self.filePath)
self.uploadThread.uploadApplicationApprovedSignal.connect(
self.uploadApplication
)
self.uploadThread.uploadFinished.connect(self.uploadFinished)
self.uploadThread.uploadFailed.connect(self.uploadFailed)
self.uploadThread.uploadProgress.connect(self.uploadProgress)
self.uploadThread.start()
def retryUpload(self):
self.currentStatusLabel.setText("重试...")
self.startUpload()
def cancelUpload(self):
if self.uploadThread:
self.progressBar.setValue(0)
self.selfDelete()
else:
self.selfDelete()
def selfDelete(self):
self.currentStatusLabel.setText("取消中...")
self.cancelButton.setEnabled(False)
self.retryButton.setEnabled(False)
if self.uploadThread:
self.uploadThread.cancelUpload()
self.uploadThread.terminate()
self.uploadThread = None
QTimer.singleShot(1000, self.deleteLater)
def uploadApplication(self):
self.currentStatusLabel.setText("已向服务器提交任务,读取文件中...")
def uploadFinished(self):
self.currentStatusLabel.setText("上传成功")
InfoBar.success(
"成功",
f"{self.fileName}上传成功,请注意查看~",
Qt.Orientation.Horizontal,
True,
5000,
InfoBarPosition.BOTTOM_RIGHT,
InfoBar.desktopView(),
)
signalBus.refreshFolderListSignal.emit()
self.retryButton.setEnabled(False)
self.progressBar.setValue(100)
def uploadProgress(self, progress, uploaded_size, total_size):
self.progressBar.setValue(int(progress))
self.currentStatusLabel.setText(
f"上传中...({formatSize(uploaded_size)}/{formatSize(total_size)})"
)
def uploadFailed(self, error_message):
self.currentStatusLabel.setText(f"上传失败:{error_message}")
self.progressBar.setValue(0)
self.retryButton.setEnabled(True)
self.uploadThread.terminate()
self.uploadThread = None
def setIcon(self):
icon_name = getFileIcon("file", self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(50)
self.iconLabel.scaledToWidth(50)
class DownloadCard(CardWidget):
def __init__(self, suffix, fileName, _id, parent=None):
super().__init__(parent=parent)
self._id = _id
self.suffix = suffix
self.setFixedHeight(75)
self.hBoxLayout = QHBoxLayout(self)
self.infomationLayout = QVBoxLayout()
self.iconLabel = ImageLabel(self)
self.fileNameLabel = SubtitleLabel(fileName, self)
self.currentStatusLabel = BodyLabel("请求中...", self)
self.progressBar = ProgressBar(self)
self.progressBar.setValue(0)
self.cancelButton = PrimaryToolButton(FluentIcon.DELETE, self)
self.cancelButton.clicked.connect(self.cancelDownload)
self.hBoxLayout.addWidget(
self.iconLabel, 0, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignLeft
)
self.hBoxLayout.addSpacing(5)
self.infomationLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(
self.currentStatusLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(self.progressBar)
self.hBoxLayout.addLayout(self.infomationLayout)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.cancelButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
suffix = self.suffix.split(".")[1].lower()
self._type = self.suffix.split(".")[0].lower()
self.setIcon(suffix)
self.downloadThread = None
self.startUpload()
def startUpload(self):
if self._type == "own":
# 同时传递file_id和file_path确保file_path不为空
# 对于own类型使用self._id作为file_path因为它已经包含了完整的cloudreve://格式路径
self.downloadThread = DownloadThread(self._id, self._id)
elif self._type == "share":
self.downloadThread = DownloadShareThread(self._id)
self.downloadThread.downloadUrlAcquired.connect(self.downloadUrlAcquired)
self.downloadThread.downloadFinished.connect(self.downloadFinished)
self.downloadThread.downloadFailed.connect(self.downloadFailed)
self.downloadThread.downloadProgress.connect(self.downloadProgress)
self.downloadThread.start()
def cancelDownload(self):
if self.downloadThread:
self.progressBar.setValue(0)
self.selfDelete()
else:
self.selfDelete()
def selfDelete(self):
self.currentStatusLabel.setText("取消中...")
self.cancelButton.setEnabled(False)
if self.downloadThread:
self.downloadThread.cancelDownload()
self.downloadThread.terminate()
self.downloadThread = None
QTimer.singleShot(1000, self.deleteLater)
def downloadUrlAcquired(self):
self.currentStatusLabel.setText("成功获取下载链接,准备下载...")
def downloadFinished(self):
self.currentStatusLabel.setText("下载成功")
InfoBar.success(
"成功",
f"{self._id}下载成功,请注意查看~",
Qt.Orientation.Horizontal,
True,
5000,
InfoBarPosition.BOTTOM_RIGHT,
InfoBar.desktopView(),
)
self.progressBar.setValue(100)
def downloadProgress(self, progress, uploaded_size, total_size):
self.progressBar.setValue(int(progress))
self.currentStatusLabel.setText(
f"下载中...({formatSize(uploaded_size)}/{formatSize(total_size)})"
)
def downloadFailed(self, error_message):
self.currentStatusLabel.setText(f"下载失败:{error_message}")
self.progressBar.setValue(0)
self.downloadThread.terminate()
self.downloadThread = None
def setIcon(self, fileName):
icon_name = getFileIcon("file", fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(50)
self.iconLabel.scaledToWidth(50)

View File

@@ -0,0 +1,37 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout
from qfluentwidgets import BodyLabel, ElevatedCardWidget, SubtitleLabel
class GbInformationCard(ElevatedCardWidget):
def __init__(self, amount,station,parent=None):
super().__init__(parent=parent)
self.currentAmountLabel = SubtitleLabel(self)
self.currentAmountLabel.setText(self.formatSize(amount))
self.currentAmountLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.stationLabel = BodyLabel(station,self)
self.stationLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(2,5,2,5)
self.vBoxLayout.addWidget(self.currentAmountLabel,0,Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addSpacing(0)
self.vBoxLayout.addWidget(self.stationLabel,0,Qt.AlignmentFlag.AlignTop)
@staticmethod
def formatSize(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 updateValue(self,value):
self.currentAmountLabel.setText(self.formatSize(value))

View File

@@ -0,0 +1,362 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QFileDialog, QVBoxLayout, QWidget
from qfluentwidgets import (
Action,
InfoBar,
InfoBarPosition,
MenuAnimationType,
RoundMenu,
ScrollArea,
)
from qfluentwidgets import FluentIcon as FIF
from app.core import (lang, ListFileThread, ListSearchThread, ListShareThread, policyConfig, signalBus)
from app.view.components.file_card import FileCard, ShareFileCard
from app.view.widgets.new_folder_messageBox import NewFolderMessageBox
from app.view.widgets.policy_messageBox import PolicyChooseMessageBox
class LinkageSwitchingBase(ScrollArea):
"""文件卡片滚动区域组件基"""
def __init__(self, parent=None):
super().__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
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)
def addFileCard(self, fileId, data):
"""
添加文件卡片
Args:
fileId: 文件的唯一标识符
data: 文件数据对象
Returns:
创建的文件卡片对象
"""
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
# 安全地获取对象属性提供默认值以避免KeyError
fileId = data.get("id", "")
fileName = data.get("name", "未知文件")
# Cloudreve V4 API使用数字1表示文件夹0表示文件
fileType_num = data.get("type", 0)
# 将数字类型转换为字符串表示
fileType = "folder" if fileType_num == 1 else "file"
filePath = data.get("path", "")
# 使用created_at或updated_at作为日期
fileDate = data.get("created_at", data.get("date", ""))
fileSize = data.get("size", 0)
fileCard = FileCard(
fileId,
fileName,
fileType,
filePath,
fileDate,
fileSize,
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:
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 refreshFolderList(self):
logger.debug("刷新文件夹列表")
InfoBar.success(
"成功",
"刷新成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
signalBus.refreshFolderListSignal.emit()
# 个人文件浏览区域
class OwnFileLinkageSwitching(LinkageSwitchingBase):
"""个人件卡片滚动区域组件"""
def __init__(self, paths, parent=None):
super(OwnFileLinkageSwitching, self).__init__(parent)
self.currentPath = paths
self.fileCardsDict = {} # 存储所有文件卡片
self.loadDict("/")
def contextMenuEvent(self, e):
"""菜单事件"""
logger.debug("触发上下文菜单事件")
menu = RoundMenu(parent=self)
menu.addAction(
Action(FIF.SYNC, lang("刷新当前"), triggered=self.refreshFolderList)
)
menu.addSeparator()
menu.addAction(
Action(FIF.ADD, lang("新建文件夹"), triggered=self._createFolder)
)
menu.addSeparator()
menu.addAction(Action(FIF.UP, lang("上传文件"), triggered=self._uploadFile))
menu.addSeparator()
menu.addAction(
Action(FIF.CLOUD, lang("设置存储策略"), triggered=self._choosePolicy)
)
menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
def _choosePolicy(self):
w = PolicyChooseMessageBox(self.window())
if w.exec():
...
def _createFolder(self):
w = NewFolderMessageBox(self.window())
if w.exec():
...
def _uploadFile(self):
file_name, _ = QFileDialog.getOpenFileName(
self.window(), "选择文件", "", "所有文件 (*)"
)
if file_name:
signalBus.addUploadFileTask.emit(file_name)
def loadDict(self, paths):
"""加载目录数据"""
logger.info(f"加载目录数据: {paths}")
policyConfig.setCurrentPath(paths)
self.currentPath = paths
self.loadDataThread = ListFileThread(paths)
self.loadDataThread.listDictSignal.connect(self.dealData)
self.loadDataThread.errorSignal.connect(self._errorLoadDict)
self.loadDataThread.start()
def dealData(self, data):
"""处理目录数据"""
self.clearFileCards()
logger.info("设置当前页策略")
# 安全地访问策略信息考虑data["data"]可能是列表的情况
if isinstance(data, dict) and "data" in data:
data_content = data["data"]
if isinstance(data_content, dict):
# Cloudreve V4 API格式处理
if "storage_policy" in data_content:
policyConfig.setPolicy(data_content["storage_policy"])
elif "policy" in data_content:
policyConfig.setPolicy(data_content["policy"])
elif isinstance(data_content, list) and data_content:
# 如果data_content是列表尝试从第一个元素获取策略
logger.warning("data['data']是列表而不是字典可能需要调整API响应处理")
# 处理data["data"]可能是列表或字典的情况
data_content = data.get("data", {})
if isinstance(data_content, list):
# 如果是列表直接使用列表作为objects
logger.info(f"成功加载目录数据,对象数量: {len(data_content)}")
self.objects = data_content
elif isinstance(data_content, dict):
# Cloudreve V4 API格式处理先检查files字段
if "files" in data_content:
# Cloudreve V4 API 使用files字段存储文件列表
logger.info(f"成功加载目录数据,对象数量: {len(data_content['files'])}")
self.objects = data_content["files"]
elif "objects" in data_content:
# 向后兼容旧版API
logger.info(f"成功加载目录数据,对象数量: {len(data_content['objects'])}")
self.objects = data_content["objects"]
else:
logger.error("目录数据格式错误字典中没有files或objects字段")
return
else:
logger.error("目录数据格式错误")
return
for obj in self.objects:
self.addFileCard(obj["id"], obj)
def _errorLoadDict(self, error_msg):
"""处理加载目录数据失败"""
logger.error(f"加载目录数据失败: {error_msg}")
InfoBar.error(
"错误",
f"加载目录数据失败: {error_msg}",
Qt.Orientation.Horizontal,
True,
-1,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.loadDict("/")
# 搜索文件浏览区域
class SearchLinkageSwitching(LinkageSwitchingBase):
"""文件卡片滚动区域组件"""
def __init__(self, parent=None):
super(SearchLinkageSwitching, self).__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
def search(self, searchType, searchContent):
"""加载数据"""
self.loadDataThread = ListSearchThread(searchContent, searchType)
self.loadDataThread.listDictSignal.connect(self._dealData)
self.loadDataThread.errorSignal.connect(self._error)
self.loadDataThread.start()
def _dealData(self, data):
"""处理数据"""
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()
for obj in self.objects:
self.addFileCard(obj["id"], obj)
def _error(self, msg):
"""处理错误"""
logger.error(f"加载数据失败: {msg}")
InfoBar.error(
"错误",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 分享文件浏览区域
class ShareLinkageSwitching(LinkageSwitchingBase):
"""文件卡片滚动区域组件"""
totalItemsSignal = pyqtSignal(int) # 信号:传递总数量
def __init__(self, parent=None):
super().__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
logger.debug(f"初始化搜索卡片滚动区域")
def addFileCard(self, fileId, obj):
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
fileCard = ShareFileCard(
obj,
self,
)
fileCard.setObjectName(f"fileCard_{fileId}")
self.fileCardsDict[fileId] = fileCard
self.layouts.addWidget(fileCard)
return fileCard
def search(self, keyword, orderBy, order, page):
"""加载数据"""
self.loadDataThread = ListShareThread(keyword, orderBy, order, page)
self.loadDataThread.listDictSignal.connect(self._dealData)
self.loadDataThread.errorSignal.connect(self._error)
self.loadDataThread.start()
def _dealData(self, data):
"""处理数据"""
if not data or "data" not in data:
logger.error("数据格式错误缺少data字段")
return
# 处理data["data"]可能是列表或字典的情况
data_content = data["data"]
if isinstance(data_content, list):
logger.warning("data['data']是列表而不是字典,将直接使用列表数据")
self.objects = data_content
elif isinstance(data_content, dict):
# 尝试从字典中获取对象列表按照Cloudreve V4 API格式处理
if "files" in data_content:
# Cloudreve V4 API 使用files字段存储文件列表
self.objects = data_content["files"]
elif "items" in data_content:
self.objects = data_content["items"]
elif "objects" in data_content:
self.objects = data_content["objects"]
else:
logger.error("数据格式错误字典中没有files、items或objects字段")
return
else:
logger.error(f"数据格式错误data['data']类型为{type(data_content).__name__},应为列表或字典")
return
logger.info(f"成功加载数据,对象数量: {len(self.objects)}")
# 尝试获取总数,如果不存在则不发送信号
if isinstance(data_content, dict) and "total" in data_content:
self.totalItemsSignal.emit(data_content["total"])
self.clearFileCards()
for obj in self.objects:
# 使用obj中可能存在的不同键名
file_id = obj.get("key", obj.get("id", None))
file_path = obj.get("path", None)
if file_id:
self.addFileCard(file_id, obj)
def _error(self, error):
"""处理错误"""
logger.error(f"加载数据失败: {error}")
InfoBar.error(
"错误",
f"加载数据失败: {error}",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)

256
app/view/login_window.py Normal file
View File

@@ -0,0 +1,256 @@
# coding:utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import (
QColor,
QIcon,
)
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
isDarkTheme,
MSFluentTitleBar,
Pivot,
PopUpAniStackedWidget,
setThemeColor,
VerticalSeparator
)
from qframelesswindow import FramelessWindow as Window
from app.core import LoginThread, RegisterThread
from app.view.widgets.login_widget import LoginWidget
from app.view.widgets.register_widget import RegisterWidget
class RegisterWindow(Window):
"""登录注册页面"""
loginSignal = pyqtSignal()
def __init__(self, parent=None):
logger.info("初始化注册窗口")
super().__init__(parent=parent)
setThemeColor("#2F80ED")
self.setTitleBar(MSFluentTitleBar(self))
self.verificationCode = ""
self.hBoxLayout = QHBoxLayout(self)
self.loginLayout = QVBoxLayout()
self.promotionalImageLabel = ImageLabel(self)
self.pivot = Pivot(self)
self.stackedWidget = PopUpAniStackedWidget(self)
self.loginWidget = LoginWidget(self)
self.registerWidget = RegisterWidget(self)
self.__initWidgets()
logger.info("注册窗口初始化完成")
def __initWidgets(self):
logger.debug("初始化注册窗口组件")
self.titleBar.maxBtn.hide()
self.titleBar.setDoubleClickEnabled(False)
self.__initLayout()
color = QColor(25, 33, 42) if isDarkTheme() else QColor(240, 244, 249)
self.setStyleSheet(f"RegisterWindow{{background: {color.name()}}}")
self.setWindowIcon(QIcon(":app/images/logo.png"))
self.setFixedSize(690, 470)
self.promotionalImageLabel.setImage(":app/images/background.png")
self.promotionalImageLabel.scaledToWidth(300)
self.pivot.addItem("LoginWidget", "登录", icon=":app/icons/login.svg")
# TODO: 内测版本隐藏注册页面
self.pivot.addItem("RegisterWidget", "注册", icon=":app/icons/register.svg")
self.pivot.setCurrentItem("LoginWidget")
self.pivot.currentItemChanged.connect(
lambda routeKey: self.stackedWidget.setCurrentWidget(
self.findChild(QWidget, routeKey)
)
)
self.loginWidget.loginButton.clicked.connect(self.login)
self.registerWidget.registerButton.clicked.connect(self.register)
self.stackedWidget.setMaximumWidth(300)
self.stackedWidget.addWidget(self.loginWidget)
self.stackedWidget.addWidget(self.registerWidget)
self.titleBar.titleLabel.setStyleSheet(
"""
QLabel{
background: transparent;
font: 14px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC';
padding: 0 4px;
color: black
}
"""
)
desktop = QApplication.screens()[0].availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
self.titleBar.raise_()
logger.debug("注册窗口组件初始化完成")
def __initLayout(self):
logger.debug("初始化注册窗口布局")
self.loginLayout.setContentsMargins(10, 40, 10, 40)
self.hBoxLayout.setContentsMargins(25, 30, 15, 30)
self.loginLayout.addWidget(
self.pivot, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.loginLayout.addSpacing(25)
self.loginLayout.addWidget(self.stackedWidget)
self.hBoxLayout.addWidget(
self.promotionalImageLabel, 0, Qt.AlignmentFlag.AlignBottom
)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(VerticalSeparator(self))
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addLayout(self.loginLayout)
logger.debug("注册窗口布局初始化完成")
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
if self.stackedWidget.currentWidget() == self.loginWidget:
self.login()
elif self.stackedWidget.currentWidget() == self.registerWidget:
self.register()
else:
super().keyPressEvent(event)
def login(self):
"""登录"""
self.loginWidget.loginButton.setEnabled(False)
userName = self.loginWidget.emailLineEdit.text()
password = self.loginWidget.passwordLineEdit.text()
captcha = self.loginWidget.verificationCodeLineEdit.text()
if not userName or not password or not captcha:
InfoBar.warning(
"提示",
"你需要填写所有项",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
return
self.loginThread = LoginThread(userName, password, captcha)
self.loginThread.successLogin.connect(self._loginSuccess)
self.loginThread.errorLogin.connect(self._loginFailed)
self.loginThread.start()
def _loginSuccess(self):
InfoBar.success(
"成功",
"登录成功,正在跳转",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
QTimer.singleShot(500, self.loginSignal.emit)
def _loginFailed(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.loginWidget.refreshVerificationCode()
self.loginWidget.verificationCodeLineEdit.clear()
self.loginWidget.loginButton.setEnabled(True)
def register(self):
"""注册"""
self.registerWidget.registerButton.setEnabled(False)
userName = self.registerWidget.emailLineEdit.text()
password = self.registerWidget.passwordLineEdit.text()
confirmPassword = self.registerWidget.confirmPasswordLineEdit.text()
captchaCode = self.registerWidget.verificationCodeLineEdit.text()
if not userName or not password or not confirmPassword:
InfoBar.warning(
"提示",
"你需要填写所有项",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.registerButton.setEnabled(True)
return
if password != confirmPassword:
InfoBar.warning(
"提示",
"两次输入的密码不一致",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)
return
self.registerThread = RegisterThread(userName, password, captchaCode)
self.registerThread.successRegister.connect(self._registerSuccess)
self.registerThread.errorRegister.connect(self._registerFailed)
self.registerThread.start()
def _registerSuccess(self):
InfoBar.info(
"成功",
"注册成功,请前往邮箱激活账号",
Qt.Orientation.Horizontal,
True,
-1,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.stackedWidget.setCurrentWidget(self.loginWidget)
self.loginWidget.emailLineEdit.setText(self.registerWidget.emailLineEdit.text())
self.registerWidget.emailLineEdit.clear()
self.registerWidget.passwordLineEdit.clear()
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)
def _registerFailed(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.refreshVerificationCode()
self.registerWidget.verificationCodeLineEdit.clear()
self.registerWidget.passwordLineEdit.clear()
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.emailLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)

201
app/view/main_window.py Normal file
View File

@@ -0,0 +1,201 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import QSize
from PyQt6.QtGui import QColor, QIcon
from PyQt6.QtWidgets import QApplication, QWidget
from qfluentwidgets import NavigationAvatarWidget, NavigationItemPosition, SplashScreen
from app.core import cfg, qconfig, userConfig, GetUserAvatarThread,lang,signalBus
from app.view.app_info_interface import AppInfoInterface
from app.view.ownFiled_interface import OwnFiledInterface
from app.view.setting_interface import SettingInterface
from app.view.storagespace_interface import StoragespaceInterface
from app.view.task_interface import TaskInterface
from app.view.widgets.custom_fluent_window import CustomFluentWindow
from app.view.widgets.preview_box import OptimizedPreviewBox, PreviewTextBox
from app.view.widgets.share_folder_messageBox import ShareFolderMessageBox
class MainWindow(CustomFluentWindow):
def __init__(self):
logger.info("开始初始化主窗口")
super().__init__()
self.initWindow()
self.ownFiledInterface = OwnFiledInterface(self)
self.storagespaceInterface = StoragespaceInterface(self)
self.taskInterface = TaskInterface(self)
self.appInfoInterface = AppInfoInterface(self)
self.connectSignalToSlot()
self.initNavigation()
logger.info("主窗口初始化完成")
def connectSignalToSlot(self):
logger.debug("连接信号和槽")
signalBus.micaEnableChanged.connect(self.setMicaEffectEnabled)
# 预览信号连接
signalBus.imagePreviewSignal.connect(self.imagePreview)
signalBus.txtPreviewSignal.connect(self.txtPreview)
# 背景信号连接
signalBus.backgroundChanged.connect(self.updateBackground)
signalBus.opacityChanged.connect(self.updateBackground)
# 下载上传任务信号连接
signalBus.addUploadFileTask.connect(self.addUploadFileTask)
signalBus.addDownloadFileTask.connect(self.addDownloadFileTask)
signalBus.shareFolderViewSignal.connect(self.shareFolderView)
# 语言变更信号连接
signalBus.languageChanged.connect(self.updateNavigation)
def updateNavigation(self):
# 更新导航项文本
self.navigationInterface.setItemText(self.ownFiledInterface, lang("我的文件"))
self.navigationInterface.setItemText(
self.storagespaceInterface, lang("存储配额")
)
self.navigationInterface.setItemText(self.taskInterface, lang("任务管理"))
self.navigationInterface.setItemText(self.appInfoInterface, lang("应用信息"))
...
def initNavigation(self):
self.navigationInterface.setAcrylicEnabled(True)
self.navigationInterface.setExpandWidth(200)
logger.info("开始初始化导航界面")
self.addSubInterface(
self.ownFiledInterface,
QIcon(":app/icons/Myfile.svg"),
lang("我的文件"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.storagespaceInterface,
QIcon(":app/icons/Storage.svg"),
lang("存储配额"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.taskInterface,
QIcon(":app/icons/Task.svg"),
lang("任务管理"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.appInfoInterface,
QIcon(":app/icons/Application.svg"),
lang("应用信息"),
NavigationItemPosition.BOTTOM,
)
# 创建默认头像widget先使用本地默认头像
self.avatarWidget = NavigationAvatarWidget(
userConfig.userName, ":app/images/logo.png"
)
self.navigationInterface.addWidget(
routeKey="settingInterface",
widget=self.avatarWidget,
position=NavigationItemPosition.BOTTOM,
onClick=self.setPersonalInfoWidget,
)
self.settingInterface = SettingInterface(self)
self.stackedWidget.addWidget(self.settingInterface)
self.splashScreen.finish()
logger.info("导航界面初始化完成")
self.avatarThread = GetUserAvatarThread("l")
self.avatarThread.avatarPixmap.connect(self.onAvatarDownloaded)
self.avatarThread.start()
def shareFolderView(self, _id):
w = ShareFolderMessageBox(_id, self)
if w.exec():
...
def addUploadFileTask(self, filePath):
logger.info(f"添加上传文件任务: {filePath}")
self.taskInterface.uploadScrollWidget.addUploadTask(filePath)
self.stackedWidget.setCurrentWidget(self.taskInterface)
self.navigationInterface.setCurrentItem("taskInterface")
self.taskInterface._changePivot("Upload")
def addDownloadFileTask(self, suffix, fileName, _id):
logger.info(f"添加下载文件任务: {fileName}")
self.taskInterface.downloadScrollWidget.addDownloadTask(suffix, fileName, _id)
self.stackedWidget.setCurrentWidget(self.taskInterface)
self.navigationInterface.setCurrentItem("taskInterface")
self.taskInterface._changePivot("Download")
def setPersonalInfoWidget(self):
self.stackedWidget.setCurrentWidget(
self.stackedWidget.findChild(QWidget, "settingInterface")
)
self.navigationInterface.setCurrentItem("settingInterface")
def onAvatarDownloaded(self, pixmap):
userConfig.setUserAvatarPixmap(pixmap)
self.avatarWidget.setAvatar(pixmap)
self.settingInterface.updateAvatar(pixmap)
def initWindow(self):
logger.info("开始初始化窗口设置")
self.resize(960, 780)
self.setMinimumWidth(760)
self.setWindowIcon(QIcon(":app/images/logo.png"))
self.setWindowTitle(lang("LeonPan"))
logger.debug("已设置窗口基本属性")
self.setCustomBackgroundColor(QColor(240, 244, 249), QColor(32, 32, 32))
self.setMicaEffectEnabled(cfg.get(cfg.micaEnabled))
logger.debug("已设置窗口背景和Mica效果")
self.setBackgroundImage(
qconfig.get(cfg.customBackground), qconfig.get(cfg.customOpactity)
) # create splash screen
# 使用自定义的背景设置方法
# create splash screen
self.splashScreen = SplashScreen(self.windowIcon(), self)
self.splashScreen.setIconSize(QSize(106, 106))
self.splashScreen.raise_()
logger.debug("已创建并设置启动屏幕")
desktop = QApplication.primaryScreen().availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
logger.debug("已移动窗口到屏幕中心")
self.show()
QApplication.processEvents()
logger.info("窗口初始化完成并显示")
def resizeEvent(self, e):
super().resizeEvent(e)
if hasattr(self, "splashScreen"):
self.splashScreen.resize(self.size())
# 窗口大小改变时更新背景
def imagePreview(self, _id):
# 使用V4 API进行预览
url = f"/file/preview/{_id}"
self.previewBox = OptimizedPreviewBox(self, url)
if self.previewBox.exec():
pass
def txtPreview(self, _id):
# 使用V4 API获取内容
url = f"/file/content/{_id}"
self.previewBox = PreviewTextBox(self, url, _id)
if self.previewBox.exec():
pass
def updateBackground(self):
"""更新窗口背景"""
self.setBackgroundImage(
qconfig.get(cfg.customBackground), qconfig.get(cfg.customOpactity)
)

View File

@@ -0,0 +1,138 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
VerticalSeparator,
)
from app.core import signalBus
from app.view.widgets.ownfile_scroll_widget import OwnFileScrollWidget
from app.view.widgets.ownFiled_widgets import SearchWidget, TagWidget
from app.view.widgets.share_search_widgets import ShareSearchScrollWidget
from app.view.widgets.ware_search_widgets import WareSearchScrollWidget
class OwnFiledInterface(QWidget):
"""主文件管理界面"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("OwnFiledInterface")
self.currentPath = "/"
logger.debug("初始化主文件管理界面")
# 初始化组件
self.searchWidget = SearchWidget(self)
self.tagWidget = TagWidget(self)
self.ownFileScrollWidget = OwnFileScrollWidget(self)
self.wareSearchScrollWidget = WareSearchScrollWidget(self)
self.wareSearchScrollWidget.hide()
self.shareSearchScrollWidget = ShareSearchScrollWidget(self)
self.shareSearchScrollWidget.hide()
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
logger.debug("设置主文件管理界面UI")
# 设置主布局
self.initLayout()
def initLayout(self):
"""初始化布局"""
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(10, 0, 10, 0)
# 创建顶部布局
self.topLayout = QHBoxLayout()
self.topLayout.setContentsMargins(0, 0, 0, 0)
self.topLayout.setAlignment(
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignVCenter
)
self.topLayout.addWidget(
self.tagWidget, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.topLayout.addSpacing(20)
self.verticalSeparator = VerticalSeparator(self)
self.verticalSeparator.setFixedHeight(25)
self.topLayout.addWidget(self.verticalSeparator)
self.topLayout.addSpacing(20)
self.topLayout.addWidget(self.searchWidget, 0, Qt.AlignmentFlag.AlignTop)
# 添加所有组件到主布局
self.vBoxLayout.addLayout(self.topLayout)
self.vBoxLayout.addWidget(self.ownFileScrollWidget)
self.vBoxLayout.addWidget(self.wareSearchScrollWidget)
self.vBoxLayout.addWidget(self.shareSearchScrollWidget)
def tagSearch(self, types, keyword):
self.wareSearchScrollWidget.show()
self.ownFileScrollWidget.hide()
self.wareSearchScrollWidget.wareSearch(types, keyword)
def search(self):
keyword = self.searchWidget.searchLineEdit.text()
searchType = self.searchWidget.searchButton.text()
if keyword == "" or keyword == ".":
InfoBar.warning(
"注意",
"搜索内容为空",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
return
if searchType == "仓内搜索":
self.wareSearchScrollWidget.show()
self.ownFileScrollWidget.hide()
self.shareSearchScrollWidget.hide()
self.wareSearchScrollWidget.wareSearch("keyword", keyword)
self.tagWidget.tagScrollArea.clearChecked()
elif searchType == "站内搜索":
self.wareSearchScrollWidget.hide()
self.ownFileScrollWidget.hide()
self.shareSearchScrollWidget.show()
self.shareSearchScrollWidget.shareSearch(keyword, 1)
self.tagWidget.tagScrollArea.clearChecked()
def returnLinkageSwitchingPage(self):
self.wareSearchScrollWidget.hide()
self.shareSearchScrollWidget.hide()
self.ownFileScrollWidget.show()
self.tagWidget.tagScrollArea.clearChecked()
self.searchWidget.searchLineEdit.clear()
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接主文件管理界面信号")
# 连接搜索信号
signalBus.dirOpenSignal.connect(
lambda x: self.ownFileScrollWidget.onChangeDir(x)
)
signalBus.refreshFolderListSignal.connect(
self.ownFileScrollWidget.refreshCurrentDirectory
)
self.wareSearchScrollWidget.returnSignal.connect(
self.returnLinkageSwitchingPage
)
self.shareSearchScrollWidget.returnSignal.connect(
self.returnLinkageSwitchingPage
)
self.searchWidget.searchButton.clicked.connect(self.search)
self.tagWidget.tagScrollArea.tagClicked.connect(self.tagSearch)

View File

@@ -0,0 +1,363 @@
# coding:utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QFileDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
AvatarWidget,
BodyLabel,
GroupHeaderCardWidget,
HyperlinkLabel,
InfoBarPosition,
LineEdit,
PushButton,
ScrollArea,
Slider,
SubtitleLabel,
VerticalSeparator,
)
from qfluentwidgets import InfoBar, MessageBoxBase
from app.core import cfg, lang, qconfig, signalBus, UserAvatarUpdateThread, userConfig, UserNickNameUpdateThread
from app.view.widgets.custom_background_messageBox import CustomBgMessageBox
class NickNameEdit(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent)
self.titleLabel = SubtitleLabel(lang("修改昵称"), self)
self.lineEdit = LineEdit(self)
self.widget.setMinimumWidth(200)
self.viewLayout.addWidget(
self.titleLabel, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.viewLayout.addWidget(self.lineEdit)
self.yesButton.setText("确定")
self.cancelButton.setText("取消")
class BasicInformationSettingCard(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("basicInformationSettingCard")
self.setTitle(lang("用户信息"))
self.nickNameEdit = PushButton(lang("修改昵称"), self)
self.nickNameEdit.clicked.connect(self._changeNickName)
self.addGroup(":app/icons/Nickname.svg", userConfig.userId, "UID", QLabel(self))
self.addGroup(
":app/icons/Nickname.svg",
userConfig.userName,
lang("用户昵称"),
self.nickNameEdit,
)
self.addGroup(
":app/icons/Email.svg",
userConfig.userEmail,
lang("电子邮箱"),
QLabel(self),
)
self.addGroup(
":app/icons/Group.svg",
userConfig.userGroup,
lang("当前用户组"),
QLabel(self),
)
self.addGroup(
":app/icons/Score.svg", userConfig.userScore, "积分", QLabel(self)
)
self.addGroup(
":app/icons/Date.svg",
userConfig.userCreatedTime,
lang("用户注册时间"),
QLabel(self),
)
def _changeNickName(self):
w = NickNameEdit(self.window())
def _onNickNameSuccess():
self.groupWidgets[2].setTitle(newNickName)
InfoBar.success(
lang("修改成功"),
lang("昵称修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def _onNickNameError(error):
InfoBar.error(
lang("修改失败"),
error,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
if w.exec():
newNickName = w.lineEdit.text()
self.nickNameServiceThread = UserNickNameUpdateThread(newNickName)
self.nickNameServiceThread.successUpdate.connect(_onNickNameSuccess)
self.nickNameServiceThread.errorUpdate.connect(_onNickNameError)
self.nickNameServiceThread.start()
class SoftWardSettingWidget(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("软件设置")
self.downloadSavePathButton = PushButton(lang("选择保存路径"), self)
self.downloadSavePathButton.clicked.connect(self._chooseDownloadSavePath)
self.addGroup(
":app/icons/SavePath.svg",
qconfig.get(cfg.downloadSavePath),
lang("选择下载保存路径"),
self.downloadSavePathButton,
)
def _chooseDownloadSavePath(self):
folder_path = QFileDialog.getExistingDirectory(self, lang("选择文件夹"))
if folder_path:
print(f"选择的文件夹路径是: {folder_path}")
qconfig.set(cfg.downloadSavePath, folder_path)
self.groupWidgets[0].setTitle(folder_path)
InfoBar.success(
lang("修改成功"),
lang("下载保存路径修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
class ThemeSettingWidget(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle(lang("背景图片设置"))
self.officialBackgroundButton = PushButton(lang("官方背景图"), self)
self.officialBackgroundButton.clicked.connect(self.officialBackground)
self.customBackgroundButton = PushButton(lang("选择自定义背景"), self)
self.customBackgroundButton.clicked.connect(self.customBackground)
self.opacitySlider = Slider(Qt.Orientation.Horizontal, self)
self.opacitySlider.setRange(0, 10)
self.opacitySlider.setFixedWidth(100)
self.opacitySlider.setValue(int(qconfig.get(cfg.customOpactity) * 10))
self.opacitySlider.valueChanged.connect(self.setOpacity)
self.addGroup(
":app/icons/BgImage.svg",
lang("官方预设背景图片"),
lang("选择背景图片"),
self.officialBackgroundButton,
)
self.addGroup(
":app/icons/BgImage.svg",
lang("自定义背景图片"),
lang("选择自定义图片,选择后请不要更改图片位置"),
self.customBackgroundButton,
)
self.addGroup(
":app/icons/Opacity.svg",
lang("图片背景透明度"),
lang("设置图片背景透明度"),
self.opacitySlider,
)
def officialBackground(self):
w = CustomBgMessageBox(self.window())
if w.exec():
index = w.returnImage()
qconfig.set(cfg.customBackground, f"app\\resource\\images\\bg{index}.png")
signalBus.backgroundChanged.emit()
def customBackground(self):
file_name, _ = QFileDialog.getOpenFileName(
self,
"选择背景",
"",
"Image Files (*.png *.jpg *.jpeg *.bmp);;All Files (*)",
)
qconfig.set(cfg.customBackground, file_name)
signalBus.backgroundChanged.emit()
def setOpacity(self, opacity):
qconfig.set(cfg.customOpactity, opacity / 10)
signalBus.opacityChanged.emit()
class AgreementLabelWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.privacyPolicy = HyperlinkLabel(
lang("隐私协议"),
self,
)
self.userAgreement = HyperlinkLabel(
lang("用户协议"),
self,
)
self.verticalSeparator = VerticalSeparator(self)
self.verticalSeparator.setFixedHeight(15)
self.privacyPolicy.setUrl("https://mp.miaostars.com/ysxy")
self.userAgreement.setUrl("https://mp.miaostars.com/xy")
self.hBoxLayout.addWidget(self.privacyPolicy, 0, Qt.AlignmentFlag.AlignCenter)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(self.verticalSeparator)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.userAgreement,
)
class SettingInterface(ScrollArea):
"""Setting interface"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.avatarWidget = AvatarWidget(self.scrollWidget)
self.basicInformationSettingCard = BasicInformationSettingCard(
self.scrollWidget
)
self.softWardSettingWidget = SoftWardSettingWidget(self.scrollWidget)
self.themeSettingWidget = ThemeSettingWidget(self.scrollWidget)
self.agreementLabelWidget = AgreementLabelWidget(self.scrollWidget)
self.infoLabel = BodyLabel(
"增值电信业务经营许可证B1-20191399鄂ICP备2025132158号 \n ©2025 LeonPan \n 武汉喵星创想互联网科技有限公司",
self.scrollWidget,
)
self.__initWidget()
def __initWidget(self):
self.resize(1000, 800)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.setViewportMargins(0, 100, 0, 20)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("settingInterface")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.avatarWidget.setImage(QPixmap(":app/images/logo.png"))
# Connect click event to open file dialog for avatar selection
self.avatarWidget.clicked.connect(self._openAvatarDialog)
self.infoLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.infoLabel.setStyleSheet("color:gray;font-size:12px;")
# initialize layout
self.__initLayout()
self._connectSignalToSlot()
def __initLayout(self):
# add setting card group to layout
# self.expandLayout.setSpacing(28)
self.expandLayout.addWidget(
self.avatarWidget,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.expandLayout.addWidget(
self.basicInformationSettingCard, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.softWardSettingWidget, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.themeSettingWidget, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.agreementLabelWidget, 1, Qt.AlignmentFlag.AlignBottom
)
self.expandLayout.addSpacing(5)
self.expandLayout.addWidget(self.infoLabel, 0, Qt.AlignmentFlag.AlignBottom)
def _connectSignalToSlot(self): ...
def _openAvatarDialog(self):
"""Open file dialog to select avatar image"""
fileDialog = QFileDialog(self)
fileDialog.setWindowTitle(lang("选择图片"))
fileDialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
fileDialog.setFileMode(QFileDialog.FileMode.ExistingFile)
fileDialog.setNameFilter("图片文件 (*.png *.jpg *.jpeg *.bmp)")
if fileDialog.exec():
file_path = fileDialog.selectedFiles()[0]
try:
image = QImage(file_path)
if not image.isNull():
self.changeAvatar(image)
# Update the avatar widget with the new image
self.avatarWidget.setImage(QPixmap.fromImage(image))
except Exception as e:
InfoBar.error(
lang("选择失败"),
str(e),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def updateAvatar(self, avatarPixmap):
self.avatarWidget.setImage(avatarPixmap)
def changeAvatar(self, image):
def avatarUpdateError(error):
InfoBar.error(
lang("修改失败"),
error,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.avatarWidget.setEnabled(True)
def avatarUpdateSuccess():
InfoBar.success(
lang("修改成功"),
lang("头像修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.avatarWidget.setEnabled(True)
self.avatarUpdateThread = UserAvatarUpdateThread(image)
self.avatarUpdateThread.successUpdate.connect(avatarUpdateSuccess)
self.avatarUpdateThread.errorUpdate.connect(avatarUpdateError)
self.avatarUpdateThread.start()
self.avatarWidget.setEnabled(False)

View File

@@ -0,0 +1,76 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea, TitleLabel
from app.core import GetPackThread, lang
from app.view.components.gb_information_card import GbInformationCard
class NumInformationWidget(QWidget):
def __init__(self, parent=None):
super(NumInformationWidget, self).__init__(parent)
self.hBoxLayout = QHBoxLayout(self)
self.basicSizeCard = GbInformationCard(0, lang("用户组基础容量"), self)
self.packSizeCard = GbInformationCard(0, lang("有效容量包附加附加容量"), self)
self.usedSizeCard = GbInformationCard(0, lang("已使用容量"), self)
self.totalSizeCard = GbInformationCard(0, lang("总容量"), self)
self.hBoxLayout.setSpacing(10)
self.hBoxLayout.addWidget(self.basicSizeCard)
self.hBoxLayout.addWidget(self.packSizeCard)
self.hBoxLayout.addWidget(self.usedSizeCard)
self.hBoxLayout.addWidget(self.totalSizeCard)
class StoragespaceInterface(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.widgets = QWidget()
self.vBoxLayout = QVBoxLayout(self.widgets)
self.firstLoad = True
self.titleLabel = TitleLabel(lang("存储配额"), self)
self.numInformationWidget = NumInformationWidget(self)
self.__initWidget()
self._loadUserCustomStorage()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.setObjectName("storageInterface")
self.widgets.setObjectName("scrollWidgets")
self.widgets.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.titleLabel.setContentsMargins(10, 5, 5, 5)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addWidget(self.titleLabel)
self.vBoxLayout.addWidget(self.numInformationWidget)
def _loadUserCustomStorage(self):
if not self.firstLoad:
return
self.packThread = GetPackThread()
self.packThread.storageDictSignal.connect(self._successGetPack)
self.packThread.start()
def _successGetPack(self, datas):
self.packData = datas["data"]
self.firstLoad = False
self.numInformationWidget.packSizeCard.updateValue(self.packData["pack"])
self.numInformationWidget.basicSizeCard.updateValue(self.packData["base"])
self.numInformationWidget.usedSizeCard.updateValue(self.packData["used"])
self.numInformationWidget.totalSizeCard.updateValue(self.packData["total"])
logger.success("用户配额加载,已刷新")

View File

@@ -0,0 +1,54 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import PopUpAniStackedWidget, SegmentedWidget, TitleLabel
from app.core import lang
from app.view.widgets.download_widget import DownloadScrollWidget
from app.view.widgets.upload_widget import UploadScrollWidget
class TaskInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.vBoxLayout = QVBoxLayout(self)
self.titleLabel = TitleLabel(lang("任务管理"), self)
self.pivot = SegmentedWidget(self)
self.stackedWidget = PopUpAniStackedWidget(self)
self.uploadScrollWidget = UploadScrollWidget(self)
self.downloadScrollWidget = DownloadScrollWidget(self)
self.__initWidget()
def __initWidget(self):
self.setObjectName("taskInterface")
self.titleLabel.setContentsMargins(10, 5, 5, 5)
self.pivot.setMinimumWidth(200)
self.pivot.addItem("Upload", lang("文件上传"))
self.pivot.addItem("Download", lang("文件下载"))
self.pivot.setCurrentItem("Upload")
self.pivot.currentItemChanged.connect(self._changePivot)
self.stackedWidget.addWidget(self.uploadScrollWidget)
self.stackedWidget.addWidget(self.downloadScrollWidget)
self.__initLayout()
def _changePivot(self, routeKey):
self.stackedWidget.setCurrentWidget(
self.stackedWidget.findChild(QWidget, routeKey + "ScrollWidget")
)
self.pivot.setCurrentItem(routeKey)
def __initLayout(self):
self.vBoxLayout.addWidget(
self.titleLabel, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(5)
self.vBoxLayout.addWidget(
self.pivot, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(5)
self.vBoxLayout.addWidget(self.stackedWidget)

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