init
This commit is contained in:
300
app/view/app_info_interface.py
Normal file
300
app/view/app_info_interface.py
Normal 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重新启动应用,但为了安全考虑,
|
||||
# 这里仅退出当前进程,让用户手动重启
|
||||
49
app/view/components/empty_card.py
Normal file
49
app/view/components/empty_card.py
Normal 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("这里空空如也")
|
||||
362
app/view/components/file_card.py
Normal file
362
app/view/components/file_card.py
Normal 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)
|
||||
263
app/view/components/file_deal_cards.py
Normal file
263
app/view/components/file_deal_cards.py
Normal 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)
|
||||
37
app/view/components/gb_information_card.py
Normal file
37
app/view/components/gb_information_card.py
Normal 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))
|
||||
362
app/view/components/linkage_switching.py
Normal file
362
app/view/components/linkage_switching.py
Normal 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
256
app/view/login_window.py
Normal 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
201
app/view/main_window.py
Normal 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)
|
||||
)
|
||||
138
app/view/ownFiled_interface.py
Normal file
138
app/view/ownFiled_interface.py
Normal 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)
|
||||
363
app/view/setting_interface.py
Normal file
363
app/view/setting_interface.py
Normal 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)
|
||||
76
app/view/storagespace_interface.py
Normal file
76
app/view/storagespace_interface.py
Normal 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("用户配额加载,已刷新")
|
||||
54
app/view/task_interface.py
Normal file
54
app/view/task_interface.py
Normal 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)
|
||||
45
app/view/widgets/add_tag_messageBox.py
Normal file
45
app/view/widgets/add_tag_messageBox.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# coding: utf-8
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
from qfluentwidgets import LineEdit, MessageBoxBase, SubtitleLabel
|
||||
|
||||
from app.core import AddTagThread,lang
|
||||
|
||||
|
||||
class AddTagMessageBox(MessageBoxBase):
|
||||
successAddTagSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.widget.setMinimumWidth(250)
|
||||
self.titleLabel = SubtitleLabel(lang("添加标签"), self)
|
||||
|
||||
self.nameLineEdit = LineEdit(self)
|
||||
self.nameLineEdit.setPlaceholderText(lang("标签名称"))
|
||||
self.expressionLineEdit = LineEdit(self)
|
||||
self.expressionLineEdit.setPlaceholderText(lang("标签通配符"))
|
||||
|
||||
self.viewLayout.addWidget(self.titleLabel)
|
||||
self.viewLayout.addWidget(self.nameLineEdit)
|
||||
self.viewLayout.addWidget(self.expressionLineEdit)
|
||||
|
||||
self.yesButton.setText(lang("添加"))
|
||||
self.yesButton.clicked.connect(self.addTag)
|
||||
self.cancelButton.setText(lang("取消"))
|
||||
|
||||
def addTag(self):
|
||||
name = self.nameLineEdit.text()
|
||||
expression = self.expressionLineEdit.text()
|
||||
if not name or not expression:
|
||||
return
|
||||
self.addTagThread = AddTagThread(name, expression)
|
||||
self.addTagThread.successSignal.connect(self.onAddTagSuccess)
|
||||
self.addTagThread.errorSignal.connect(self.onAddTagError)
|
||||
self.addTagThread.start()
|
||||
|
||||
def onAddTagSuccess(self, name, result):
|
||||
self.accept()
|
||||
self.successAddTagSignal.emit(name, result)
|
||||
|
||||
def onAddTagError(self, name, error_msg):
|
||||
logger.error(f"添加标签失败: {name} - {error_msg}")
|
||||
23
app/view/widgets/custom_background_messageBox.py
Normal file
23
app/view/widgets/custom_background_messageBox.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# coding: utf-8
|
||||
from pathlib import Path
|
||||
from qfluentwidgets import HorizontalFlipView, MessageBoxBase, SubtitleLabel
|
||||
|
||||
|
||||
class CustomBgMessageBox(MessageBoxBase):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.titleLabel = SubtitleLabel(parent=self)
|
||||
self.titleLabel.setText("内设壁纸")
|
||||
self.viewLayout.addWidget(self.titleLabel)
|
||||
|
||||
self.imageChoice = HorizontalFlipView(parent=self)
|
||||
self.imageChoice.setBorderRadius(8)
|
||||
for i in range(0, 6):
|
||||
self.imageChoice.addImage(f"app\\resource\\images\\bg{i}.png")
|
||||
self.viewLayout.addWidget(self.imageChoice)
|
||||
|
||||
self.yesButton.setText("确定")
|
||||
self.cancelButton.hide()
|
||||
|
||||
def returnImage(self):
|
||||
return self.imageChoice.currentIndex()
|
||||
363
app/view/widgets/custom_fluent_window.py
Normal file
363
app/view/widgets/custom_fluent_window.py
Normal file
@@ -0,0 +1,363 @@
|
||||
# coding:utf-8
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
from PyQt6.QtCore import QRect, QSize, Qt
|
||||
from PyQt6.QtGui import QColor, QIcon, QPainter, QPixmap
|
||||
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
from qfluentwidgets.common.animation import BackgroundAnimationWidget
|
||||
from qfluentwidgets.common.config import qconfig
|
||||
from qfluentwidgets.common.icon import FluentIconBase
|
||||
from qfluentwidgets.common.router import qrouter
|
||||
from qfluentwidgets.common.style_sheet import (
|
||||
FluentStyleSheet,
|
||||
isDarkTheme,
|
||||
)
|
||||
from qfluentwidgets.components.navigation import (
|
||||
NavigationInterface,
|
||||
NavigationItemPosition,
|
||||
NavigationTreeWidget,
|
||||
)
|
||||
from qfluentwidgets.components.widgets.frameless_window import FramelessWindow
|
||||
from qframelesswindow import TitleBar, TitleBarBase
|
||||
|
||||
from app.view.widgets.stacked_widget import StackedWidget
|
||||
|
||||
|
||||
class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
|
||||
"""Fluent window base class"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self._isMicaEnabled = False
|
||||
self._lightBackgroundColor = QColor(240, 244, 249)
|
||||
self._darkBackgroundColor = QColor(32, 32, 32)
|
||||
self._backgroundPixmap = None # 存储背景图片
|
||||
self._backgroundOpacity = 1.0 # 背景图片不透明度
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.stackedWidget = StackedWidget(self)
|
||||
self.navigationInterface = None
|
||||
|
||||
# initialize layout
|
||||
self.hBoxLayout.setSpacing(0)
|
||||
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
FluentStyleSheet.FLUENT_WINDOW.apply(self.stackedWidget)
|
||||
|
||||
# enable mica effect on win11
|
||||
self.setMicaEffectEnabled(True)
|
||||
|
||||
# show system title bar buttons on macOS
|
||||
if sys.platform == "darwin":
|
||||
self.setSystemTitleBarButtonVisible(True)
|
||||
|
||||
qconfig.themeChangedFinished.connect(self._onThemeChangedFinished)
|
||||
|
||||
def addSubInterface(
|
||||
self,
|
||||
interface: QWidget,
|
||||
icon: Union[FluentIconBase, QIcon, str],
|
||||
text: str,
|
||||
position=NavigationItemPosition.TOP,
|
||||
):
|
||||
"""add sub interface"""
|
||||
raise NotImplementedError
|
||||
|
||||
def removeInterface(self, interface: QWidget, isDelete=False):
|
||||
"""remove sub interface
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interface: QWidget
|
||||
sub interface to be removed
|
||||
|
||||
isDelete: bool
|
||||
whether to delete the sub interface
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def switchTo(self, interface: QWidget):
|
||||
self.stackedWidget.setCurrentWidget(interface, popOut=False)
|
||||
|
||||
def _onCurrentInterfaceChanged(self, index: int):
|
||||
widget = self.stackedWidget.widget(index)
|
||||
self.navigationInterface.setCurrentItem(widget.objectName())
|
||||
qrouter.push(self.stackedWidget, widget.objectName())
|
||||
|
||||
self._updateStackedBackground()
|
||||
|
||||
def _updateStackedBackground(self):
|
||||
isTransparent = self.stackedWidget.currentWidget().property(
|
||||
"isStackedTransparent"
|
||||
)
|
||||
if bool(self.stackedWidget.property("isTransparent")) == isTransparent:
|
||||
return
|
||||
|
||||
self.stackedWidget.setProperty("isTransparent", isTransparent)
|
||||
self.stackedWidget.setStyle(QApplication.style())
|
||||
|
||||
def setCustomBackgroundColor(self, light, dark):
|
||||
"""set custom background color
|
||||
|
||||
Parameters
|
||||
----------
|
||||
light, dark: QColor | Qt.GlobalColor | str
|
||||
background color in light/dark theme mode
|
||||
"""
|
||||
self._lightBackgroundColor = QColor(light)
|
||||
self._darkBackgroundColor = QColor(dark)
|
||||
self._updateBackgroundColor()
|
||||
|
||||
def setBackgroundImage(self, imagePath: str, opacity: float = 1.0):
|
||||
"""设置背景图片
|
||||
|
||||
Parameters
|
||||
----------
|
||||
imagePath: str
|
||||
背景图片路径
|
||||
opacity: float
|
||||
背景图片不透明度,范围0.0-1.0
|
||||
"""
|
||||
self._backgroundPixmap = QPixmap(imagePath)
|
||||
if self._backgroundPixmap.isNull():
|
||||
print(f"无法加载背景图片: {imagePath}")
|
||||
return
|
||||
|
||||
self._backgroundOpacity = max(0.0, min(1.0, opacity)) # 确保在0-1范围内
|
||||
|
||||
# 设置StackedWidget为透明,以便显示背景图片
|
||||
self.stackedWidget.setProperty("isTransparent", True)
|
||||
self.stackedWidget.setStyle(QApplication.style())
|
||||
|
||||
self.update() # 触发重绘
|
||||
|
||||
def removeBackgroundImage(self):
|
||||
"""移除背景图片"""
|
||||
self._backgroundPixmap = None
|
||||
|
||||
# 恢复StackedWidget的默认背景
|
||||
self.stackedWidget.setProperty("isTransparent", False)
|
||||
self.stackedWidget.setStyle(QApplication.style())
|
||||
|
||||
self.update()
|
||||
|
||||
def _normalBackgroundColor(self):
|
||||
if not self.isMicaEffectEnabled():
|
||||
return (
|
||||
self._darkBackgroundColor
|
||||
if isDarkTheme()
|
||||
else self._lightBackgroundColor
|
||||
)
|
||||
|
||||
return QColor(0, 0, 0, 0)
|
||||
|
||||
def _onThemeChangedFinished(self):
|
||||
if self.isMicaEffectEnabled():
|
||||
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
|
||||
|
||||
def paintEvent(self, e):
|
||||
# 创建绘制器
|
||||
painter = QPainter(self)
|
||||
|
||||
# 如果有背景图片,先绘制背景图片
|
||||
if self._backgroundPixmap and not self._backgroundPixmap.isNull():
|
||||
# 设置不透明度
|
||||
painter.setOpacity(self._backgroundOpacity)
|
||||
|
||||
# 缩放图片以适应窗口大小
|
||||
scaled_pixmap = self._backgroundPixmap.scaled(
|
||||
self.size(), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
painter.drawPixmap(0, 0, scaled_pixmap)
|
||||
|
||||
# 然后调用父类的绘制方法
|
||||
super().paintEvent(e)
|
||||
|
||||
def setMicaEffectEnabled(self, isEnabled: bool):
|
||||
"""set whether the mica effect is enabled, only available on Win11"""
|
||||
if sys.platform != "win32" or sys.getwindowsversion().build < 22000:
|
||||
return
|
||||
|
||||
self._isMicaEnabled = isEnabled
|
||||
|
||||
if isEnabled:
|
||||
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
|
||||
# 启用Mica效果时移除背景图片
|
||||
self.removeBackgroundImage()
|
||||
else:
|
||||
self.windowEffect.removeBackgroundEffect(self.winId())
|
||||
|
||||
self.setBackgroundColor(self._normalBackgroundColor())
|
||||
|
||||
def isMicaEffectEnabled(self):
|
||||
return self._isMicaEnabled
|
||||
|
||||
def systemTitleBarRect(self, size: QSize) -> QRect:
|
||||
"""Returns the system title bar rect, only works for macOS
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size: QSize
|
||||
original system title bar rect
|
||||
"""
|
||||
return QRect(
|
||||
size.width() - 75, 0 if self.isFullScreen() else 9, 75, size.height()
|
||||
)
|
||||
|
||||
def setTitleBar(self, titleBar):
|
||||
super().setTitleBar(titleBar)
|
||||
|
||||
# hide title bar buttons on macOS
|
||||
if (
|
||||
sys.platform == "darwin"
|
||||
and self.isSystemButtonVisible()
|
||||
and isinstance(titleBar, TitleBarBase)
|
||||
):
|
||||
titleBar.minBtn.hide()
|
||||
titleBar.maxBtn.hide()
|
||||
titleBar.closeBtn.hide()
|
||||
|
||||
|
||||
class FluentTitleBar(TitleBar):
|
||||
"""Fluent title bar"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(48)
|
||||
self.hBoxLayout.removeWidget(self.minBtn)
|
||||
self.hBoxLayout.removeWidget(self.maxBtn)
|
||||
self.hBoxLayout.removeWidget(self.closeBtn)
|
||||
|
||||
# add window icon
|
||||
self.iconLabel = QLabel(self)
|
||||
self.iconLabel.setFixedSize(18, 18)
|
||||
self.hBoxLayout.insertWidget(
|
||||
0, self.iconLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.window().windowIconChanged.connect(self.setIcon)
|
||||
|
||||
# add title label
|
||||
self.titleLabel = QLabel(self)
|
||||
self.hBoxLayout.insertWidget(
|
||||
1, self.titleLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.titleLabel.setObjectName("titleLabel")
|
||||
self.window().windowTitleChanged.connect(self.setTitle)
|
||||
|
||||
self.vBoxLayout = QVBoxLayout()
|
||||
self.buttonLayout = QHBoxLayout()
|
||||
self.buttonLayout.setSpacing(0)
|
||||
self.buttonLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.buttonLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.buttonLayout.addWidget(self.minBtn)
|
||||
self.buttonLayout.addWidget(self.maxBtn)
|
||||
self.buttonLayout.addWidget(self.closeBtn)
|
||||
self.vBoxLayout.addLayout(self.buttonLayout)
|
||||
self.vBoxLayout.addStretch(1)
|
||||
self.hBoxLayout.addLayout(self.vBoxLayout, 0)
|
||||
|
||||
FluentStyleSheet.FLUENT_WINDOW.apply(self)
|
||||
|
||||
def setTitle(self, title):
|
||||
self.titleLabel.setText(title)
|
||||
self.titleLabel.adjustSize()
|
||||
|
||||
def setIcon(self, icon):
|
||||
self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18))
|
||||
|
||||
|
||||
class CustomFluentWindow(CustomFluentWindowBase):
|
||||
"""Fluent window"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitleBar(FluentTitleBar(self))
|
||||
|
||||
self.navigationInterface = NavigationInterface(self, showReturnButton=True)
|
||||
self.widgetLayout = QHBoxLayout()
|
||||
|
||||
# initialize layout
|
||||
self.hBoxLayout.addWidget(self.navigationInterface)
|
||||
self.hBoxLayout.addLayout(self.widgetLayout)
|
||||
self.hBoxLayout.setStretchFactor(self.widgetLayout, 1)
|
||||
|
||||
self.widgetLayout.addWidget(self.stackedWidget)
|
||||
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
|
||||
|
||||
self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_)
|
||||
self.titleBar.raise_()
|
||||
|
||||
def addSubInterface(
|
||||
self,
|
||||
interface: QWidget,
|
||||
icon: Union[FluentIconBase, QIcon, str],
|
||||
text: str,
|
||||
position=NavigationItemPosition.TOP,
|
||||
parent=None,
|
||||
isTransparent=False,
|
||||
) -> NavigationTreeWidget:
|
||||
"""add sub interface, the object name of `interface` should be set already
|
||||
before calling this method
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interface: QWidget
|
||||
the subinterface to be added
|
||||
|
||||
icon: FluentIconBase | QIcon | str
|
||||
the icon of navigation item
|
||||
|
||||
text: str
|
||||
the text of navigation item
|
||||
|
||||
position: NavigationItemPosition
|
||||
the position of navigation item
|
||||
|
||||
parent: QWidget
|
||||
the parent of navigation item
|
||||
|
||||
isTransparent: bool
|
||||
whether to use transparent background
|
||||
"""
|
||||
if not interface.objectName():
|
||||
raise ValueError("The object name of `interface` can't be empty string.")
|
||||
if parent and not parent.objectName():
|
||||
raise ValueError("The object name of `parent` can't be empty string.")
|
||||
|
||||
interface.setProperty("isStackedTransparent", isTransparent)
|
||||
self.stackedWidget.addWidget(interface)
|
||||
|
||||
# add navigation item
|
||||
routeKey = interface.objectName()
|
||||
item = self.navigationInterface.addItem(
|
||||
routeKey=routeKey,
|
||||
icon=icon,
|
||||
text=text,
|
||||
onClick=lambda: self.switchTo(interface),
|
||||
position=position,
|
||||
tooltip=text,
|
||||
parentRouteKey=parent.objectName() if parent else None,
|
||||
)
|
||||
|
||||
# initialize selected item
|
||||
if self.stackedWidget.count() == 1:
|
||||
self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged)
|
||||
self.navigationInterface.setCurrentItem(routeKey)
|
||||
qrouter.setDefaultRouteKey(self.stackedWidget, routeKey)
|
||||
|
||||
self._updateStackedBackground()
|
||||
|
||||
return item
|
||||
|
||||
def removeInterface(self, interface, isDelete=False):
|
||||
self.navigationInterface.removeWidget(interface.objectName())
|
||||
self.stackedWidget.removeWidget(interface)
|
||||
interface.hide()
|
||||
|
||||
if isDelete:
|
||||
interface.deleteLater()
|
||||
|
||||
def resizeEvent(self, e):
|
||||
self.titleBar.move(46, 0)
|
||||
self.titleBar.resize(self.width() - 46, self.titleBar.height())
|
||||
42
app/view/widgets/download_widget.py
Normal file
42
app/view/widgets/download_widget.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# coding: utf-8
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QWidget
|
||||
from qfluentwidgets import ScrollArea
|
||||
|
||||
from app.view.components.file_deal_cards import DownloadCard
|
||||
|
||||
|
||||
class DownloadScrollWidget(ScrollArea):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.scrollWidget = QWidget()
|
||||
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
|
||||
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setWidget(self.scrollWidget)
|
||||
self.setWidgetResizable(True)
|
||||
self.setObjectName("DownloadScrollWidget")
|
||||
|
||||
self.scrollWidget.setObjectName("scrollWidget")
|
||||
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
|
||||
self.setStyleSheet("background:transparent;border:none;")
|
||||
|
||||
self.__initLayout()
|
||||
|
||||
def __initLayout(self):
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
def addDownloadTask(self, suffix, fileName, _id):
|
||||
self.vBoxLayout.addWidget(
|
||||
DownloadCard(
|
||||
suffix,
|
||||
fileName,
|
||||
_id,
|
||||
self.scrollWidget,
|
||||
)
|
||||
)
|
||||
117
app/view/widgets/login_widget.py
Normal file
117
app/view/widgets/login_widget.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import QRegularExpression, Qt, pyqtSignal
|
||||
from PyQt6.QtGui import (
|
||||
QRegularExpressionValidator,
|
||||
)
|
||||
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
from qfluentwidgets import (
|
||||
CheckBox,
|
||||
LineEdit,
|
||||
PasswordLineEdit,
|
||||
PushButton,
|
||||
)
|
||||
|
||||
from app.core import CaptchaThread,cfg, qconfig
|
||||
|
||||
|
||||
class LoginWidget(QWidget):
|
||||
loginSignal = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
logger.debug("初始化登录组件")
|
||||
super().__init__(parent)
|
||||
self.setObjectName("LoginWidget")
|
||||
self.setWindowTitle("LeonPan")
|
||||
|
||||
self.emailLineEdit = LineEdit(self)
|
||||
self.emailLineEdit.setPlaceholderText("请输入邮箱")
|
||||
self.passwordLineEdit = PasswordLineEdit(self)
|
||||
self.passwordLineEdit.setPlaceholderText("请输入密码")
|
||||
self.rememberMeCheckBox = CheckBox("记住我", self)
|
||||
self.rememberMeCheckBox.checkStateChanged.connect(
|
||||
lambda: qconfig.set(cfg.rememberMe, self.rememberMeCheckBox.isChecked())
|
||||
)
|
||||
self.loginButton = PushButton("登录", self)
|
||||
self.loginButton.setDisabled(False)
|
||||
|
||||
self.verificationCodeLabel = QLabel(self)
|
||||
self.verificationCodeLabel.setFixedSize(120, 35)
|
||||
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
|
||||
self.verificationCodeLabel.mousePressEvent = (
|
||||
self.refreshVerificationCode
|
||||
) # 绑定点击事件
|
||||
self.verificationCodeLineEdit = LineEdit(self)
|
||||
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
|
||||
self.verificationLayout = QHBoxLayout()
|
||||
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
|
||||
self.verificationLayout.addWidget(self.verificationCodeLabel)
|
||||
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.vBoxLayout.addSpacing(10)
|
||||
self.vBoxLayout.addWidget(self.emailLineEdit)
|
||||
self.vBoxLayout.addSpacing(15)
|
||||
self.vBoxLayout.addWidget(self.passwordLineEdit)
|
||||
self.vBoxLayout.addSpacing(15)
|
||||
self.vBoxLayout.addLayout(self.verificationLayout)
|
||||
self.vBoxLayout.addSpacing(15)
|
||||
self.vBoxLayout.addWidget(
|
||||
self.rememberMeCheckBox, 0, Qt.AlignmentFlag.AlignLeft
|
||||
)
|
||||
|
||||
self.vBoxLayout.addSpacing(15)
|
||||
self.vBoxLayout.addWidget(self.loginButton)
|
||||
|
||||
email_regex = QRegularExpression(
|
||||
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
||||
)
|
||||
# TODO: 内测时用的邮箱匹配
|
||||
|
||||
# email_regex = QRegularExpression(r"^[a-zA-Z0-9_.+-]+@miaostars\.cn$")
|
||||
validator = QRegularExpressionValidator(email_regex, self)
|
||||
self.emailLineEdit.setValidator(validator)
|
||||
self.emailLineEdit.textChanged.connect(self.checkEmail)
|
||||
|
||||
self.refreshVerificationCode()
|
||||
self.rememberMe()
|
||||
logger.debug("登录组件初始化完成")
|
||||
|
||||
def rememberMe(self):
|
||||
logger.debug("检查记住我选项")
|
||||
if qconfig.get(cfg.rememberMe):
|
||||
logger.debug("已启用记住我功能,填充保存的邮箱和密码")
|
||||
self.emailLineEdit.setText(qconfig.get(cfg.email))
|
||||
self.passwordLineEdit.setText(qconfig.get(cfg.activationCode))
|
||||
self.rememberMeCheckBox.setChecked(True)
|
||||
else:
|
||||
logger.debug("已禁用记住我功能,清空保存的邮箱和密码")
|
||||
self.emailLineEdit.clear()
|
||||
self.passwordLineEdit.clear()
|
||||
|
||||
def checkEmail(self, text):
|
||||
# 检查当前输入是否通过验证器
|
||||
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
|
||||
if state == QRegularExpressionValidator.State.Acceptable:
|
||||
logger.debug("邮箱格式验证通过")
|
||||
self.loginButton.setDisabled(False)
|
||||
else:
|
||||
self.loginButton.setDisabled(True)
|
||||
|
||||
def refreshVerificationCode(self, event=None):
|
||||
logger.debug("刷新验证码")
|
||||
self.verificationCodeLabel.setEnabled(False)
|
||||
self.captchaThread = CaptchaThread()
|
||||
self.captchaThread.captchaReady.connect(self._showVerificationCode)
|
||||
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
|
||||
self.captchaThread.start()
|
||||
|
||||
def _showVerificationCode(self, pixmap):
|
||||
logger.debug("显示验证码")
|
||||
self.verificationCodeLabel.setEnabled(True)
|
||||
self.verificationCodeLabel.setPixmap(pixmap)
|
||||
|
||||
def _showCaptchaFailed(self, message):
|
||||
logger.debug(f"验证码刷新失败:{message}")
|
||||
self.verificationCodeLabel.setEnabled(True)
|
||||
self.verificationCodeLineEdit.clear()
|
||||
142
app/view/widgets/new_folder_messageBox.py
Normal file
142
app/view/widgets/new_folder_messageBox.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# coding: utf-8
|
||||
from PyQt6.QtCore import Qt
|
||||
from qfluentwidgets import (
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
LineEdit,
|
||||
MessageBoxBase,
|
||||
SubtitleLabel,
|
||||
)
|
||||
|
||||
from app.core import CreateFolderThread, signalBus
|
||||
|
||||
|
||||
class NewFolderMessageBox(MessageBoxBase):
|
||||
"""新建文件夹对话框"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._setupUi()
|
||||
self._connectSignals()
|
||||
|
||||
# 线程引用,防止被垃圾回收
|
||||
self.createFolderThread = None
|
||||
|
||||
def _setupUi(self):
|
||||
"""设置UI界面"""
|
||||
self.titleLabel = SubtitleLabel("新建文件夹", self)
|
||||
self.nameLineEdit = LineEdit(self)
|
||||
self.nameLineEdit.setPlaceholderText("请输入文件夹名称")
|
||||
self.nameLineEdit.setClearButtonEnabled(True)
|
||||
|
||||
# 设置对话框属性
|
||||
self.widget.setMinimumWidth(400)
|
||||
self.yesButton.setText("新建")
|
||||
self.cancelButton.setText("取消")
|
||||
|
||||
# 添加组件到布局
|
||||
self.viewLayout.addWidget(self.titleLabel)
|
||||
self.viewLayout.addWidget(self.nameLineEdit)
|
||||
|
||||
# 初始时禁用确认按钮
|
||||
self.yesButton.setEnabled(False)
|
||||
|
||||
def _connectSignals(self):
|
||||
"""连接信号槽"""
|
||||
self.yesButton.clicked.connect(self._onCreateClicked)
|
||||
self.nameLineEdit.textChanged.connect(self._onTextChanged)
|
||||
self.nameLineEdit.returnPressed.connect(self._onReturnPressed)
|
||||
|
||||
def _onTextChanged(self, text):
|
||||
"""文本框内容变化时的处理"""
|
||||
# 检查文件夹名称是否有效
|
||||
is_valid = bool(text.strip()) and not any(char in text for char in '/\\:*?"<>|')
|
||||
self.yesButton.setEnabled(is_valid)
|
||||
|
||||
if not is_valid and text.strip():
|
||||
self.nameLineEdit.setToolTip('文件夹名称不能包含 /\\:*?"<>| 等特殊字符')
|
||||
else:
|
||||
self.nameLineEdit.setToolTip("")
|
||||
|
||||
def _onReturnPressed(self):
|
||||
"""回车键处理"""
|
||||
if self.yesButton.isEnabled():
|
||||
self._onCreateClicked()
|
||||
|
||||
def _onCreateClicked(self):
|
||||
"""创建文件夹按钮点击处理"""
|
||||
folder_name = self.nameLineEdit.text().strip()
|
||||
if not folder_name:
|
||||
return
|
||||
|
||||
# 禁用按钮防止重复点击
|
||||
self._setUiEnabled(False)
|
||||
self.yesButton.setText("创建中...")
|
||||
|
||||
# 创建并启动线程
|
||||
self.createFolderThread = CreateFolderThread(folder_name)
|
||||
self.createFolderThread.successSignal.connect(self._onCreateSuccess)
|
||||
self.createFolderThread.errorSignal.connect(self._onCreateError)
|
||||
self.createFolderThread.start()
|
||||
|
||||
def _setUiEnabled(self, enabled):
|
||||
"""设置UI启用状态"""
|
||||
self.yesButton.setEnabled(enabled)
|
||||
self.cancelButton.setEnabled(enabled)
|
||||
self.nameLineEdit.setEnabled(enabled)
|
||||
|
||||
def _onCreateSuccess(self):
|
||||
"""创建成功处理"""
|
||||
self._showInfoBar("success", "操作成功", "新建文件夹成功")
|
||||
signalBus.refreshFolderListSignal.emit()
|
||||
"""线程完成时的清理工作"""
|
||||
self.yesButton.setText("新建")
|
||||
self._setUiEnabled(True)
|
||||
self.accept()
|
||||
|
||||
def _onCreateError(self, error_msg):
|
||||
"""创建失败处理"""
|
||||
self._showInfoBar("error", "操作失败", error_msg)
|
||||
# 不关闭对话框,让用户有机会修改后重试
|
||||
self.nameLineEdit.setFocus()
|
||||
self.nameLineEdit.selectAll()
|
||||
"""线程完成时的清理工作"""
|
||||
self.yesButton.setText("新建")
|
||||
self._setUiEnabled(True)
|
||||
|
||||
def _showInfoBar(self, type_, title, content):
|
||||
"""显示信息栏"""
|
||||
if type_ == "success":
|
||||
InfoBar.success(
|
||||
title=title,
|
||||
content=content,
|
||||
orient=Qt.Orientation.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=2000,
|
||||
parent=self.window(),
|
||||
)
|
||||
else:
|
||||
InfoBar.error(
|
||||
title=title,
|
||||
content=content,
|
||||
orient=Qt.Orientation.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=3000, # 错误信息显示稍长时间
|
||||
parent=self.window(),
|
||||
)
|
||||
|
||||
def showEvent(self, event):
|
||||
"""显示事件处理"""
|
||||
super().showEvent(event)
|
||||
self.nameLineEdit.setFocus()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭事件处理"""
|
||||
# 确保线程安全退出
|
||||
if self.createFolderThread and self.createFolderThread.isRunning():
|
||||
self.createFolderThread.quit()
|
||||
self.createFolderThread.wait(1000) # 等待1秒
|
||||
|
||||
super().closeEvent(event)
|
||||
297
app/view/widgets/ownFiled_widgets.py
Normal file
297
app/view/widgets/ownFiled_widgets.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from qfluentwidgets import (Action, InfoBar, InfoBarPosition, LineEdit, MenuAnimationType, PillPushButton,
|
||||
PrimarySplitPushButton, PushButton, RoundMenu, ScrollArea)
|
||||
|
||||
from app.core import DeleteTagThread, userConfig, lang
|
||||
from app.view.widgets.add_tag_messageBox import AddTagMessageBox
|
||||
|
||||
|
||||
class TagsScrollArea(ScrollArea):
|
||||
"""标签滚动区域组件,支持动态添加和移除标签,支持单选模式"""
|
||||
|
||||
# 信号:标签被点击时发出,传递标签文本
|
||||
tagClicked = pyqtSignal(str, str)
|
||||
TAG_TYPES = {"video": "视频", "doc": "文档", "image": "图片", "audio": "音乐"}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tagsDict = {} # 存储所有标签按钮
|
||||
self.currentCheckedTag = None # 当前选中的标签ID
|
||||
|
||||
self.setupUi()
|
||||
logger.debug("初始化标签滚动区域组件")
|
||||
|
||||
def setupUi(self):
|
||||
"""初始化UI"""
|
||||
self.widgets = QWidget()
|
||||
self.layouts = QHBoxLayout(self.widgets)
|
||||
self.setWidget(self.widgets)
|
||||
self.setWidgetResizable(True)
|
||||
self.setMaximumWidth(400)
|
||||
|
||||
# 设置布局属性
|
||||
self.layouts.setContentsMargins(0, 0, 0, 0)
|
||||
self.layouts.setSpacing(10)
|
||||
|
||||
# 初始化默认标签
|
||||
self.initDefaultTags()
|
||||
|
||||
# 设置样式
|
||||
self.widgets.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.setStyleSheet("background-color: transparent; border: none;")
|
||||
|
||||
# 设置滚动策略
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
# 安全地获取 tags 字段,如果不存在则使用空列表
|
||||
self.tags = userConfig.userData.get("data", {}).get("tags", [])
|
||||
|
||||
for tag in self.tags:
|
||||
self.addTag(tag["id"], tag["name"])
|
||||
self.tagsDict[tag["id"]] = tag["name"]
|
||||
|
||||
def initDefaultTags(self):
|
||||
"""初始化默认标签"""
|
||||
logger.debug("初始化默认标签")
|
||||
for tagId, tagText in self.TAG_TYPES.items():
|
||||
self.addTag(tagId, tagText)
|
||||
|
||||
def addTag(self, tagId, text):
|
||||
"""添加一个新标签
|
||||
|
||||
Args:
|
||||
tagId: 标签的唯一标识符
|
||||
text: 标签显示的文本
|
||||
|
||||
Returns:
|
||||
创建的标签按钮对象
|
||||
"""
|
||||
self.tagsDict[tagId] = text
|
||||
logger.debug(f"添加新标签: {tagId} - {text}")
|
||||
tagBtn = PillPushButton(text, self.widgets)
|
||||
tagBtn.setObjectName(f"tag_{tagId}")
|
||||
tagBtn.setCheckable(True) # 设置为可选中状态
|
||||
tagBtn.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu
|
||||
) # 启用自定义上下文菜单
|
||||
|
||||
# 连接点击信号,实现单选逻辑
|
||||
tagBtn.clicked.connect(
|
||||
lambda checked, tid=tagId: self.onTagClicked(tid, checked)
|
||||
)
|
||||
|
||||
# 连接右键菜单信号
|
||||
tagBtn.customContextMenuRequested.connect(
|
||||
lambda pos, tid=tagId: self.onTagRightClicked(tid, pos)
|
||||
)
|
||||
|
||||
self.layouts.addWidget(tagBtn)
|
||||
|
||||
return tagBtn
|
||||
|
||||
def onTagClicked(self, tagId, checked):
|
||||
"""处理标签点击事件,实现单选逻辑"""
|
||||
if checked:
|
||||
# 如果当前点击的标签被选中,取消之前选中的标签
|
||||
if self.currentCheckedTag and self.currentCheckedTag != tagId:
|
||||
# 找到之前选中的标签并取消选中
|
||||
previousTagBtn = self.findChild(
|
||||
QWidget, f"tag_{self.currentCheckedTag}"
|
||||
)
|
||||
if previousTagBtn:
|
||||
previousTagBtn.setChecked(False)
|
||||
|
||||
# 更新当前选中的标签
|
||||
self.currentCheckedTag = tagId
|
||||
|
||||
if tagId in ["video", "doc", "image", "audio"]:
|
||||
self.tagClicked.emit("internalTag", tagId)
|
||||
else:
|
||||
self.tagClicked.emit("externalTag", tagId)
|
||||
logger.debug(f"选中标签: {tagId}")
|
||||
else:
|
||||
# 如果取消选中当前标签,清空当前选中的标签
|
||||
if self.currentCheckedTag == tagId:
|
||||
self.currentCheckedTag = None
|
||||
logger.debug(f"取消选中标签: {tagId}")
|
||||
|
||||
# 发出标签点击信号
|
||||
|
||||
def onTagRightClicked(self, tagId, pos):
|
||||
"""处理标签右键点击事件"""
|
||||
logger.debug(f"标签被右键点击: {tagId}")
|
||||
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
|
||||
if tagBtn:
|
||||
global_pos = tagBtn.mapToGlobal(pos)
|
||||
if tagBtn.text() in ["视频", "文档", "图片", "音乐"]:
|
||||
return
|
||||
menu = RoundMenu(parent=self)
|
||||
|
||||
menu.addAction(Action(lang("删除"), triggered=lambda: self.deleteTag(tagId)))
|
||||
|
||||
menu.exec(global_pos, aniType=MenuAnimationType.DROP_DOWN)
|
||||
|
||||
def deleteTag(self, tagId):
|
||||
self.deleteTagThread = DeleteTagThread(tagId)
|
||||
self.deleteTagThread.successDeleteSignal.connect(
|
||||
lambda: self._onTagDeleteError(tagId)
|
||||
)
|
||||
self.deleteTagThread.errorSignal.connect(self._onTagDeleteError)
|
||||
self.deleteTagThread.start()
|
||||
|
||||
def _onTagDeleteError(self, msg):
|
||||
InfoBar.error(
|
||||
"失败",
|
||||
msg,
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
|
||||
def _onTagDeleteError(self, tagId):
|
||||
self.removeTag(tagId)
|
||||
InfoBar.success(
|
||||
"成功",
|
||||
"标签删除成功",
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
|
||||
def removeTag(self, tagId):
|
||||
"""移除指定标签"""
|
||||
if tagId in self.tagsDict:
|
||||
logger.debug(f"移除标签: {tagId}")
|
||||
|
||||
# 如果移除的是当前选中的标签,清空选中状态
|
||||
if self.currentCheckedTag == tagId:
|
||||
self.currentCheckedTag = None
|
||||
|
||||
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
|
||||
if tagBtn:
|
||||
self.layouts.removeWidget(tagBtn)
|
||||
tagBtn.deleteLater()
|
||||
del self.tagsDict[tagId]
|
||||
else:
|
||||
logger.warning(f"尝试移除不存在的标签: {tagId}")
|
||||
|
||||
def getCheckedTag(self):
|
||||
"""获取当前选中的标签ID"""
|
||||
return self.currentCheckedTag
|
||||
|
||||
def setCheckedTag(self, tagId):
|
||||
"""设置指定标签为选中状态"""
|
||||
if tagId in self.tagsDict:
|
||||
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
|
||||
if tagBtn:
|
||||
tagBtn.setChecked(True)
|
||||
# onTagClicked 方法会自动处理单选逻辑
|
||||
else:
|
||||
logger.warning(f"尝试选中不存在的标签: {tagId}")
|
||||
|
||||
def clearChecked(self):
|
||||
"""清除所有选中状态"""
|
||||
if self.currentCheckedTag:
|
||||
tagBtn = self.findChild(QWidget, f"tag_{self.currentCheckedTag}")
|
||||
if tagBtn:
|
||||
tagBtn.setChecked(False)
|
||||
self.currentCheckedTag = None
|
||||
|
||||
|
||||
class TagWidget(QWidget):
|
||||
"""标签管理组件"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tagScrollArea = TagsScrollArea(self)
|
||||
self.addPushButton = PushButton(lang("添加标签"), self)
|
||||
|
||||
logger.debug("初始化标签管理组件")
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
def setupUi(self):
|
||||
"""初始化UI"""
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.hBoxLayout.addWidget(self.tagScrollArea)
|
||||
self.hBoxLayout.addSpacing(10)
|
||||
self.hBoxLayout.addWidget(self.addPushButton, Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
def connectSignals(self):
|
||||
"""连接信号与槽"""
|
||||
logger.debug("连接标签管理组件信号")
|
||||
self.addPushButton.clicked.connect(self.addTag)
|
||||
|
||||
def addTag(self):
|
||||
w = AddTagMessageBox(self.window())
|
||||
w.successAddTagSignal.connect(self.onAddTagSuccess)
|
||||
if w.exec():
|
||||
...
|
||||
|
||||
def onAddTagSuccess(self, name, result):
|
||||
"""处理标签添加成功事件"""
|
||||
InfoBar.success(
|
||||
"成功",
|
||||
"标签添加成功",
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
self.tagScrollArea.addTag(result["data"], name)
|
||||
|
||||
|
||||
class SearchWidget(QWidget):
|
||||
"""搜索组件"""
|
||||
|
||||
# 信号:搜索请求时发出,传递搜索关键词
|
||||
searchRequested = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.searchLineEdit = LineEdit(self)
|
||||
self.searchButton = PrimarySplitPushButton(lang("仓内搜索"), self)
|
||||
|
||||
self.menu = RoundMenu(parent=self)
|
||||
self.menu.addAction(
|
||||
Action(
|
||||
lang("仓内搜索"),
|
||||
triggered=lambda: self.changeButtonText(lang("仓内搜索")),
|
||||
)
|
||||
)
|
||||
self.menu.addAction(
|
||||
Action(
|
||||
lang("站内搜索"),
|
||||
triggered=lambda: self.changeButtonText(lang("站内搜索")),
|
||||
)
|
||||
)
|
||||
|
||||
self.searchButton.setFlyout(self.menu)
|
||||
|
||||
logger.debug("初始化搜索组件")
|
||||
|
||||
self.setupUi()
|
||||
|
||||
def changeButtonText(self, text):
|
||||
self.searchButton.setText(text)
|
||||
|
||||
def setupUi(self):
|
||||
"""初始化UI"""
|
||||
self.searchLineEdit.setPlaceholderText(lang("搜索文件"))
|
||||
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.hBoxLayout.setContentsMargins(0, 24, 0, 0)
|
||||
self.hBoxLayout.addWidget(self.searchLineEdit)
|
||||
self.hBoxLayout.addWidget(self.searchButton)
|
||||
91
app/view/widgets/ownfile_scroll_widget.py
Normal file
91
app/view/widgets/ownfile_scroll_widget.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# coding: utf-8
|
||||
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QWidget
|
||||
from qfluentwidgets import (
|
||||
BreadcrumbBar,
|
||||
setFont,
|
||||
)
|
||||
|
||||
from app.view.components.linkage_switching import OwnFileLinkageSwitching
|
||||
|
||||
|
||||
class OwnFileScrollWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.currentPath = "/"
|
||||
|
||||
self.breadcrumbBar = BreadcrumbBar(self)
|
||||
self.ownFileLinkageSwitching = OwnFileLinkageSwitching("/", self)
|
||||
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
self.setObjectName("OwnFileScrollWidget")
|
||||
|
||||
self.breadcrumbBar.addItem("/", "/")
|
||||
setFont(self.breadcrumbBar, 18)
|
||||
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
|
||||
self.breadcrumbBar.setSpacing(15)
|
||||
self.__initLayout()
|
||||
|
||||
def __initLayout(self):
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.vBoxLayout.setSpacing(5)
|
||||
self.vBoxLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
|
||||
self.vBoxLayout.addWidget(self.ownFileLinkageSwitching)
|
||||
|
||||
def loadDict(self, paths):
|
||||
self.ownFileLinkageSwitching.loadDict(paths)
|
||||
|
||||
def onChangeDir(self, path):
|
||||
"""处理目录变更"""
|
||||
logger.info(f"变更目录: {path}")
|
||||
if path == "":
|
||||
self.currentPath = "/"
|
||||
else:
|
||||
self.currentPath = path
|
||||
self.ownFileLinkageSwitching.loadDict(path)
|
||||
|
||||
# 更新面包屑导航
|
||||
displayName = path.split("/")[-1] if path != "/" else "/"
|
||||
self.breadcrumbBar.addItem(displayName, displayName)
|
||||
|
||||
def clickChangeDir(self, name):
|
||||
"""处理面包屑导航项点击事件"""
|
||||
logger.info(f"面包屑导航项点击: {name}")
|
||||
|
||||
# 获取点击的路径
|
||||
if name == "":
|
||||
name = "/"
|
||||
|
||||
pathList = []
|
||||
|
||||
for item in self.breadcrumbBar.items:
|
||||
if item.text == name:
|
||||
pathList.append(item.text)
|
||||
break
|
||||
else:
|
||||
pathList.append(item.text)
|
||||
|
||||
for i in pathList:
|
||||
if i == "":
|
||||
pathList.remove(i)
|
||||
|
||||
path = "/".join(pathList) if pathList else "/"
|
||||
path = path[1:]
|
||||
if path == "":
|
||||
path = "/"
|
||||
|
||||
if path == self.currentPath:
|
||||
logger.debug("路径未变化,跳过导航")
|
||||
return
|
||||
|
||||
self.onChangeDir(path)
|
||||
|
||||
def refreshCurrentDirectory(self):
|
||||
"""刷新当前目录的文件卡片"""
|
||||
logger.info(f"刷新当前目录: {self.currentPath}")
|
||||
self.loadDict(self.currentPath)
|
||||
162
app/view/widgets/page_flip_widget.py
Normal file
162
app/view/widgets/page_flip_widget.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# coding: utf-8
|
||||
# flip bookmark
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
from PyQt6.QtGui import QIntValidator
|
||||
from PyQt6.QtWidgets import QHBoxLayout
|
||||
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, LineEdit, PushButton, ToolButton)
|
||||
|
||||
|
||||
class PageFlipWidget(CardWidget):
|
||||
# 定义页码变化信号
|
||||
pageChangeSignal = pyqtSignal(int)
|
||||
|
||||
"""
|
||||
page : 总页码,默认为10
|
||||
currentPage : 当前页码
|
||||
numberButtonList : 页码按钮列表
|
||||
currentButtonNumbet : 当前显示的按钮数字列表
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, page=10):
|
||||
super().__init__(parent)
|
||||
self.page = page
|
||||
self.currentPage = 1
|
||||
self.numberButtonList = []
|
||||
self.currentButtonNumber = []
|
||||
|
||||
# 定义翻页按钮
|
||||
self.leftPageButton = ToolButton(self)
|
||||
self.rightPageButton = ToolButton(self)
|
||||
|
||||
# 定义跳转页面组件
|
||||
self.pageLineEdit = LineEdit(self)
|
||||
self.allPageLabel = BodyLabel(self)
|
||||
self.turnPageButton = PushButton(self)
|
||||
|
||||
self.__initWidget()
|
||||
# 动态设置按钮
|
||||
self._addButton()
|
||||
|
||||
def __initWidget(self):
|
||||
# 组件设置
|
||||
self.leftPageButton.setIcon(FluentIcon.PAGE_LEFT)
|
||||
self.leftPageButton.setFixedSize(40, 40)
|
||||
self.leftPageButton.clicked.connect(self.backPage)
|
||||
|
||||
self.rightPageButton.setIcon(FluentIcon.PAGE_RIGHT)
|
||||
self.rightPageButton.setFixedSize(40, 40)
|
||||
self.rightPageButton.clicked.connect(self.forwardPage)
|
||||
|
||||
self.pageLineEdit.setText("1")
|
||||
self.pageLineEdit.setValidator(QIntValidator())
|
||||
self.pageLineEdit.editingFinished.connect(self._validator)
|
||||
self.pageLineEdit.setFixedWidth(45)
|
||||
|
||||
self.allPageLabel.setText(f"/{self.page}")
|
||||
|
||||
self.turnPageButton.setText(self.tr("jump"))
|
||||
self.turnPageButton.setFixedWidth(60)
|
||||
self.turnPageButton.clicked.connect(self.turnPage)
|
||||
|
||||
self.__initLayout()
|
||||
|
||||
def __initLayout(self):
|
||||
# 布局设置
|
||||
self.layouts = QHBoxLayout(self)
|
||||
|
||||
self.layouts.addWidget(self.leftPageButton)
|
||||
self.layouts.addWidget(self.rightPageButton)
|
||||
|
||||
self.layouts.addSpacing(30)
|
||||
self.layouts.addWidget(self.pageLineEdit)
|
||||
self.layouts.addWidget(self.allPageLabel)
|
||||
self.layouts.addWidget(self.turnPageButton)
|
||||
|
||||
# 向后翻页
|
||||
def forwardPage(self):
|
||||
if self.page > 5:
|
||||
if int(self.numberButtonList[-1].text()) < self.page:
|
||||
for i in self.numberButtonList:
|
||||
i.setText(str(int(i.text()) + 1))
|
||||
|
||||
for i in range(len(self.currentButtonNumber)):
|
||||
self.currentButtonNumber[i] += 1
|
||||
if self.currentPage < self.page:
|
||||
self.currentPage += 1
|
||||
self.pageLineEdit.setText(str(self.currentPage))
|
||||
self.pageChangeSignal.emit(self.currentPage)
|
||||
|
||||
# 向前翻页
|
||||
def backPage(self):
|
||||
if int(self.numberButtonList[0].text()) > 1:
|
||||
for i in self.numberButtonList:
|
||||
i.setText(str(int(i.text()) - 1))
|
||||
|
||||
for i in range(len(self.currentButtonNumber)):
|
||||
self.currentButtonNumber[i] -= 1
|
||||
if self.currentPage > 1:
|
||||
self.currentPage -= 1
|
||||
self.pageLineEdit.setText(str(self.currentPage))
|
||||
self.pageChangeSignal.emit(self.currentPage)
|
||||
|
||||
# 跳转页面
|
||||
def turnPage(self, page: int):
|
||||
"""
|
||||
page : 目标跳转页
|
||||
"""
|
||||
page = int(self.pageLineEdit.text())
|
||||
numberList = [1, 2, 3, 4, 5]
|
||||
if page not in numberList:
|
||||
|
||||
while True:
|
||||
numberList = [x + 1 for x in numberList]
|
||||
if page in numberList and max(numberList) <= self.page:
|
||||
break
|
||||
|
||||
self.currentButtonNumber = numberList
|
||||
|
||||
for i in self.numberButtonList:
|
||||
i.setText(str(self.currentButtonNumber[self.numberButtonList.index(i)]))
|
||||
self.pageChangeSignal.emit(page)
|
||||
self.currentPage = page
|
||||
|
||||
# 输入框判断器,规定只可输入数字,并且数字不能超过规定范围
|
||||
def _validator(self):
|
||||
page = int(self.pageLineEdit.text())
|
||||
if page <= 0:
|
||||
page = 1
|
||||
elif page > self.page:
|
||||
page = self.page
|
||||
self.pageLineEdit.setText(str(page))
|
||||
|
||||
# 动态添加按钮
|
||||
def _addButton(self):
|
||||
if self.page >= 5:
|
||||
for i in range(1, 6):
|
||||
numberButton = PushButton(str(i), self)
|
||||
numberButton.setFixedSize(40, 40)
|
||||
numberButton.clicked.connect(self._pageChanged)
|
||||
|
||||
self.layouts.insertWidget(i, numberButton)
|
||||
self.numberButtonList.append(numberButton)
|
||||
self.currentButtonNumber = [1, 2, 3, 4, 5]
|
||||
|
||||
else:
|
||||
for i in range(1, self.page + 1):
|
||||
numberButton = PushButton(str(i), self)
|
||||
numberButton.setFixedSize(40, 40)
|
||||
numberButton.clicked.connect(self._pageChanged)
|
||||
|
||||
self.layouts.insertWidget(i, numberButton)
|
||||
self.numberButtonList.append(numberButton)
|
||||
self.currentButtonNumber.append(i)
|
||||
|
||||
# 页面翻页时发出信号
|
||||
def _pageChanged(self, checked):
|
||||
sender = self.sender()
|
||||
if sender:
|
||||
button_text = sender.text()
|
||||
self.pageChangeSignal.emit(int(button_text))
|
||||
self.pageLineEdit.setText(button_text)
|
||||
self.currentPage = int(button_text)
|
||||
165
app/view/widgets/policy_messageBox.py
Normal file
165
app/view/widgets/policy_messageBox.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# coding: utf-8
|
||||
import logging
|
||||
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtWidgets import QListWidgetItem
|
||||
from qfluentwidgets import (
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
ListWidget,
|
||||
MessageBoxBase,
|
||||
SubtitleLabel,
|
||||
)
|
||||
|
||||
from app.core import ChangePolicyThread, GetPoliciesThread, policyConfig, signalBus
|
||||
|
||||
|
||||
class PolicyChooseMessageBox(MessageBoxBase):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.currentPath = "/"
|
||||
self.policyDict = {}
|
||||
self.isLoading = True
|
||||
self.originalTitle = "选择存储策略"
|
||||
self.setClosableOnMaskClicked(True)
|
||||
|
||||
self.setupUI()
|
||||
|
||||
# 开始获取策略列表
|
||||
self.getPoliciesThread = GetPoliciesThread()
|
||||
self.getPoliciesThread.successGetSignal.connect(self.refreshPolicyList)
|
||||
self.getPoliciesThread.errorSignal.connect(self._handleGetPoliciesError)
|
||||
self.getPoliciesThread.start()
|
||||
|
||||
# 初始化更改策略线程(但不启动)
|
||||
self.changePolicyThread = None
|
||||
|
||||
def setupUI(self):
|
||||
"""设置UI界面"""
|
||||
self.titleLabel = SubtitleLabel(self.originalTitle, self)
|
||||
self.policyListWidget = ListWidget(self)
|
||||
|
||||
# 添加加载提示
|
||||
self.loadingItem = QListWidgetItem("正在加载策略列表...")
|
||||
self.policyListWidget.addItem(self.loadingItem)
|
||||
|
||||
self.viewLayout.addWidget(self.titleLabel)
|
||||
self.viewLayout.addWidget(self.policyListWidget)
|
||||
|
||||
# 隐藏确定取消按钮组
|
||||
self.buttonGroup.hide()
|
||||
|
||||
def connectSignals(self):
|
||||
"""连接信号与槽"""
|
||||
self.policyListWidget.currentTextChanged.connect(self.onPolicyChanged)
|
||||
self.policyListWidget.itemClicked.connect(self.selfClicked)
|
||||
|
||||
def selfClicked(self, listWidget: QListWidgetItem):
|
||||
if listWidget.text() == policyConfig.returnPolicy()["name"]:
|
||||
QTimer.singleShot(100, self.accept)
|
||||
|
||||
def onPolicyChanged(self, text):
|
||||
"""处理策略更改"""
|
||||
if not text or self.isLoading or text == "正在加载策略列表...":
|
||||
return
|
||||
|
||||
policy_id = self.policyDict.get(text)
|
||||
if not policy_id:
|
||||
return
|
||||
|
||||
# 如果已经有更改线程在运行,先停止它
|
||||
if self.changePolicyThread and self.changePolicyThread.isRunning():
|
||||
self.changePolicyThread.quit()
|
||||
self.changePolicyThread.wait()
|
||||
|
||||
# 创建并启动新的更改线程
|
||||
self.changePolicyThread = ChangePolicyThread(self.currentPath, policy_id)
|
||||
self.changePolicyThread.successChangedSignal.connect(
|
||||
self._handlePolicyChangeSuccess
|
||||
)
|
||||
self.changePolicyThread.errorSignal.connect(self._handlePolicyChangeError)
|
||||
self.changePolicyThread.start()
|
||||
|
||||
# 更新UI状态 - 只更改标题
|
||||
self._setLoadingState(True, f"正在切换到策略: {text}")
|
||||
|
||||
def refreshPolicyList(self, policiesList):
|
||||
"""刷新策略列表"""
|
||||
self.isLoading = False
|
||||
self.policyListWidget.clear()
|
||||
self.policyDict.clear()
|
||||
|
||||
currentPolicy = policyConfig.returnPolicy()
|
||||
currentIndex = 0
|
||||
|
||||
for i, policy in enumerate(policiesList):
|
||||
self.policyListWidget.addItem(QListWidgetItem(policy["name"]))
|
||||
self.policyDict[policy["name"]] = policy["id"]
|
||||
|
||||
if policy["id"] == currentPolicy["id"]:
|
||||
currentIndex = i
|
||||
|
||||
# 设置当前选中项
|
||||
if self.policyListWidget.count() > 0:
|
||||
self.policyListWidget.setCurrentRow(currentIndex)
|
||||
|
||||
self.currentPath = policyConfig.returnCurrentPath()
|
||||
self.connectSignals()
|
||||
|
||||
# 恢复原始标题
|
||||
self.titleLabel.setText(self.originalTitle)
|
||||
|
||||
def _handleGetPoliciesError(self, error_msg):
|
||||
"""处理获取策略列表错误"""
|
||||
self.policyListWidget.clear()
|
||||
errorItem = QListWidgetItem(f"加载失败: {error_msg}")
|
||||
self.policyListWidget.addItem(errorItem)
|
||||
self.isLoading = False
|
||||
# 恢复原始标题
|
||||
self.titleLabel.setText(self.originalTitle)
|
||||
|
||||
def _handlePolicyChangeSuccess(self):
|
||||
"""处理策略更改成功"""
|
||||
self._setLoadingState(False)
|
||||
|
||||
# 显示成功提示
|
||||
if self.parent():
|
||||
InfoBar.success(
|
||||
title="操作成功",
|
||||
content="存储策略已成功更改",
|
||||
orient=Qt.Orientation.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=2000,
|
||||
parent=self.window(),
|
||||
)
|
||||
signalBus.refreshFolderListSignal.emit()
|
||||
QTimer.singleShot(1000, self.accept)
|
||||
|
||||
def _handlePolicyChangeError(self, error_msg):
|
||||
"""处理策略更改错误"""
|
||||
self._setLoadingState(False)
|
||||
|
||||
# 显示错误提示
|
||||
if self.parent():
|
||||
InfoBar.error(
|
||||
title="操作失败",
|
||||
content=f"更改策略时出错: {error_msg}",
|
||||
orient=Qt.Orientation.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=3000,
|
||||
parent=self.window(),
|
||||
)
|
||||
QTimer.singleShot(1000, self.reject)
|
||||
|
||||
def _setLoadingState(self, loading, message=None):
|
||||
"""设置加载状态"""
|
||||
if loading:
|
||||
self.policyListWidget.setEnabled(False)
|
||||
if message:
|
||||
logging.info(message)
|
||||
else:
|
||||
self.policyListWidget.setEnabled(True)
|
||||
# 恢复原始标题
|
||||
self.titleLabel.setText(self.originalTitle)
|
||||
292
app/view/widgets/preview_box.py
Normal file
292
app/view/widgets/preview_box.py
Normal file
@@ -0,0 +1,292 @@
|
||||
# coding: utf-8
|
||||
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from qfluentwidgets import (
|
||||
ImageLabel,
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
IndeterminateProgressBar,
|
||||
MessageBoxBase,
|
||||
PlainTextEdit,
|
||||
PushButton,
|
||||
)
|
||||
|
||||
from app.core import (ImageLoaderThread, TextLoaderThread, UpdateFileContentThread)
|
||||
from app.core.services.text_speech import LocalSpeechController
|
||||
from app.view.components.empty_card import EmptyCard
|
||||
|
||||
|
||||
# 图片预览类
|
||||
|
||||
|
||||
def createThumbnail(pixmap, max_size=200):
|
||||
"""创建快速缩略图"""
|
||||
if pixmap.isNull():
|
||||
return pixmap
|
||||
|
||||
# 使用快速缩放算法
|
||||
return pixmap.scaled(
|
||||
max_size,
|
||||
max_size,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.FastTransformation,
|
||||
)
|
||||
|
||||
|
||||
class OptimizedPreviewBox(MessageBoxBase):
|
||||
def __init__(self, parent=None, url=None):
|
||||
super().__init__(parent=parent)
|
||||
self.widget.setMinimumSize(500, 500)
|
||||
|
||||
self.original_pixmap = None
|
||||
self.current_scale = 1.0
|
||||
|
||||
# 加载状态显示
|
||||
self.loadingCard = EmptyCard(self)
|
||||
self.loadingCard.load()
|
||||
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 图片显示标签
|
||||
self.previewLabel = ImageLabel(self)
|
||||
self.previewLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
|
||||
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 使用优化的图片加载线程
|
||||
self.imageLoaderThread = ImageLoaderThread(url)
|
||||
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
|
||||
self.imageLoaderThread.errorOccurred.connect(self.handleError)
|
||||
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
|
||||
|
||||
# 延迟启动加载,避免阻塞UI初始化
|
||||
from PyQt6.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(100, self.startLoading)
|
||||
|
||||
def startLoading(self):
|
||||
"""开始加载图片"""
|
||||
self.imageLoaderThread.start()
|
||||
|
||||
def updateProgress(self, progress):
|
||||
"""更新加载进度"""
|
||||
self.loadingCard.setText(f"正在加载图片... {progress}%")
|
||||
|
||||
def setPreviewImg(self, img: QPixmap):
|
||||
"""设置预览图片"""
|
||||
self.loadingCard.hide()
|
||||
self.original_pixmap = img
|
||||
|
||||
# 立即显示缩略图
|
||||
thumbnail = createThumbnail(img)
|
||||
self.previewLabel.setPixmap(thumbnail)
|
||||
|
||||
# 然后异步加载高质量版本
|
||||
self.adjustImageSize()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""重写窗口大小改变事件"""
|
||||
super().resizeEvent(event)
|
||||
if self.original_pixmap and not self.original_pixmap.isNull():
|
||||
# 使用定时器延迟调整,避免频繁调整
|
||||
from PyQt6.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(50, self.adjustImageSize)
|
||||
|
||||
def adjustImageSize(self):
|
||||
"""根据窗口大小动态调整图片尺寸"""
|
||||
if not self.original_pixmap or self.original_pixmap.isNull():
|
||||
return
|
||||
|
||||
# 获取可用显示区域大小
|
||||
margin = 80
|
||||
available_width = self.width() - margin * 2
|
||||
available_height = self.height() - margin * 2
|
||||
|
||||
# 获取原始图片尺寸
|
||||
original_width = self.original_pixmap.width()
|
||||
original_height = self.original_pixmap.height()
|
||||
|
||||
# 计算缩放比例
|
||||
width_ratio = available_width / original_width
|
||||
height_ratio = available_height / original_height
|
||||
scale_ratio = min(width_ratio, height_ratio, 1.0)
|
||||
|
||||
# 只在需要时重新缩放
|
||||
if abs(scale_ratio - self.current_scale) > 0.05: # 变化超过5%才重新缩放
|
||||
self.current_scale = scale_ratio
|
||||
new_width = int(original_width * scale_ratio)
|
||||
new_height = int(original_height * scale_ratio)
|
||||
|
||||
# 使用平滑缩放
|
||||
scaled_pixmap = self.original_pixmap.scaled(
|
||||
new_width,
|
||||
new_height,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation,
|
||||
)
|
||||
|
||||
self.previewLabel.setPixmap(scaled_pixmap)
|
||||
|
||||
def handleError(self, msg):
|
||||
"""处理加载错误"""
|
||||
self.loadingCard.error()
|
||||
self.previewLabel.hide()
|
||||
|
||||
|
||||
# 文本文档预览类
|
||||
class PreviewTextBox(MessageBoxBase):
|
||||
"""文本预览对话框"""
|
||||
|
||||
def __init__(self, parent=None, url=None, _id=None):
|
||||
super().__init__(parent=parent)
|
||||
self.updateTxtThread = None
|
||||
self.widget.setMinimumSize(600, 400)
|
||||
self._id = _id
|
||||
self.isChanged = False
|
||||
self.speech_controller = LocalSpeechController(self)
|
||||
|
||||
self.textSpeakButton = PushButton("朗读文本", self)
|
||||
self.textSpeakButton.hide()
|
||||
self.isSpeaking = False
|
||||
self.textSpeakButton.clicked.connect(self.playTextSpeech)
|
||||
self.viewLayout.addWidget(
|
||||
self.textSpeakButton,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
||||
)
|
||||
|
||||
# 创建文本编辑框
|
||||
self.textEdit = PlainTextEdit(self)
|
||||
self.textEdit.hide()
|
||||
self.textEdit.setLineWrapMode(PlainTextEdit.LineWrapMode.NoWrap) # 不自动换行
|
||||
|
||||
# 设置等宽字体,便于阅读代码或日志
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
font = QFont("微软雅黑", 10) # 等宽字体
|
||||
self.textEdit.setFont(font)
|
||||
|
||||
self.viewLayout.addWidget(self.textEdit)
|
||||
|
||||
# 加载状态显示
|
||||
self.loadingCard = EmptyCard(self)
|
||||
self.loadingCard.load()
|
||||
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 使用文本加载线程
|
||||
self.textLoaderThread = TextLoaderThread(url)
|
||||
self.textLoaderThread.textLoaded.connect(self.setTextContent)
|
||||
self.textLoaderThread.errorOccurred.connect(self.handleError)
|
||||
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
|
||||
|
||||
self.yesButton.hide()
|
||||
# 创建保存按钮
|
||||
self.saveButton = PushButton("保存修改", self)
|
||||
# 创建进度条
|
||||
self.saveProgressBar = IndeterminateProgressBar(self)
|
||||
self.saveProgressBar.setFixedHeight(4)
|
||||
self.saveProgressBar.hide()
|
||||
|
||||
# 添加按钮和进度条到布局
|
||||
self.buttonLayout.insertWidget(
|
||||
0, self.saveButton, 1, Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.buttonLayout.insertWidget(
|
||||
1, self.saveProgressBar, 1, Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
|
||||
self.saveButton.setEnabled(False)
|
||||
self.saveButton.clicked.connect(self.saveText)
|
||||
self.cancelButton.setText("返回")
|
||||
|
||||
# 延迟启动加载,避免阻塞UI初始化
|
||||
QTimer.singleShot(100, self.startLoading)
|
||||
|
||||
def saveText(self):
|
||||
logger.info("保存用户修改")
|
||||
# 显示进度条并禁用按钮
|
||||
self.saveProgressBar.show()
|
||||
self.saveButton.setEnabled(False)
|
||||
self.saveTextThread = UpdateFileContentThread(
|
||||
self._id,
|
||||
self.textEdit.toPlainText(),
|
||||
)
|
||||
self.saveTextThread.successUpdated.connect(self._successSave)
|
||||
self.saveTextThread.errorUpdated.connect(self._errorSave)
|
||||
self.saveTextThread.start()
|
||||
|
||||
def _successSave(self):
|
||||
InfoBar.success(
|
||||
"成功",
|
||||
"修改保存成功",
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
# 隐藏进度条
|
||||
self.saveProgressBar.hide()
|
||||
QTimer.singleShot(700, self.accept)
|
||||
|
||||
def _errorSave(self, msg):
|
||||
InfoBar.error(
|
||||
"失败",
|
||||
msg,
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
# 隐藏进度条并重新启用按钮
|
||||
self.saveProgressBar.hide()
|
||||
self.saveButton.setEnabled(True)
|
||||
|
||||
def playTextSpeech(self):
|
||||
"""播放文本语音"""
|
||||
if not self.isSpeaking:
|
||||
text = self.textEdit.toPlainText()
|
||||
if text and len(text.strip()) > 0:
|
||||
self.speech_controller.play_text(text)
|
||||
self.isSpeaking = True
|
||||
self.textSpeakButton.setText("暂停朗读")
|
||||
else:
|
||||
self.speech_controller.stop_playback()
|
||||
self.isSpeaking = False
|
||||
self.textSpeakButton.setText("朗读文本")
|
||||
|
||||
def startLoading(self):
|
||||
"""开始加载文本"""
|
||||
self.textLoaderThread.start()
|
||||
|
||||
def updateProgress(self, progress):
|
||||
"""更新加载进度"""
|
||||
self.loadingCard.setText(f"正在加载文本... {progress}%")
|
||||
|
||||
def setTextContent(self, content):
|
||||
"""设置文本内容"""
|
||||
self.loadingCard.hide()
|
||||
self.textEdit.show()
|
||||
self.textSpeakButton.show()
|
||||
self.saveButton.setEnabled(True)
|
||||
# 限制显示的内容长度,避免性能问题
|
||||
max_display_length = 100000 # 最多显示10万个字符
|
||||
if len(content) > max_display_length:
|
||||
content = (
|
||||
content[:max_display_length]
|
||||
+ f"\n\n... (内容过长,已截断前{max_display_length}个字符,完整内容请下载文件查看)"
|
||||
)
|
||||
|
||||
self.textEdit.setPlainText(content)
|
||||
|
||||
def handleError(self, error_msg):
|
||||
"""处理加载错误"""
|
||||
self.loadingCard.error()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""重写窗口大小改变事件"""
|
||||
super().resizeEvent(event)
|
||||
# 文本预览框会自动适应大小,无需特殊处理
|
||||
86
app/view/widgets/register_widget.py
Normal file
86
app/view/widgets/register_widget.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import QRegularExpression, Qt
|
||||
from PyQt6.QtGui import (
|
||||
QRegularExpressionValidator,
|
||||
)
|
||||
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
from qfluentwidgets import (
|
||||
LineEdit,
|
||||
PasswordLineEdit,
|
||||
PrimaryPushButton,
|
||||
)
|
||||
|
||||
from app.core import CaptchaThread
|
||||
|
||||
|
||||
class RegisterWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
logger.debug("初始化注册组件")
|
||||
super().__init__(parent)
|
||||
self.setObjectName("RegisterWidget")
|
||||
|
||||
self.emailLineEdit = LineEdit(self)
|
||||
self.emailLineEdit.setPlaceholderText("请输入注册邮箱")
|
||||
self.passwordLineEdit = PasswordLineEdit(self)
|
||||
self.passwordLineEdit.setPlaceholderText("请输入密码")
|
||||
self.confirmPasswordLineEdit = PasswordLineEdit(self)
|
||||
self.confirmPasswordLineEdit.setPlaceholderText("请确认您的密码")
|
||||
|
||||
self.verificationCodeLabel = QLabel(self)
|
||||
self.verificationCodeLabel.setFixedSize(120, 35)
|
||||
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
|
||||
self.verificationCodeLabel.mousePressEvent = (
|
||||
self.refreshVerificationCode
|
||||
) # 绑定点击事件
|
||||
self.verificationCodeLineEdit = LineEdit(self)
|
||||
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
|
||||
|
||||
self.verificationLayout = QHBoxLayout()
|
||||
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
|
||||
self.verificationLayout.addWidget(self.verificationCodeLabel)
|
||||
|
||||
self.registerButton = PrimaryPushButton("注册", self)
|
||||
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.vBoxLayout.setSpacing(15)
|
||||
self.vBoxLayout.addSpacing(15)
|
||||
self.vBoxLayout.addWidget(self.emailLineEdit)
|
||||
self.vBoxLayout.addWidget(self.passwordLineEdit)
|
||||
self.vBoxLayout.addWidget(self.confirmPasswordLineEdit)
|
||||
self.vBoxLayout.addLayout(self.verificationLayout)
|
||||
self.vBoxLayout.addWidget(self.registerButton)
|
||||
|
||||
email_regex = QRegularExpression(
|
||||
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
||||
)
|
||||
validator = QRegularExpressionValidator(email_regex, self)
|
||||
self.emailLineEdit.setValidator(validator)
|
||||
self.emailLineEdit.textChanged.connect(self.checkeEmail)
|
||||
logger.debug("注册组件初始化完成")
|
||||
self.refreshVerificationCode()
|
||||
|
||||
def checkeEmail(self, text):
|
||||
# 检查当前输入是否通过验证器
|
||||
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
|
||||
if state == QRegularExpressionValidator.Acceptable:
|
||||
logger.debug("注册邮箱格式验证通过")
|
||||
self.registerButton.setDisabled(False)
|
||||
else:
|
||||
self.registerButton.setDisabled(True)
|
||||
|
||||
def refreshVerificationCode(self, event=None):
|
||||
logger.debug("刷新验证码")
|
||||
self.captchaThread = CaptchaThread()
|
||||
self.captchaThread.captchaReady.connect(self._showVerificationCode)
|
||||
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
|
||||
self.captchaThread.start()
|
||||
|
||||
def _showVerificationCode(self, pixmap):
|
||||
logger.debug("显示验证码")
|
||||
self.verificationCodeLabel.setPixmap(pixmap)
|
||||
|
||||
def _showCaptchaFailed(self, message):
|
||||
logger.debug(f"验证码刷新失败:{message}")
|
||||
self.verificationCodeLineEdit.clear()
|
||||
147
app/view/widgets/share_file_messageBox.py
Normal file
147
app/view/widgets/share_file_messageBox.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# coding: utf-8
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, QTimer, QUrl
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
||||
from PyQt6.QtWidgets import QHBoxLayout
|
||||
from qfluentwidgets import (BodyLabel, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, MessageBoxBase,
|
||||
SubtitleLabel)
|
||||
|
||||
from app.core import formatDate, formatSize, GetShareFileInfoThread, signalBus
|
||||
|
||||
|
||||
class ShareFileMessageBox(MessageBoxBase):
|
||||
def __init__(self, _id, fileIcon=None, suffix="", parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.widget.setFixedWidth(350)
|
||||
self.suffix = suffix
|
||||
self._id = _id
|
||||
|
||||
self.fileTypeImageLabel = ImageLabel(parent=self)
|
||||
self.fileTypeImageLabel.setImage(fileIcon)
|
||||
self.fileTypeImageLabel.scaledToHeight(60)
|
||||
self.fileTypeImageLabel.scaledToWidth(60)
|
||||
|
||||
self.fileNameLabel = SubtitleLabel(parent=self)
|
||||
self.fileSizeLabel = BodyLabel(parent=self)
|
||||
|
||||
self.fileInformationLabel = BodyLabel(parent=self)
|
||||
self.fileInformationLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
self.userImageLabel = ImageLabel(":app/images/logo.png", parent=self)
|
||||
self.userImageLabel.setBorderRadius(20, 20, 20, 20)
|
||||
self.userImageLabel.setFixedSize(30, 30)
|
||||
self.userImageLabel.scaledToHeight(30)
|
||||
self.userImageLabel.scaledToWidth(30)
|
||||
|
||||
self.userNameLabel = SubtitleLabel(parent=self)
|
||||
self.userLayout = QHBoxLayout()
|
||||
self.userLayout.addWidget(self.userImageLabel)
|
||||
self.userLayout.addWidget(self.userNameLabel)
|
||||
|
||||
self.viewLayout.addWidget(
|
||||
self.fileTypeImageLabel,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
self.viewLayout.addWidget(
|
||||
self.fileNameLabel,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
self.viewLayout.addWidget(
|
||||
self.fileSizeLabel,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
self.viewLayout.addWidget(
|
||||
self.fileInformationLabel,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
self.viewLayout.addWidget(HorizontalSeparator(parent=self))
|
||||
self.viewLayout.addLayout(self.userLayout)
|
||||
|
||||
self.yesButton.setText("下载")
|
||||
self.yesButton.clicked.connect(self.downloadFile)
|
||||
self.cancelButton.setText("取消")
|
||||
|
||||
self.apiWorker = GetShareFileInfoThread(_id)
|
||||
self.apiWorker.shareFileInfoSignal.connect(self.handleApiResponse)
|
||||
self.apiWorker.errorSignal.connect(self.handleError)
|
||||
self.apiWorker.start()
|
||||
|
||||
self.networkManager = QNetworkAccessManager(self)
|
||||
self.networkManager.finished.connect(self.onAvatarDownloaded)
|
||||
|
||||
def downloadFile(self):
|
||||
signalBus.addDownloadFileTask.emit(
|
||||
f"share.{self.suffix}",
|
||||
self.fileNameLabel.text(),
|
||||
f"undefined/undefined.{self._id}",
|
||||
)
|
||||
self.accept()
|
||||
|
||||
def handleApiResponse(self, response_data):
|
||||
response_data = response_data["data"]
|
||||
self.fileNameLabel.setText(response_data["source"]["name"])
|
||||
self.fileSizeLabel.setText(
|
||||
f"大小: {formatSize(response_data['source']['size'])}"
|
||||
)
|
||||
|
||||
infoLabel = f"创建时间: {formatDate(response_data['create_date'])}\n浏览次数: {response_data['views']}\n下载次数: {response_data['downloads']}"
|
||||
self.fileInformationLabel.setText(infoLabel)
|
||||
|
||||
self.userNameLabel.setText(response_data["creator"]["nick"])
|
||||
self.loadAvatarFromId(response_data["creator"]["key"])
|
||||
|
||||
def handleError(self, msg):
|
||||
InfoBar.error(
|
||||
"失败",
|
||||
msg,
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
QTimer.singleShot(1000, self.accept)
|
||||
|
||||
def loadAvatarFromId(self, _id):
|
||||
"""从网络URL加载头像"""
|
||||
# 使用V4 API获取头像 - 假设格式变为/api/v4/user/avatar/{_id}/l
|
||||
url = f"/user/avatar/{_id}/l"
|
||||
logger.info(f"开始从网络加载头像")
|
||||
request = QNetworkRequest(QUrl(url))
|
||||
self.networkManager.get(request)
|
||||
|
||||
def onAvatarDownloaded(self, reply):
|
||||
"""处理头像下载完成"""
|
||||
if reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
# 读取下载的数据
|
||||
data = reply.readAll()
|
||||
# 创建QPixmap并加载数据
|
||||
pixmap = QPixmap()
|
||||
if pixmap.loadFromData(data):
|
||||
# 更新头像
|
||||
self.userImageLabel.setImage(pixmap)
|
||||
logger.info("网络头像加载成功")
|
||||
else:
|
||||
logger.error("头像数据格式不支持")
|
||||
else:
|
||||
logger.error(f"头像下载失败: {reply.errorString()}")
|
||||
pixmap = QPixmap(":app/images/logo.png")
|
||||
self.userImageLabel.setImage(pixmap)
|
||||
logger.info("使用默认头像")
|
||||
self.userImageLabel.scaledToHeight(30)
|
||||
self.userImageLabel.scaledToWidth(30)
|
||||
|
||||
reply.deleteLater()
|
||||
|
||||
def format_size(self, size):
|
||||
"""格式化文件大小"""
|
||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||
if size < 1024:
|
||||
return f"{size:.2f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.2f} PB"
|
||||
463
app/view/widgets/share_folder_messageBox.py
Normal file
463
app/view/widgets/share_folder_messageBox.py
Normal file
@@ -0,0 +1,463 @@
|
||||
# coding: utf-8
|
||||
|
||||
# 导入loguru日志库
|
||||
from datetime import datetime
|
||||
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, QThread, QUrl, pyqtSignal
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
||||
from PyQt6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from qfluentwidgets import Action, BodyLabel, BreadcrumbBar, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, \
|
||||
MenuAnimationType, MessageBoxBase, RoundMenu, ScrollArea, setFont, SubtitleLabel
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
|
||||
from app.core import lang
|
||||
from app.core import miaoStarsBasicApi
|
||||
|
||||
from app.core import signalBus
|
||||
from app.view.components.file_card import SharedFolderFileCard
|
||||
|
||||
# 使用miaoStarsBasicApi中已经配置好的V4 API
|
||||
UserSession = miaoStarsBasicApi.returnSession()
|
||||
|
||||
|
||||
class APIWorker(QThread):
|
||||
"""API 工作线程"""
|
||||
|
||||
finished = pyqtSignal(object, str) # 信号:传递响应数据和错误信息
|
||||
|
||||
def __init__(self, url_path, method="GET", data=None):
|
||||
super().__init__()
|
||||
self.url_path = url_path # 相对路径,如 "/share/list/{id}/path"
|
||||
self.method = method
|
||||
self.data = data
|
||||
logger.debug(f"初始化API工作线程,路径: {url_path}, 方法: {method}")
|
||||
|
||||
def run(self):
|
||||
"""执行API请求,使用miaoStarsBasicApi中已配置好的Cloudreve V4 API"""
|
||||
try:
|
||||
logger.info(f"开始API请求")
|
||||
|
||||
# 使用miaoStarsBasicApi进行请求
|
||||
if self.method == "GET":
|
||||
response = miaoStarsBasicApi.request(method="GET", url=self.url_path)
|
||||
else:
|
||||
response = miaoStarsBasicApi.request(method=self.method, url=self.url_path, json=self.data)
|
||||
|
||||
# Cloudreve V4 API 返回的是处理后的结果,不需要再解析响应
|
||||
logger.success(f"API请求成功")
|
||||
# 由于miaoStarsBasicApi.request已经处理了错误,直接返回结果
|
||||
if isinstance(response, dict) and "code" in response and response["code"] == 0:
|
||||
self.finished.emit(response, "")
|
||||
else:
|
||||
error_msg = response.get("msg", "请求失败") if isinstance(response, dict) else "请求失败"
|
||||
logger.error(f"API请求失败, 错误: {error_msg}")
|
||||
self.finished.emit(None, error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"请求异常: {str(e)}"
|
||||
logger.exception(f"API请求异常, 异常信息: {error_msg}")
|
||||
self.finished.emit(None, error_msg)
|
||||
|
||||
|
||||
class LinkageSwitching(ScrollArea):
|
||||
"""文件卡片滚动区域组件"""
|
||||
|
||||
# 信号:文件卡片相关操作时发出
|
||||
fileActionRequested = pyqtSignal(str, str) # (actionName, fileId)
|
||||
|
||||
def __init__(self, _id, paths, breadcrumbBar, parent=None):
|
||||
super().__init__(parent)
|
||||
self.paths = paths
|
||||
self._id = _id
|
||||
self.currentPath = paths
|
||||
self.breadcrumbBar = breadcrumbBar
|
||||
self.fileCardsDict = {} # 存储所有文件卡片
|
||||
|
||||
logger.debug(f"初始化文件卡片滚动区域,路径: {paths}")
|
||||
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
"""初始化UI"""
|
||||
self.widgets = QWidget()
|
||||
self.layouts = QVBoxLayout(self.widgets)
|
||||
self.layouts.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.setWidget(self.widgets)
|
||||
self.setWidgetResizable(True)
|
||||
|
||||
# 设置布局属性
|
||||
self.layouts.setContentsMargins(5, 5, 5, 0)
|
||||
self.layouts.setSpacing(5)
|
||||
|
||||
# 设置样式
|
||||
self.widgets.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.setStyleSheet("background-color: transparent; border: none;")
|
||||
|
||||
# 设置滚动策略
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
|
||||
self._onLoadDict("/")
|
||||
|
||||
def addFileCard(self, fileId, key, obj):
|
||||
"""添加文件卡片
|
||||
|
||||
Args:
|
||||
fileId: 文件的唯一标识符
|
||||
obj: 文件数据对象
|
||||
|
||||
Returns:
|
||||
创建的文件卡片对象
|
||||
"""
|
||||
if fileId in self.fileCardsDict:
|
||||
logger.warning(f"文件卡片已存在: {fileId}")
|
||||
return self.fileCardsDict[fileId]
|
||||
|
||||
# logger.debug(f"添加文件卡片: {fileId} - {obj.get('name', '未知')}")
|
||||
fileCard = SharedFolderFileCard(
|
||||
key,
|
||||
obj["id"],
|
||||
obj["name"],
|
||||
obj["type"],
|
||||
obj["path"],
|
||||
obj["date"],
|
||||
obj["size"],
|
||||
self,
|
||||
)
|
||||
|
||||
fileCard.setObjectName(f"fileCard_{fileId}")
|
||||
|
||||
self.fileCardsDict[fileId] = fileCard
|
||||
self.layouts.addWidget(fileCard)
|
||||
|
||||
return fileCard
|
||||
|
||||
def removeFileCard(self, fileId):
|
||||
"""移除文件卡片"""
|
||||
if fileId in self.fileCardsDict:
|
||||
# logger.debug(f"移除文件卡片: {fileId}")
|
||||
fileCard = self.fileCardsDict[fileId]
|
||||
self.layouts.removeWidget(fileCard)
|
||||
fileCard.deleteLater()
|
||||
del self.fileCardsDict[fileId]
|
||||
else:
|
||||
logger.warning(f"尝试移除不存在的文件卡片: {fileId}")
|
||||
|
||||
def clearFileCards(self):
|
||||
"""清除所有文件卡片"""
|
||||
logger.debug("清除所有文件卡片")
|
||||
fileIds = list(self.fileCardsDict.keys())
|
||||
for fileId in fileIds:
|
||||
self.removeFileCard(fileId)
|
||||
|
||||
def contextMenuEvent(self, e):
|
||||
"""重写上下文菜单事件"""
|
||||
logger.debug("触发上下文菜单事件")
|
||||
menu = RoundMenu(parent=self)
|
||||
# 添加操作
|
||||
|
||||
menu.addAction(
|
||||
Action(FIF.SYNC, lang("刷新当前"), triggered=self._refreshFolderList)
|
||||
)
|
||||
menu.addSeparator()
|
||||
|
||||
# 显示菜单
|
||||
menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
|
||||
|
||||
def _refreshFolderList(self):
|
||||
logger.debug("刷新文件夹列表")
|
||||
InfoBar.success(
|
||||
"成功",
|
||||
"刷新成功",
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.parent(),
|
||||
)
|
||||
|
||||
def _onLoadDict(self, paths):
|
||||
"""加载目录数据"""
|
||||
logger.info(f"加载目录数据: {paths}")
|
||||
self.currentPath = paths
|
||||
# 使用Cloudreve V4 API的分享列表路径
|
||||
# 注意:V4 API中分享列表可能使用不同的路径和参数格式
|
||||
# 这里假设路径为 /share/list/{share_id}?path={path}
|
||||
url_path = f"/share/list/{self._id}?path={paths}"
|
||||
self.loadDataThread = APIWorker(url_path)
|
||||
self.loadDataThread.finished.connect(self._dealDatas)
|
||||
self.loadDataThread.start()
|
||||
self.breadcrumbBar.setEnabled(False)
|
||||
|
||||
def _dealDatas(self, data, msg):
|
||||
"""处理目录数据"""
|
||||
logger.info("设置当前页策略")
|
||||
if msg:
|
||||
logger.error(f"加载目录数据失败: {msg}")
|
||||
return
|
||||
|
||||
if not data or "data" not in data or "objects" not in data["data"]:
|
||||
logger.error("目录数据格式错误")
|
||||
return
|
||||
|
||||
logger.info(f"成功加载目录数据,对象数量: {len(data['data']['objects'])}")
|
||||
self.objects = data["data"]["objects"]
|
||||
self.clearFileCards()
|
||||
self.breadcrumbBar.setEnabled(True)
|
||||
for obj in self.objects:
|
||||
try:
|
||||
self.addFileCard(obj["id"], obj["key"], obj)
|
||||
except:
|
||||
self.addFileCard(obj["id"], obj["id"], obj)
|
||||
|
||||
|
||||
|
||||
|
||||
class BasicInfoThread(QThread):
|
||||
"""API 工作线程"""
|
||||
|
||||
finished = pyqtSignal(object, str) # 信号:传递响应数据和错误信息
|
||||
|
||||
def __init__(self, _id):
|
||||
super().__init__()
|
||||
self._id = _id
|
||||
logger.debug(f"初始化API工作线程")
|
||||
|
||||
def run(self):
|
||||
"""执行API请求,使用miaoStarsBasicApi中已配置好的Cloudreve V4 API"""
|
||||
try:
|
||||
# 使用Cloudreve V4 API的URL格式
|
||||
url_path = f"/share/{self._id}"
|
||||
logger.info(f"开始API请求")
|
||||
# 使用miaoStarsBasicApi进行请求
|
||||
response = miaoStarsBasicApi.request(method="GET", url=url_path)
|
||||
|
||||
# 由于miaoStarsBasicApi.request已经处理了错误,直接返回结果
|
||||
if isinstance(response, dict) and "code" in response and response["code"] == 0:
|
||||
logger.success(f"API请求成功")
|
||||
self.finished.emit(response, "")
|
||||
else:
|
||||
error_msg = response.get("msg", "请求失败") if isinstance(response, dict) else "请求失败"
|
||||
logger.error(f"API请求失败, 错误: {error_msg}")
|
||||
self.finished.emit(None, error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"请求异常: {str(e)}"
|
||||
logger.exception(f"API请求异常, 异常信息: {error_msg}")
|
||||
self.finished.emit(None, error_msg)
|
||||
|
||||
|
||||
class ShareFolderMessageBox(MessageBoxBase):
|
||||
"""主文件管理界面"""
|
||||
|
||||
# 信号:面包屑导航项点击时发出,传递路径
|
||||
breadcrumbItemClicked = pyqtSignal(str)
|
||||
|
||||
def __init__(self, _id, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("ShareFolderMessageBox")
|
||||
self.widget.setMinimumWidth(900)
|
||||
self.widget.setMinimumHeight(600)
|
||||
self.currentPath = "/"
|
||||
self._id = _id
|
||||
|
||||
logger.debug("初始化分析文件管理")
|
||||
|
||||
self.folderTitleLabel = SubtitleLabel(self)
|
||||
self.infomationLabel = BodyLabel(self)
|
||||
|
||||
self.authorAvatar = ImageLabel(":app/images/logo.png", self)
|
||||
self.authorAvatar.setBorderRadius(20, 20, 20, 20)
|
||||
self.authorAvatar.scaledToHeight(20)
|
||||
self.authorAvatar.scaledToWidth(20)
|
||||
self.authorNameLabel = BodyLabel(self)
|
||||
|
||||
self.breadcrumbBar = BreadcrumbBar(self)
|
||||
|
||||
self.basicInfoThread = BasicInfoThread(self._id)
|
||||
self.basicInfoThread.finished.connect(self.handleApiResponse)
|
||||
self.basicInfoThread.start()
|
||||
|
||||
self.networkManager = QNetworkAccessManager(self)
|
||||
self.networkManager.finished.connect(self.onAvatarDownloaded)
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
def setupUi(self):
|
||||
"""初始化UI"""
|
||||
logger.debug("设置分析文件管理界面UI")
|
||||
|
||||
# 初始化面包屑导航
|
||||
self.breadcrumbBar.addItem("/", "/")
|
||||
setFont(self.breadcrumbBar, 18)
|
||||
self.breadcrumbBar.setSpacing(15)
|
||||
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
|
||||
|
||||
# 初始化堆叠窗口
|
||||
self.linkageSwitching = LinkageSwitching(
|
||||
self._id, "/", self.breadcrumbBar, self
|
||||
)
|
||||
# 设置主布局
|
||||
self.initLayout()
|
||||
|
||||
def initLayout(self):
|
||||
"""初始化布局"""
|
||||
self.viewLayout.setContentsMargins(10, 20, 10, 5)
|
||||
|
||||
self.viewLayout.addWidget(self.folderTitleLabel, 0, Qt.AlignmentFlag.AlignTop)
|
||||
self.viewLayout.addWidget(self.infomationLabel, 0, Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.viewLayout.addWidget(HorizontalSeparator(self))
|
||||
self.authorLayout = QHBoxLayout()
|
||||
self.authorLayout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.authorLayout.addWidget(self.authorAvatar, 0, Qt.AlignmentFlag.AlignLeft)
|
||||
self.authorLayout.addWidget(self.authorNameLabel, 0, Qt.AlignmentFlag.AlignLeft)
|
||||
self.viewLayout.addLayout(self.authorLayout)
|
||||
self.viewLayout.addWidget(HorizontalSeparator(self))
|
||||
# 添加所有组件到主布局
|
||||
self.viewLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
|
||||
self.viewLayout.addWidget(self.linkageSwitching)
|
||||
|
||||
def handleApiResponse(self, response_data):
|
||||
response_data = response_data["data"]
|
||||
self.folderTitleLabel.setText(response_data["source"]["name"])
|
||||
|
||||
infoLabel = f"创建时间: {self.format_date(response_data['create_date'])} | 浏览次数: {response_data['views']} | 下载次数: {response_data['downloads']}"
|
||||
self.infomationLabel.setText(infoLabel)
|
||||
|
||||
self.authorAvatar.setText(response_data["creator"]["nick"])
|
||||
self.authorNameLabel.setText(response_data["creator"]["nick"])
|
||||
self.loadAvatarFromId(response_data["creator"]["key"])
|
||||
|
||||
def loadAvatarFromId(self, _id):
|
||||
"""从网络URL加载头像"""
|
||||
# 使用V4 API获取头像
|
||||
url = f"/user/avatar/{_id}/l"
|
||||
logger.info(f"开始从网络加载头像")
|
||||
request = QNetworkRequest(QUrl(url))
|
||||
self.networkManager.get(request)
|
||||
|
||||
def onAvatarDownloaded(self, reply):
|
||||
"""处理头像下载完成"""
|
||||
if reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
# 读取下载的数据
|
||||
data = reply.readAll()
|
||||
# 创建QPixmap并加载数据
|
||||
pixmap = QPixmap()
|
||||
if pixmap.loadFromData(data):
|
||||
# 更新头像
|
||||
self.authorAvatar.setImage(pixmap)
|
||||
logger.info("网络头像加载成功")
|
||||
else:
|
||||
logger.error("头像数据格式不支持")
|
||||
else:
|
||||
logger.error(f"头像下载失败: {reply.errorString()}")
|
||||
pixmap = QPixmap(":app/images/logo.png")
|
||||
self.authorAvatar.setImage(pixmap)
|
||||
logger.info("使用默认头像")
|
||||
self.authorAvatar.scaledToHeight(20)
|
||||
self.authorAvatar.scaledToWidth(20)
|
||||
|
||||
reply.deleteLater()
|
||||
|
||||
def format_size(self, size):
|
||||
"""格式化文件大小"""
|
||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||
if size < 1024:
|
||||
return f"{size:.2f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.2f} PB"
|
||||
|
||||
def format_date(self, date_str):
|
||||
"""格式化日期时间"""
|
||||
try:
|
||||
# 处理带小数秒的情况
|
||||
if "." in date_str:
|
||||
# 分割日期和小数秒部分
|
||||
date_part, fractional_part = date_str.split(".", 1)
|
||||
# 去除末尾的'Z'并截取前6位小数
|
||||
fractional_sec = fractional_part.rstrip("Z")[:6]
|
||||
# 重新组合日期字符串
|
||||
normalized_date_str = f"{date_part}.{fractional_sec}Z"
|
||||
date_time = datetime.strptime(
|
||||
normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
)
|
||||
else:
|
||||
# 处理没有小数秒的情况
|
||||
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
|
||||
except ValueError:
|
||||
# 如果所有格式都失败,返回原始字符串
|
||||
return date_str
|
||||
|
||||
return date_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def refreshCurrentDirectory(self):
|
||||
"""刷新当前目录的文件卡片"""
|
||||
logger.info(f"刷新当前目录: {self.currentPath}")
|
||||
|
||||
# 重新加载当前目录数据
|
||||
self.linkageSwitching._onLoadDict(self.currentPath)
|
||||
|
||||
def clickChangeDir(self, name):
|
||||
"""处理面包屑导航项点击事件"""
|
||||
logger.info(f"面包屑导航项点击: {name}")
|
||||
|
||||
# 获取点击的路径
|
||||
if name == "":
|
||||
name = "/"
|
||||
|
||||
pathList = []
|
||||
|
||||
for item in self.breadcrumbBar.items:
|
||||
if item.text == name:
|
||||
pathList.append(item.text)
|
||||
break
|
||||
else:
|
||||
pathList.append(item.text)
|
||||
|
||||
# 清理空路径
|
||||
for i in pathList:
|
||||
if i == "":
|
||||
pathList.remove(i)
|
||||
|
||||
path = "/".join(pathList) if pathList else "/"
|
||||
path = path[1:]
|
||||
if path == "":
|
||||
path = "/"
|
||||
|
||||
if path == self.currentPath:
|
||||
logger.debug("路径未变化,跳过导航")
|
||||
return
|
||||
|
||||
self.onChangeDir(path)
|
||||
|
||||
def connectSignals(self):
|
||||
"""连接信号与槽"""
|
||||
logger.debug("连接主文件管理界面信号")
|
||||
|
||||
# 连接搜索信号
|
||||
|
||||
signalBus.shareDirOpenSignal.connect(lambda x: self.onChangeDir(x))
|
||||
# signalBus.refreshFolderListSignal.connect(self.refreshCurrentDirectory)
|
||||
signalBus.shareFileDownloadSignal.connect(self.accept)
|
||||
|
||||
def onChangeDir(self, path):
|
||||
"""处理目录变更"""
|
||||
logger.info(f"变更目录: {path}")
|
||||
self.linkageSwitching._onLoadDict(path)
|
||||
|
||||
if path == "":
|
||||
self.currentPath = "/"
|
||||
else:
|
||||
self.currentPath = path
|
||||
|
||||
# 更新面包屑导航
|
||||
display_name = path.split("/")[-1] if path != "/" else "/"
|
||||
self.breadcrumbBar.addItem(display_name, display_name)
|
||||
94
app/view/widgets/share_search_widgets.py
Normal file
94
app/view/widgets/share_search_widgets.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# coding: utf-8
|
||||
|
||||
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
|
||||
from qfluentwidgets import ComboBox, PushButton
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
|
||||
from app.view.components.linkage_switching import ShareLinkageSwitching
|
||||
from app.view.widgets.page_flip_widget import PageFlipWidget
|
||||
|
||||
|
||||
#
|
||||
|
||||
|
||||
class ShareSearchScrollWidget(QWidget):
|
||||
returnSignal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.currentKeyword = ""
|
||||
self.currentPage = 1
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
|
||||
self.topLayout = QHBoxLayout()
|
||||
self.returnButton = PushButton(
|
||||
FIF.RETURN,
|
||||
"返回",
|
||||
self,
|
||||
)
|
||||
self.searchMethod = ComboBox(self)
|
||||
self.searchMethod.addItems(["创建时间", "下载次数", "浏览次数"])
|
||||
self.searchMethod.currentTextChanged.connect(
|
||||
lambda: self.shareSearch(self.currentKeyword, self.currentPage)
|
||||
)
|
||||
self.sortMethod = ComboBox(self)
|
||||
self.sortMethod.addItems(["从大到小", "从小到大"])
|
||||
self.sortMethod.currentTextChanged.connect(
|
||||
lambda: self.shareSearch(self.currentKeyword, self.currentPage)
|
||||
)
|
||||
|
||||
self.returnButton.clicked.connect(self.clear)
|
||||
|
||||
self.searchScrolledArea = ShareLinkageSwitching(self)
|
||||
self.searchScrolledArea.totalItemsSignal.connect(self.updatePageFlip)
|
||||
|
||||
self.pageFlipWidget = None
|
||||
|
||||
self.topLayout.addWidget(self.returnButton, 0, Qt.AlignmentFlag.AlignLeft)
|
||||
self.topLayout.addWidget(self.searchMethod, 1, Qt.AlignmentFlag.AlignRight)
|
||||
self.topLayout.addWidget(self.sortMethod, 0, Qt.AlignmentFlag.AlignRight)
|
||||
self.vBoxLayout.addLayout(self.topLayout)
|
||||
self.vBoxLayout.addWidget(self.searchScrolledArea)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
QTimer.singleShot(1000, lambda: self.shareSearch("A", 1))
|
||||
|
||||
def updatePageFlip(self, total):
|
||||
if not self.pageFlipWidget:
|
||||
pages = total // 18 if total % 18 == 0 else total // 18 + 1
|
||||
self.pageFlipWidget = PageFlipWidget(self, pages)
|
||||
self.vBoxLayout.addWidget(
|
||||
self.pageFlipWidget,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
self.pageFlipWidget.pageChangeSignal.connect(
|
||||
lambda page: self.shareSearch(self.currentKeyword, page)
|
||||
)
|
||||
|
||||
def shareSearch(self, keyword, page):
|
||||
if self.currentKeyword != keyword:
|
||||
self.currentKeyword = keyword
|
||||
if self.currentPage != page:
|
||||
self.currentPage = page
|
||||
|
||||
orderByDict = {
|
||||
"创建时间": "created_at",
|
||||
"下载次数": "downloads",
|
||||
"浏览次数": "views",
|
||||
}
|
||||
sortDict = {
|
||||
"从大到小": "DESC",
|
||||
"从小到大": "ASC",
|
||||
}
|
||||
self.searchScrolledArea.search(
|
||||
keyword,
|
||||
orderByDict[self.searchMethod.currentText()],
|
||||
sortDict[self.sortMethod.currentText()],
|
||||
page,
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
self.searchScrolledArea.clearFileCards()
|
||||
self.returnSignal.emit()
|
||||
66
app/view/widgets/stacked_widget.py
Normal file
66
app/view/widgets/stacked_widget.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# coding:utf-8
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QEasingCurve
|
||||
from PyQt6.QtWidgets import QFrame, QHBoxLayout, QAbstractScrollArea
|
||||
|
||||
from qfluentwidgets.components.widgets.stacked_widget import PopUpAniStackedWidget
|
||||
|
||||
|
||||
|
||||
class StackedWidget(QFrame):
|
||||
""" Stacked widget """
|
||||
|
||||
currentChanged = pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.view = PopUpAniStackedWidget(self)
|
||||
|
||||
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.hBoxLayout.addWidget(self.view)
|
||||
|
||||
self.view.currentChanged.connect(self.currentChanged)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
|
||||
|
||||
def isAnimationEnabled(self) -> bool:
|
||||
return self.view.isAnimationEnabled
|
||||
|
||||
def setAnimationEnabled(self, isEnabled: bool):
|
||||
"""set whether the pop animation is enabled"""
|
||||
self.view.setAnimationEnabled(isEnabled)
|
||||
|
||||
def addWidget(self, widget):
|
||||
""" add widget to view """
|
||||
self.view.addWidget(widget)
|
||||
|
||||
def removeWidget(self, widget):
|
||||
""" remove widget from view """
|
||||
self.view.removeWidget(widget)
|
||||
|
||||
def widget(self, index: int):
|
||||
return self.view.widget(index)
|
||||
|
||||
def setCurrentWidget(self, widget, popOut=True):
|
||||
if isinstance(widget, QAbstractScrollArea):
|
||||
widget.verticalScrollBar().setValue(0)
|
||||
|
||||
if not popOut:
|
||||
self.view.setCurrentWidget(widget, duration=300)
|
||||
else:
|
||||
self.view.setCurrentWidget(
|
||||
widget, True, False, 200, QEasingCurve.Type.InQuad)
|
||||
|
||||
def setCurrentIndex(self, index, popOut=True):
|
||||
self.setCurrentWidget(self.view.widget(index), popOut)
|
||||
|
||||
def currentIndex(self):
|
||||
return self.view.currentIndex()
|
||||
|
||||
def currentWidget(self):
|
||||
return self.view.currentWidget()
|
||||
|
||||
def indexOf(self, widget):
|
||||
return self.view.indexOf(widget)
|
||||
|
||||
def count(self):
|
||||
return self.view.count()
|
||||
38
app/view/widgets/system_trayMenu.py
Normal file
38
app/view/widgets/system_trayMenu.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# encoding:utf-8
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QSystemTrayIcon
|
||||
from qfluentwidgets import Action, InfoBar, InfoBarPosition, MessageBox, SystemTrayMenu
|
||||
|
||||
|
||||
class SystemTrayIcon(QSystemTrayIcon):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setIcon(parent.windowIcon())
|
||||
self.setToolTip("-六棱光界-")
|
||||
|
||||
self.menu = SystemTrayMenu(parent=parent)
|
||||
self.menu.addActions(
|
||||
[
|
||||
Action("🙂 显示界面", triggered=self.showSofware),
|
||||
Action("🙃 退出软件", triggered=self.exitSoftware),
|
||||
]
|
||||
)
|
||||
self.setContextMenu(self.menu)
|
||||
|
||||
|
||||
def exitSoftware(self):
|
||||
self.parent().window().showNormal()
|
||||
InfoBar.info("提示","你正在进行退出操作,请返回软件",Qt.Orientation.Horizontal,True,5000,InfoBarPosition.BOTTOM_RIGHT,InfoBar.desktopView())
|
||||
w = MessageBox(
|
||||
title="提示",
|
||||
content="确定退出软件吗?正在进行的任务将会取消",
|
||||
parent=self.parent().window(),
|
||||
)
|
||||
w.yesButton.setText("确定退出")
|
||||
w.cancelButton.setText("算了我想想")
|
||||
if w.exec():
|
||||
self.parent().window().close()
|
||||
|
||||
def showSofware(self):
|
||||
self.parent().window().showNormal()
|
||||
42
app/view/widgets/upload_widget.py
Normal file
42
app/view/widgets/upload_widget.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# coding: utf-8
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QWidget
|
||||
from qfluentwidgets import ScrollArea
|
||||
|
||||
from app.view.components.file_deal_cards import UploadCard
|
||||
|
||||
|
||||
class UploadScrollWidget(ScrollArea):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.scrollWidget = QWidget()
|
||||
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
|
||||
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# self.setViewportMargins(0, 100, 0, 20)
|
||||
self.setWidget(self.scrollWidget)
|
||||
self.setWidgetResizable(True)
|
||||
self.setObjectName("UploadScrollWidget")
|
||||
|
||||
self.scrollWidget.setObjectName("scrollWidget")
|
||||
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
|
||||
self.setStyleSheet("background:transparent;border:none;")
|
||||
|
||||
self.__initLayout()
|
||||
|
||||
def __initLayout(self):
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
def addUploadTask(self, filePath):
|
||||
self.vBoxLayout.addWidget(
|
||||
UploadCard(
|
||||
"file",
|
||||
filePath,
|
||||
self.scrollWidget,
|
||||
)
|
||||
)
|
||||
41
app/view/widgets/ware_search_widgets.py
Normal file
41
app/view/widgets/ware_search_widgets.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# coding: utf-8
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
from qfluentwidgets import (
|
||||
PushButton,
|
||||
)
|
||||
|
||||
from app.view.components.linkage_switching import SearchLinkageSwitching
|
||||
|
||||
|
||||
class WareSearchScrollWidget(QWidget):
|
||||
returnSignal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
|
||||
self.returnButton = PushButton(
|
||||
FIF.RETURN,
|
||||
"返回",
|
||||
self,
|
||||
)
|
||||
self.returnButton.clicked.connect(self.clear)
|
||||
self.searchScrolledArea = SearchLinkageSwitching(self)
|
||||
self.vBoxLayout.addWidget(
|
||||
self.returnButton, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
|
||||
)
|
||||
self.vBoxLayout.addWidget(self.searchScrolledArea)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def wareSearch(self, searchType, searchContent):
|
||||
self.searchScrolledArea.search(searchType, searchContent)
|
||||
|
||||
def clear(self):
|
||||
self.searchScrolledArea.clearFileCards()
|
||||
self.returnSignal.emit()
|
||||
Reference in New Issue
Block a user