awa
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# coding: utf-8
|
||||
|
||||
import re
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QPixmap
|
||||
@@ -8,7 +9,7 @@ from qfluentwidgets import Action, BodyLabel, CardWidget, ImageLabel, InfoBar, I
|
||||
MessageBox, PushButton, RoundMenu, StrongBodyLabel
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
|
||||
from app.core import (DeleteFileThread, formatDate, formatSize, getFileIcon, lang, signalBus)
|
||||
from app.core import (DeleteFileThread, RenameFileThread, formatDate, formatSize, getFileIcon, lang, signalBus)
|
||||
from app.view.widgets.share_file_messageBox import ShareFileMessageBox
|
||||
|
||||
|
||||
@@ -132,6 +133,9 @@ class FileCard(CardWidget):
|
||||
Action(
|
||||
FIF.PROJECTOR, lang("预览"), triggered=lambda: self.selfPreview()
|
||||
),
|
||||
Action(
|
||||
FIF.EDIT, lang("重命名"), triggered=lambda: self.renameSelf()
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -151,6 +155,9 @@ class FileCard(CardWidget):
|
||||
menu.addActions(
|
||||
[
|
||||
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
|
||||
Action(
|
||||
FIF.EDIT, lang("重命名"), triggered=lambda: self.renameSelf()
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -171,31 +178,47 @@ class FileCard(CardWidget):
|
||||
)
|
||||
if w.exec():
|
||||
# 构建Cloudreve V4 API所需的正确路径格式
|
||||
if self.filePath == "/":
|
||||
# 确保路径格式规范,避免Path not exist错误
|
||||
# 只使用路径中的目录部分,不包含文件名,确保不会重复
|
||||
dir_path = self.filePath
|
||||
if dir_path == "/":
|
||||
# 根目录情况
|
||||
full_path = f"cloudreve://my/{self.fileName}"
|
||||
else:
|
||||
# 子目录情况,确保正确拼接路径
|
||||
# 清理路径,避免重复的斜杠和前缀
|
||||
clean_path = self.filePath.lstrip("/")
|
||||
# 确保路径中不包含cloudreve://my前缀
|
||||
clean_path = clean_path.replace("cloudreve://my/", "")
|
||||
full_path = f"cloudreve://my/{clean_path}/{self.fileName}"
|
||||
# 子目录情况,确保只使用目录路径
|
||||
# 清理路径,移除协议前缀
|
||||
clean_dir = dir_path
|
||||
if clean_dir.startswith("cloudreve://my/"):
|
||||
clean_dir = clean_dir[15:] # 移除 "cloudreve://my/"
|
||||
elif clean_dir.startswith("/"):
|
||||
clean_dir = clean_dir[1:] # 移除前导斜杠
|
||||
|
||||
# 确保路径格式正确,移除重复的前缀
|
||||
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
||||
# 确保路径中间没有多余的斜杠
|
||||
clean_dir = '/'.join([part for part in clean_dir.split('/') if part])
|
||||
|
||||
# 更健壮地处理重复文件名的情况
|
||||
# 分割路径并去重
|
||||
# 构建完整URI,只包含目录路径和文件名一次
|
||||
full_path = f"cloudreve://my/{clean_dir}/{self.fileName}"
|
||||
|
||||
# 检查并移除可能的重复文件名部分
|
||||
# 避免同时包含编码形式和原始形式的文件名
|
||||
import re
|
||||
# 查找可能的编码形式+原始形式的重复模式
|
||||
path_parts = full_path.split('/')
|
||||
if len(path_parts) > 1:
|
||||
# 检查最后一个部分是否是文件名
|
||||
if path_parts[-1] == self.fileName:
|
||||
# 检查倒数第二个部分是否也是文件名
|
||||
if len(path_parts) > 2 and path_parts[-2] == self.fileName:
|
||||
# 移除重复的文件名部分
|
||||
path_parts.pop(-2)
|
||||
full_path = '/'.join(path_parts)
|
||||
if len(path_parts) > 3:
|
||||
# 检查最后两个部分是否可能是编码和原始形式的关系
|
||||
potential_encoding = path_parts[-2]
|
||||
original = path_parts[-1]
|
||||
# 如果倒数第二个部分看起来像URL编码且最后一个部分是中文
|
||||
if re.match(r'^%[0-9A-Fa-f]{2}%[0-9A-Fa-f]{2}%[0-9A-Fa-f]{2}%[0-9A-Fa-f]{2}$', potential_encoding) and any('\u4e00'-'\u9fff' in c for c in original):
|
||||
# 移除倒数第二个部分,避免重复
|
||||
path_parts.pop(-2)
|
||||
full_path = '/'.join(path_parts)
|
||||
|
||||
# 最终清理
|
||||
full_path = full_path.replace("//", "/") # 移除多余的斜杠
|
||||
|
||||
# 记录构建的路径用于调试
|
||||
print(f"[DEBUG] 重命名路径: {full_path}")
|
||||
|
||||
self.deleteThread = DeleteFileThread(full_path, self.fileType)
|
||||
self.deleteThread.successDelete.connect(self.deleteSuccess)
|
||||
@@ -235,6 +258,162 @@ class FileCard(CardWidget):
|
||||
self.window(),
|
||||
)
|
||||
logger.error(f"删除文件失败:{error_msg}")
|
||||
|
||||
def renameSelf(self):
|
||||
"""重命名文件或文件夹"""
|
||||
# 使用输入对话框获取新名称
|
||||
from qfluentwidgets import LineEdit, MessageBoxBase, SubtitleLabel
|
||||
from PyQt6.QtCore import Qt
|
||||
import re
|
||||
|
||||
# 创建自定义对话框
|
||||
class RenameMessageBox(MessageBoxBase):
|
||||
"""自定义重命名对话框"""
|
||||
|
||||
def __init__(self, parent=None, initial_name=""):
|
||||
super().__init__(parent)
|
||||
self.titleLabel = SubtitleLabel(f"{lang("请输入新名称")}:")
|
||||
self.lineEdit = LineEdit()
|
||||
self.lineEdit.setText(initial_name)
|
||||
|
||||
# 选择文件名部分(不包括扩展名)
|
||||
if "." in initial_name:
|
||||
name_part = initial_name.rsplit(".", 1)[0]
|
||||
self.lineEdit.setSelection(0, len(name_part))
|
||||
else:
|
||||
self.lineEdit.selectAll()
|
||||
|
||||
# 将组件添加到布局中
|
||||
self.viewLayout.addWidget(self.titleLabel)
|
||||
self.viewLayout.addWidget(self.lineEdit)
|
||||
|
||||
# 设置对话框的最小宽度
|
||||
self.widget.setMinimumWidth(400)
|
||||
|
||||
# 设置窗口标题
|
||||
self.setWindowTitle(lang("重命名"))
|
||||
|
||||
# 创建并显示对话框
|
||||
dlg = RenameMessageBox(self.window(), self.fileName)
|
||||
if dlg.exec():
|
||||
new_name = dlg.lineEdit.text().strip()
|
||||
|
||||
# 验证文件名
|
||||
if not new_name:
|
||||
InfoBar.warning(
|
||||
lang("警告"),
|
||||
lang("文件名不能为空"),
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
return
|
||||
|
||||
# 检查文件名是否包含无效字符
|
||||
if re.search(r'[<>"/\\|?*]', new_name):
|
||||
InfoBar.warning(
|
||||
lang("警告"),
|
||||
lang("文件名包含无效字符"),
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
return
|
||||
|
||||
# 检查文件名是否与当前目录中的其他文件重复
|
||||
# 获取当前目录下的所有文件名称
|
||||
current_folder = self.filePath
|
||||
# 从父窗口获取文件列表(假设父窗口有fileList属性)
|
||||
parent_window = self.window()
|
||||
if hasattr(parent_window, 'fileList'):
|
||||
# 遍历文件列表,检查是否有相同名称的文件(排除当前正在重命名的文件)
|
||||
for file_item in parent_window.fileList:
|
||||
# 跳过当前正在重命名的文件
|
||||
if hasattr(file_item, 'fileName') and file_item.fileName == self.fileName:
|
||||
continue
|
||||
# 检查是否有同名文件
|
||||
if hasattr(file_item, 'fileName') and file_item.fileName == new_name:
|
||||
InfoBar.warning(
|
||||
lang("警告"),
|
||||
lang("文件名已存在"),
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
return
|
||||
|
||||
# 构建Cloudreve V4 API所需的正确路径格式
|
||||
# 确保路径格式规范,避免Path not exist错误
|
||||
# 只使用路径中的目录部分,不包含文件名
|
||||
if self.filePath == "/":
|
||||
# 根目录情况,URI只包含根目录路径
|
||||
full_path = "cloudreve://my" # 直接使用正确的双斜杠格式
|
||||
else:
|
||||
# 子目录情况,确保只使用目录路径,不包含文件名
|
||||
# 清理路径,移除协议前缀
|
||||
clean_path = self.filePath
|
||||
if clean_path.startswith("cloudreve://my/"):
|
||||
clean_path = clean_path[15:] # 移除 "cloudreve://my/"
|
||||
elif clean_path.startswith("/"):
|
||||
clean_path = clean_path[1:] # 移除前导斜杠
|
||||
|
||||
# 构建完整URI,只包含目录路径
|
||||
full_path = f"cloudreve://my/{clean_path}" # 直接使用正确的双斜杠格式
|
||||
|
||||
# 强制确保协议前缀格式正确,使用双斜杠
|
||||
# 直接替换所有可能的单斜杠格式
|
||||
full_path = full_path.replace("cloudreve:/my", "cloudreve://my")
|
||||
full_path = full_path.replace("cloudreve:/", "cloudreve://")
|
||||
# 再次确认格式完全正确
|
||||
if not full_path.startswith("cloudreve://"):
|
||||
# 如果格式仍然不正确,重新构建整个URI
|
||||
path_part = full_path.replace("cloudreve:", "").lstrip("/")
|
||||
full_path = f"cloudreve://{path_part}"
|
||||
|
||||
# 不再需要处理文件名重复,因为URI中只包含目录路径
|
||||
|
||||
# 最终清理
|
||||
full_path = full_path.replace("//", "/") # 移除多余的斜杠
|
||||
|
||||
# 记录构建的路径用于调试
|
||||
print(f"[DEBUG] 重命名路径: {full_path}")
|
||||
|
||||
# 调用重命名线程
|
||||
self.renameThread = RenameFileThread(full_path, new_name, self.fileType)
|
||||
self.renameThread.successRename.connect(self.renameSuccess)
|
||||
self.renameThread.errorRename.connect(self.renameError)
|
||||
self.renameThread.start()
|
||||
|
||||
def renameSuccess(self):
|
||||
InfoBar.success(
|
||||
lang("成功"),
|
||||
lang("重命名成功"),
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
# 刷新文件夹列表
|
||||
signalBus.refreshFolderListSignal.emit()
|
||||
|
||||
def renameError(self, error_msg):
|
||||
InfoBar.error(
|
||||
lang("失败"),
|
||||
f"{lang("重命名失败")}: {error_msg}",
|
||||
Qt.Orientation.Horizontal,
|
||||
True,
|
||||
1000,
|
||||
InfoBarPosition.TOP_RIGHT,
|
||||
self.window(),
|
||||
)
|
||||
logger.error(f"重命名文件失败:{error_msg}")
|
||||
|
||||
def contextMenuEvent(self, e):
|
||||
"""重写上下文菜单事件,确保只有右键点击才会触发"""
|
||||
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
from welcome_video import WelcomeVideoPlayer
|
||||
|
||||
|
||||
class RegisterWindow(Window):
|
||||
@@ -50,6 +51,7 @@ class RegisterWindow(Window):
|
||||
|
||||
self.loginWidget = LoginWidget(self)
|
||||
self.registerWidget = RegisterWidget(self)
|
||||
self.videoPlayer = WelcomeVideoPlayer(self)
|
||||
|
||||
self.__initWidgets()
|
||||
logger.info("注册窗口初始化完成")
|
||||
@@ -65,6 +67,11 @@ class RegisterWindow(Window):
|
||||
self.setStyleSheet(f"RegisterWindow{{background: {color.name()}}}")
|
||||
self.setWindowIcon(QIcon(":app/images/logo.png"))
|
||||
self.setFixedSize(690, 470)
|
||||
|
||||
# 设置视频播放器为全窗口大小
|
||||
self.videoPlayer.setGeometry(0, 0, 690, 470)
|
||||
# 初始时隐藏视频播放器
|
||||
self.videoPlayer.hide()
|
||||
|
||||
# self.promotionalImageLabel.setImage(":app/images/background.png")
|
||||
# self.promotionalImageLabel.scaledToWidth(300)
|
||||
@@ -92,6 +99,25 @@ class RegisterWindow(Window):
|
||||
self.stackedWidget.setMaximumWidth(300)
|
||||
self.stackedWidget.addWidget(self.loginWidget)
|
||||
self.stackedWidget.addWidget(self.registerWidget)
|
||||
|
||||
def show_welcome_video(self):
|
||||
"""显示欢迎视频"""
|
||||
# 确保视频播放器显示在最前面
|
||||
self.videoPlayer.raise_()
|
||||
# 开始播放视频
|
||||
self.videoPlayer.start_playback()
|
||||
# 再次确保跳过按钮在最顶层
|
||||
self.videoPlayer.skip_button.raise_()
|
||||
# 连接视频完成信号
|
||||
self.videoPlayer.videoFinished.connect(self.hide_welcome_video)
|
||||
|
||||
def hide_welcome_video(self):
|
||||
"""隐藏欢迎视频,显示登录界面"""
|
||||
self.videoPlayer.stop_playback()
|
||||
# 确保登录界面元素可见
|
||||
self.pivot.show()
|
||||
self.stackedWidget.show()
|
||||
self.logoImage.show()
|
||||
|
||||
self.titleBar.titleLabel.setStyleSheet(
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,7 @@ from PyQt6.QtCore import QSize
|
||||
from PyQt6.QtGui import QColor, QIcon
|
||||
from PyQt6.QtWidgets import QApplication, QWidget
|
||||
from qfluentwidgets import NavigationAvatarWidget, NavigationItemPosition, SplashScreen
|
||||
from qfluentwidgets import FluentIcon
|
||||
|
||||
from app.core import cfg, qconfig, userConfig, GetUserAvatarThread,lang,signalBus
|
||||
from app.view.app_info_interface import AppInfoInterface
|
||||
@@ -68,26 +69,26 @@ class MainWindow(CustomFluentWindow):
|
||||
# 注意:MSFluentWindow的addSubInterface方法支持selectedIcon参数
|
||||
self.addSubInterface(
|
||||
self.ownFiledInterface,
|
||||
QIcon(":app/icons/Myfile.svg"),
|
||||
FluentIcon.FOLDER,
|
||||
lang("我的文件"),
|
||||
position=NavigationItemPosition.TOP,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.storagespaceInterface,
|
||||
QIcon(":app/icons/Storage.svg"),
|
||||
FluentIcon.APPLICATION,
|
||||
lang("存储配额"),
|
||||
position=NavigationItemPosition.TOP,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.taskInterface,
|
||||
QIcon(":app/icons/Task.svg"),
|
||||
FluentIcon.PASTE,
|
||||
lang("任务管理"),
|
||||
position=NavigationItemPosition.TOP,
|
||||
)
|
||||
|
||||
self.addSubInterface(
|
||||
self.appInfoInterface,
|
||||
QIcon(":app/icons/Application.svg"),
|
||||
FluentIcon.INFO,
|
||||
lang("应用信息"),
|
||||
position=NavigationItemPosition.BOTTOM,
|
||||
)
|
||||
|
||||
@@ -15,13 +15,13 @@ class NumInformationWidget(QWidget):
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
|
||||
self.basicSizeCard = GbInformationCard(0, lang("用户组基础容量"), self)
|
||||
self.packSizeCard = 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.packSizeCard)
|
||||
self.hBoxLayout.addWidget(self.usedSizeCard)
|
||||
self.hBoxLayout.addWidget(self.totalSizeCard)
|
||||
|
||||
@@ -69,7 +69,7 @@ class StoragespaceInterface(ScrollArea):
|
||||
def _successGetPack(self, datas):
|
||||
self.packData = datas["data"]
|
||||
self.firstLoad = False
|
||||
self.numInformationWidget.packSizeCard.updateValue(self.packData["pack"])
|
||||
# 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"])
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
# coding: utf-8
|
||||
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
import os
|
||||
from PyQt6.QtCore import Qt, QTimer, QUrl, QDir, QFileInfo
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineContextMenuRequest
|
||||
from qfluentwidgets import (
|
||||
ImageLabel,
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
IndeterminateProgressBar,
|
||||
MessageBoxBase,
|
||||
PlainTextEdit,
|
||||
PushButton,
|
||||
)
|
||||
|
||||
from app.core import (ImageLoaderThread, TextLoaderThread, UpdateFileContentThread)
|
||||
|
||||
|
||||
class MonacoWebEnginePage(QWebEnginePage):
|
||||
"""自定义WebEnginePage以处理右键菜单"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""自定义右键菜单事件,允许基本的编辑操作"""
|
||||
request = event.request()
|
||||
# 可以在这里根据需要自定义右键菜单的行为
|
||||
# 例如,只允许复制、粘贴等特定操作
|
||||
# 目前我们让Monaco Editor自己处理右键菜单
|
||||
pass
|
||||
from app.core.api import miaoStarsBasicApi
|
||||
from app.core.services.text_speech import LocalSpeechController
|
||||
|
||||
from app.view.components.empty_card import EmptyCard
|
||||
|
||||
|
||||
@@ -251,30 +267,31 @@ class PreviewTextBox(MessageBoxBase):
|
||||
self.widget.setMinimumSize(600, 400)
|
||||
self._id = _id
|
||||
self.isChanged = False
|
||||
self.speech_controller = LocalSpeechController(self)
|
||||
|
||||
# 设置编辑器HTML文件路径 - 修正为项目根目录的_internal文件夹
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
self.editor_index = os.path.join(project_root, "_internal/editor_main/index.html")
|
||||
logger.info(f"编辑器HTML路径: {self.editor_index}")
|
||||
|
||||
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)
|
||||
# 创建占位符标签替代WebEngineView
|
||||
from PyQt6.QtWidgets import QLabel
|
||||
self.placeholderLabel = QLabel('''<div style='text-align:center; padding:20px;'>
|
||||
<h3>文本编辑将在外部浏览器中打开</h3>
|
||||
<p>请在浏览器中完成编辑后,点击"保存并返回"按钮</p>
|
||||
<p>应用将自动检测并更新内容</p>
|
||||
</div>''')
|
||||
self.placeholderLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.placeholderLabel.setWordWrap(True)
|
||||
self.viewLayout.addWidget(self.placeholderLabel)
|
||||
|
||||
# 初始化变量
|
||||
self.editorContent = ""
|
||||
self.tempFilePath = None
|
||||
|
||||
# 轮询定时器,用于检查浏览器是否保存了内容
|
||||
self.pollingTimer = QTimer(self)
|
||||
self.pollingTimer.setInterval(1000) # 每秒检查一次
|
||||
self.pollingTimer.timeout.connect(self._checkBrowserContent)
|
||||
|
||||
# 加载状态显示
|
||||
self.loadingCard = EmptyCard(self)
|
||||
@@ -315,13 +332,8 @@ class PreviewTextBox(MessageBoxBase):
|
||||
# 显示进度条并禁用按钮
|
||||
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()
|
||||
# 直接使用editorContent变量,因为我们已经切换到外部浏览器编辑模式
|
||||
self._saveContent(self.editorContent)
|
||||
|
||||
def _successSave(self):
|
||||
logger.info(f"文本文件保存成功,文件ID: {self._id}")
|
||||
@@ -353,20 +365,7 @@ class PreviewTextBox(MessageBoxBase):
|
||||
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:
|
||||
logger.info(f"开始文本朗读,文件ID: {self._id}")
|
||||
self.speech_controller.play_text(text)
|
||||
self.isSpeaking = True
|
||||
self.textSpeakButton.setText("暂停朗读")
|
||||
else:
|
||||
logger.info(f"暂停文本朗读,文件ID: {self._id}")
|
||||
self.speech_controller.stop_playback()
|
||||
self.isSpeaking = False
|
||||
self.textSpeakButton.setText("朗读文本")
|
||||
|
||||
|
||||
def _ensure_full_url(self, url):
|
||||
"""确保URL是完整的,添加scheme和base URL(如果缺失)"""
|
||||
@@ -432,29 +431,453 @@ class PreviewTextBox(MessageBoxBase):
|
||||
self.loadingCard.setText(f"正在加载文本... {progress}%")
|
||||
|
||||
def setTextContent(self, content):
|
||||
"""设置文本内容"""
|
||||
"""设置文本内容并在外部浏览器中打开"""
|
||||
# 检查内容是否已保存,如果已保存则不允许再次编辑
|
||||
if hasattr(self, 'isContentSaved') and self.isContentSaved:
|
||||
logger.warning("内容已保存,不允许再次编辑")
|
||||
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
|
||||
<h3>已经保存</h3>
|
||||
<p>该内容已经保存,不再允许编辑</p>
|
||||
</div>''')
|
||||
return
|
||||
|
||||
logger.info(f"文本文件加载成功,原始内容长度: {len(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:
|
||||
logger.warning(f"文本内容过长,已截断显示,原始长度: {len(content)}字符")
|
||||
content = (
|
||||
content[:max_display_length]
|
||||
+ f"\n\n... (内容过长,已截断前{max_display_length}个字符,完整内容请下载文件查看)"
|
||||
)
|
||||
|
||||
# 保存原始内容
|
||||
self.editorContent = content
|
||||
|
||||
# 检测语言类型
|
||||
language = self._detect_language(content)
|
||||
|
||||
# 创建临时文件用于保存内容和语言信息
|
||||
import tempfile
|
||||
import base64
|
||||
|
||||
# 创建临时文件
|
||||
fd, self.tempFilePath = tempfile.mkstemp(suffix='.json', text=True)
|
||||
os.close(fd)
|
||||
|
||||
# 对内容进行Base64编码
|
||||
encoded_content = base64.b64encode(content.encode('utf-8')).decode('ascii')
|
||||
|
||||
# 保存到临时文件
|
||||
import json
|
||||
with open(self.tempFilePath, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
'content': encoded_content,
|
||||
'language': language,
|
||||
'file_id': self._id,
|
||||
'saved': False # 初始状态为未保存
|
||||
}, f)
|
||||
|
||||
# 获取编辑器HTML文件的绝对路径
|
||||
editor_abs_path = os.path.abspath(self.editor_index)
|
||||
|
||||
# 构建URL参数
|
||||
params = {
|
||||
'temp_file': self.tempFilePath,
|
||||
'content': encoded_content,
|
||||
'language': language
|
||||
}
|
||||
|
||||
# 导入必要的模块来创建本地Web服务器
|
||||
import http.server
|
||||
import socketserver
|
||||
import threading
|
||||
import sys
|
||||
|
||||
# 创建本地Web服务器类
|
||||
class SimpleHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
# 禁用服务器日志输出
|
||||
pass
|
||||
|
||||
# 获取_internal目录的路径
|
||||
# 当前文件路径是app/view/widgets/preview_box.py,需要找到项目根目录
|
||||
current_file = os.path.abspath(__file__)
|
||||
# 从当前文件路径向上找项目根目录(跳过app目录)
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_file)))
|
||||
internal_dir = os.path.join(project_root, "_internal")
|
||||
logger.info(f"项目根目录: {project_root}")
|
||||
logger.info(f"_internal目录: {internal_dir}")
|
||||
|
||||
# 启动本地Web服务器
|
||||
port = 36852
|
||||
handler = SimpleHTTPRequestHandler
|
||||
|
||||
# 创建一个允许地址重用的TCPServer类
|
||||
class ReuseTCPServer(socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
# 自定义HTTP请求处理器,用于处理POST保存请求
|
||||
class EditorHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
# 保存对PreviewTextBox实例的引用
|
||||
preview_box_instance = None
|
||||
|
||||
def do_POST(self):
|
||||
"""处理POST请求,特别是保存请求"""
|
||||
if self.path == '/save':
|
||||
# 获取请求体长度
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
# 读取请求体数据
|
||||
post_data = self.rfile.read(content_length)
|
||||
|
||||
try:
|
||||
# 解析JSON数据
|
||||
import json
|
||||
save_data = json.loads(post_data)
|
||||
|
||||
# 记录保存请求
|
||||
logger.info("接收到来自浏览器的保存请求")
|
||||
|
||||
# 使用preview_box_instance处理保存的数据
|
||||
if EditorHTTPRequestHandler.preview_box_instance:
|
||||
EditorHTTPRequestHandler.preview_box_instance._processSavedContent(save_data)
|
||||
|
||||
# 返回成功响应
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'status': 'success'}).encode('utf-8'))
|
||||
except Exception as e:
|
||||
logger.error(f"处理保存请求失败: {e}")
|
||||
# 返回错误响应
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'status': 'error', 'message': str(e)}).encode('utf-8'))
|
||||
else:
|
||||
# 对于其他POST请求,返回404
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""禁用日志消息,保持安静"""
|
||||
return
|
||||
|
||||
# 尝试使用多个备用端口,避免端口被占用
|
||||
max_attempts = 5
|
||||
attempts = 0
|
||||
while attempts < max_attempts:
|
||||
try:
|
||||
# 设置处理器的实例引用
|
||||
EditorHTTPRequestHandler.preview_box_instance = self
|
||||
# 使用自定义处理器创建服务器
|
||||
self.httpd = ReuseTCPServer(("127.0.0.1", port), EditorHTTPRequestHandler)
|
||||
logger.info(f"Web服务器成功在端口 {port} 启动")
|
||||
break
|
||||
except OSError as e:
|
||||
attempts += 1
|
||||
logger.warning(f"端口 {port} 被占用,尝试使用备用端口")
|
||||
port += 1 # 使用下一个端口
|
||||
if attempts >= max_attempts:
|
||||
logger.error(f"无法找到可用端口,最后一个错误: {e}")
|
||||
raise
|
||||
|
||||
# 切换工作目录到_internal目录
|
||||
if os.path.exists(internal_dir):
|
||||
os.chdir(internal_dir)
|
||||
logger.info(f"切换Web服务器工作目录到: {internal_dir}")
|
||||
else:
|
||||
logger.error(f"_internal目录不存在: {internal_dir}")
|
||||
# 如果目录不存在,保持在当前目录运行服务器
|
||||
|
||||
# 在后台线程中运行服务器
|
||||
self.server_thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
|
||||
self.server_thread.start()
|
||||
|
||||
# 构建基于HTTP的URL
|
||||
from urllib.parse import urlencode
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
# 根据目录是否存在来调整HTML路径
|
||||
if os.path.exists(internal_dir):
|
||||
# 如果在_internal目录下运行服务器,使用相对路径
|
||||
html_path = "editor_main/index.html"
|
||||
else:
|
||||
# 如果不在_internal目录下,使用完整路径
|
||||
html_path = "_internal/editor_main/index.html"
|
||||
url = f'http://127.0.0.1:{port}/{html_path}?{urlencode(params)}'
|
||||
|
||||
# 使用系统默认浏览器打开URL
|
||||
logger.info(f"在外部浏览器中打开编辑器(HTTP): {url}")
|
||||
QDesktopServices.openUrl(QUrl(url))
|
||||
|
||||
# 显示保存提醒
|
||||
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
|
||||
<h3>文本编辑已在外部浏览器中打开</h3>
|
||||
<p><strong>重要提示:</strong>完成编辑后,请点击浏览器中的"保存并返回"按钮</p>
|
||||
<p>应用正在自动检测...</p>
|
||||
</div>''')
|
||||
|
||||
# 启动轮询检查
|
||||
self.pollingTimer.start()
|
||||
|
||||
def _checkBrowserContent(self):
|
||||
"""检查浏览器是否已保存内容"""
|
||||
# 首先尝试从localStorage读取
|
||||
try:
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
|
||||
# 尝试获取localStorage数据
|
||||
# 在Windows上,localStorage通常存储在用户的AppData目录中
|
||||
# 由于直接访问localStorage有困难,我们使用一个更可靠的方法:
|
||||
# 1. 首先检查临时文件
|
||||
if self.tempFilePath and os.path.exists(self.tempFilePath):
|
||||
try:
|
||||
with open(self.tempFilePath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
if data.get('saved', False):
|
||||
logger.info("检测到浏览器已保存内容(通过临时文件)")
|
||||
self._processSavedContent(data)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"读取临时文件失败: {e}")
|
||||
|
||||
# 2. 作为备用方案,检查是否有特定的保存文件
|
||||
# 这个文件可以由浏览器通过特定的方法创建
|
||||
import tempfile
|
||||
app_data_dir = os.path.join(tempfile.gettempdir(), 'LeonPan')
|
||||
os.makedirs(app_data_dir, exist_ok=True)
|
||||
save_file_path = os.path.join(app_data_dir, 'editor_content.json')
|
||||
|
||||
if os.path.exists(save_file_path):
|
||||
try:
|
||||
with open(save_file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
if data.get('saved', False):
|
||||
logger.info("检测到浏览器已保存内容(通过备用文件)")
|
||||
self._processSavedContent(data)
|
||||
# 删除备用文件
|
||||
os.unlink(save_file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"读取备用保存文件失败: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查浏览器内容时出错: {e}")
|
||||
|
||||
def _processSavedContent(self, data):
|
||||
"""处理已保存的内容"""
|
||||
# 停止轮询
|
||||
self.pollingTimer.stop()
|
||||
|
||||
# 更新内容
|
||||
import base64
|
||||
encoded_content = data.get('content', '')
|
||||
try:
|
||||
self.editorContent = base64.b64decode(encoded_content).decode('utf-8')
|
||||
except Exception as e:
|
||||
logger.error(f"解码内容失败: {e}")
|
||||
return
|
||||
|
||||
# 标记内容已保存
|
||||
self.isContentSaved = True
|
||||
|
||||
# 更新占位符文本,显示已保存信息
|
||||
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
|
||||
<h3>已经保存</h3>
|
||||
<p>内容已从浏览器同步到应用并已保存</p>
|
||||
<p>该内容将不再允许编辑</p>
|
||||
</div>''')
|
||||
|
||||
# 禁用保存按钮
|
||||
self.saveButton.setEnabled(False)
|
||||
|
||||
# 清理临时文件
|
||||
if self.tempFilePath and os.path.exists(self.tempFilePath):
|
||||
try:
|
||||
os.unlink(self.tempFilePath)
|
||||
self.tempFilePath = None
|
||||
except Exception as e:
|
||||
logger.error(f"删除临时文件失败: {e}")
|
||||
|
||||
|
||||
self.textEdit.setPlainText(content)
|
||||
|
||||
def handleError(self, error_msg):
|
||||
"""处理加载错误"""
|
||||
logger.error(f"文本预览失败,URL: {self.url}, 错误: {error_msg}")
|
||||
self.loadingCard.error()
|
||||
|
||||
def _initMonacoEditor(self, content):
|
||||
"""初始化Monaco Editor并加载内容"""
|
||||
# 获取编辑器文件的绝对路径
|
||||
editor_dir = QDir("_internal/editor")
|
||||
editor_path = editor_dir.absoluteFilePath("min/vs/editor/editor.main.js")
|
||||
editor_file = QFileInfo(editor_path)
|
||||
base_url = QUrl.fromLocalFile(editor_file.absolutePath())
|
||||
|
||||
# 检测文本语言
|
||||
language = self._detect_language(content)
|
||||
logger.info(f"检测到文本语言类型: {language}")
|
||||
|
||||
# 构建HTML内容,使用ES模块方式加载Monaco Editor
|
||||
html_content = f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body, html {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
}}
|
||||
#container {{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}}
|
||||
</style>
|
||||
<script src="vs/loader.js"></script>
|
||||
<script>
|
||||
let editor;
|
||||
|
||||
require.config({{ paths: {{ 'vs': './vs' }} }});
|
||||
require(['vs/editor/editor.main'], function() {{
|
||||
editor = monaco.editor.create(document.getElementById('container'), {{
|
||||
value: `{content.replace('`', '\\`')}`,
|
||||
}});
|
||||
// 重新获取编辑器实例以确保正确初始化
|
||||
setTimeout(() => {{
|
||||
editor = monaco.editor.getModels()[0] ? monaco.editor.getModels()[0].getContainerInfo().domNode.monacoEditor : null;
|
||||
}}, 100);'\\`'}}`,
|
||||
language: '{language}',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: {{ enabled: true }},
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'on',
|
||||
readOnly: false,
|
||||
fontFamily: 'Consolas, "Microsoft YaHei", monospace'
|
||||
}});
|
||||
|
||||
// 添加内容变化监听器
|
||||
editor.onDidChangeModelContent(function() {{
|
||||
// 可以在这里添加内容变化的处理逻辑
|
||||
}});
|
||||
}});
|
||||
|
||||
// 暴露获取内容的方法给QWebEngineView调用
|
||||
function getEditorContent() {{
|
||||
return editor ? editor.getValue() : '';
|
||||
}}
|
||||
|
||||
// 暴露设置内容的方法
|
||||
function setEditorContent(content) {{
|
||||
if (editor) {{
|
||||
editor.setValue(content);
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# 由于我们已经切换到外部浏览器编辑模式,这里不再需要加载HTML内容到textEdit
|
||||
# self.textEdit.setHtml(html_content, base_url) - 已注释,因为textEdit不再存在
|
||||
|
||||
def _detect_language(self, content):
|
||||
"""检测文本语言类型"""
|
||||
content_lower = content.lower()
|
||||
|
||||
if 'public class' in content_lower and ('import ' in content_lower or 'package ' in content_lower):
|
||||
return 'java'
|
||||
elif 'def ' in content_lower and ('import ' in content_lower or 'print(' in content_lower):
|
||||
return 'python'
|
||||
elif '<!doctype html>' in content_lower or '<html>' in content_lower:
|
||||
return 'html'
|
||||
elif '{' in content_lower and '}' in content_lower and ':' in content_lower:
|
||||
return 'json'
|
||||
elif '{' in content_lower and '}' in content_lower and ('function' in content_lower or 'const ' in content_lower):
|
||||
return 'javascript'
|
||||
elif '<?xml' in content_lower:
|
||||
return 'xml'
|
||||
elif '<?php' in content_lower:
|
||||
return 'php'
|
||||
elif 'class ' in content_lower and '{' in content_lower and '}' in content_lower and ';' in content_lower:
|
||||
return 'cpp'
|
||||
elif 'select ' in content_lower and 'from ' in content_lower:
|
||||
return 'sql'
|
||||
elif '#include' in content_lower:
|
||||
return 'cpp'
|
||||
elif '<script' in content_lower:
|
||||
return 'html'
|
||||
elif 'function' in content_lower and '{' in content_lower and '}' in content_lower:
|
||||
return 'javascript'
|
||||
elif 'namespace' in content_lower and '{' in content_lower and '}' in content_lower:
|
||||
return 'cpp'
|
||||
elif 'var ' in content_lower and '=' in content_lower:
|
||||
return 'javascript'
|
||||
elif 'using namespace' in content_lower:
|
||||
return 'cpp'
|
||||
elif 'struct ' in content_lower and '{' in content_lower:
|
||||
return 'cpp'
|
||||
elif '#' in content_lower and 'define' in content_lower:
|
||||
return 'cpp'
|
||||
else:
|
||||
return 'text'
|
||||
|
||||
# JavaScript执行完成的处理已移除
|
||||
|
||||
def _saveContent(self, content):
|
||||
"""保存编辑器内容并提交修改"""
|
||||
logger.info(f"保存文本文件修改,文件ID: {self._id}")
|
||||
self.saveTextThread = UpdateFileContentThread(
|
||||
self._id,
|
||||
content,
|
||||
)
|
||||
self.saveTextThread.successUpdated.connect(self._successSave)
|
||||
self.saveTextThread.errorUpdated.connect(self._errorSave)
|
||||
self.saveTextThread.start()
|
||||
|
||||
def getTextContent(self):
|
||||
"""获取编辑器内容"""
|
||||
return self.editorContent
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""重写窗口大小改变事件"""
|
||||
super().resizeEvent(event)
|
||||
# 文本预览框会自动适应大小,无需特殊处理
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭事件,清理临时文件、定时器和Web服务器"""
|
||||
# 停止轮询定时器
|
||||
if hasattr(self, 'pollingTimer'):
|
||||
self.pollingTimer.stop()
|
||||
logger.info("轮询定时器已停止")
|
||||
|
||||
# 停止Web服务器
|
||||
if hasattr(self, 'httpd'):
|
||||
try:
|
||||
logger.info("正在停止本地Web服务器...")
|
||||
self.httpd.shutdown()
|
||||
self.httpd.server_close()
|
||||
logger.info("本地Web服务器已成功停止")
|
||||
except Exception as e:
|
||||
logger.error(f"停止Web服务器失败: {e}")
|
||||
|
||||
# 清理临时文件
|
||||
if hasattr(self, 'tempFilePath') and os.path.exists(self.tempFilePath):
|
||||
try:
|
||||
os.unlink(self.tempFilePath)
|
||||
logger.info(f"临时文件已清理: {self.tempFilePath}")
|
||||
except Exception as e:
|
||||
logger.error(f"清理临时文件失败: {e}")
|
||||
|
||||
# 调用父类方法并接受事件
|
||||
super().closeEvent(event)
|
||||
event.accept()
|
||||
|
||||
|
||||
133
app/view/widgets/welcome_video.py
Normal file
133
app/view/widgets/welcome_video.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# coding:utf-8
|
||||
import os
|
||||
import sys
|
||||
from PyQt6.QtCore import Qt, QUrl, QTimer
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
from PyQt6.QtMultimedia import QMediaPlayer, QVideoSink, QAudioOutput
|
||||
from PyQt6.QtMultimediaWidgets import QVideoWidget
|
||||
from loguru import logger
|
||||
|
||||
from app.core.utils.config import cfg
|
||||
|
||||
class WelcomeVideoPlayer(QWidget):
|
||||
"""欢迎视频播放器,用于首次运行时播放介绍视频"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
# 设置视频播放器组件
|
||||
self.media_player = QMediaPlayer()
|
||||
|
||||
# 创建视频窗口
|
||||
self.video_widget = QVideoWidget()
|
||||
# 使用正确的属性赋值方式
|
||||
self.media_player.setVideoOutput(self.video_widget)
|
||||
|
||||
# 创建跳过按钮
|
||||
self.skip_button = QPushButton("跳过")
|
||||
self.skip_button.setFixedSize(100, 40)
|
||||
self.skip_button.setObjectName("skipButton")
|
||||
self.skip_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
""")
|
||||
self.skip_button.clicked.connect(self.skip_video)
|
||||
|
||||
# 设置布局
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.addWidget(self.video_widget)
|
||||
|
||||
# 创建跳过按钮布局
|
||||
self.skip_layout = QHBoxLayout()
|
||||
self.skip_layout.addStretch()
|
||||
self.skip_layout.addWidget(self.skip_button)
|
||||
self.skip_layout.setContentsMargins(0, 0, 20, 20)
|
||||
|
||||
# 将跳过按钮布局添加到主布局
|
||||
self.main_layout.addLayout(self.skip_layout)
|
||||
self.main_layout.setAlignment(self.skip_layout, Qt.AlignmentFlag.AlignBottom)
|
||||
|
||||
# 连接信号
|
||||
self.media_player.playbackStateChanged.connect(self.on_playback_state_changed)
|
||||
self.media_player.errorOccurred.connect(self.on_error)
|
||||
|
||||
# 设置视频文件路径
|
||||
self.video_path = os.path.join("_internal", "start.mp4")
|
||||
|
||||
def play(self):
|
||||
"""播放视频"""
|
||||
if os.path.exists(self.video_path):
|
||||
logger.info(f"开始播放欢迎视频: {self.video_path}")
|
||||
# 设置视频源
|
||||
self.media_player.setSource(QUrl.fromLocalFile(self.video_path))
|
||||
|
||||
# 调整窗口大小以适应屏幕
|
||||
self.adjust_window_size()
|
||||
|
||||
# 显示窗口
|
||||
self.show()
|
||||
|
||||
# 开始播放
|
||||
self.media_player.play()
|
||||
else:
|
||||
logger.warning(f"欢迎视频文件不存在: {self.video_path}")
|
||||
self.close()
|
||||
|
||||
def skip_video(self):
|
||||
"""跳过视频"""
|
||||
logger.info("用户点击跳过按钮,停止播放欢迎视频")
|
||||
self.stop_and_close()
|
||||
|
||||
def stop_and_close(self):
|
||||
"""停止播放并关闭窗口"""
|
||||
self.media_player.stop()
|
||||
# 标记为非首次运行
|
||||
cfg.firstRun.value = False
|
||||
# 延迟关闭,确保配置保存
|
||||
QTimer.singleShot(100, self.close)
|
||||
|
||||
def on_playback_state_changed(self, state):
|
||||
"""处理播放状态变化"""
|
||||
from PyQt6.QtMultimedia import QMediaPlayer
|
||||
if state == QMediaPlayer.PlaybackState.StoppedState:
|
||||
logger.info("欢迎视频播放完成")
|
||||
self.stop_and_close()
|
||||
|
||||
def on_error(self, error, error_string):
|
||||
"""处理播放器错误"""
|
||||
logger.error(f"视频播放错误: {error_string}")
|
||||
self.stop_and_close()
|
||||
|
||||
def adjust_window_size(self):
|
||||
"""调整窗口大小以适应屏幕"""
|
||||
# 获取屏幕大小
|
||||
screen = QApplication.primaryScreen().availableGeometry()
|
||||
|
||||
# 设置窗口大小为屏幕的90%
|
||||
width = int(screen.width() * 0.9)
|
||||
height = int(screen.height() * 0.9)
|
||||
|
||||
self.resize(width, height)
|
||||
|
||||
# 居中显示
|
||||
self.move(screen.center().x() - width // 2, screen.center().y() - height // 2)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭事件"""
|
||||
self.media_player.stop()
|
||||
event.accept()
|
||||
Reference in New Issue
Block a user