awa
This commit is contained in:
@@ -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