Files
leonpan-pc/app/view/widgets/preview_box.py
Leonmmcoset ead93c1e49 feat(预览): 支持更多文本文件类型预览并增强URL处理
添加对js和html文件类型的文本预览支持,并改进预览组件的URL处理逻辑。现在能自动补全相对路径为完整URL,同时增加详细的日志记录以便调试。
2025-10-29 22:27:39 +08:00

349 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.api import miaoStarsBasicApi
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)
# 处理URL确保它是完整的URL
self.url = self._ensure_full_url(url)
logger.info(f"初始化图片预览框URL: {self.url}")
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(self.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 _ensure_full_url(self, url):
"""确保URL是完整的添加scheme和base URL如果缺失"""
if not url:
return url
# 检查URL是否已经包含scheme
if url.startswith(('http://', 'https://')):
return url
# 对于相对路径使用API的base URL构建完整URL
# 移除可能的前导斜杠,避免重复
path = url.lstrip('/')
# 从MiaoStarsBasicApi获取base URL但只使用到域名部分
base_url = miaoStarsBasicApi.basicApi.split('/api/v4')[0]
full_url = f"{base_url}/{path}"
logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}")
return full_url
def startLoading(self):
"""开始加载图片"""
logger.info(f"开始加载图片: {self.url}")
self.imageLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
logger.debug(f"图片加载进度: {progress}%")
self.loadingCard.setText(f"正在加载图片... {progress}%")
def setPreviewImg(self, img: QPixmap):
"""设置预览图片"""
logger.info(f"图片加载成功,尺寸: {img.width()}x{img.height()}px")
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):
"""处理加载错误"""
logger.error(f"图片预览失败: {msg}")
self.loadingCard.error()
self.previewLabel.hide()
# 文本文档预览类
class PreviewTextBox(MessageBoxBase):
"""文本预览对话框"""
def __init__(self, parent=None, url=None, _id=None):
super().__init__(parent=parent)
# 处理URL确保它是完整的URL
self.url = self._ensure_full_url(url)
logger.info(f"初始化文本预览框URL: {self.url}, 文件ID: {_id}")
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(f"保存文本文件修改文件ID: {self._id}")
# 显示进度条并禁用按钮
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):
logger.info(f"文本文件保存成功文件ID: {self._id}")
InfoBar.success(
"成功",
"修改保存成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条
self.saveProgressBar.hide()
QTimer.singleShot(700, self.accept)
def _errorSave(self, msg):
logger.error(f"文本文件保存失败文件ID: {self._id}, 错误: {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:
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如果缺失"""
if not url:
return url
# 检查URL是否已经包含scheme
if url.startswith(('http://', 'https://')):
return url
# 对于相对路径使用API的base URL构建完整URL
# 移除可能的前导斜杠,避免重复
path = url.lstrip('/')
# 从MiaoStarsBasicApi获取base URL但只使用到域名部分
base_url = miaoStarsBasicApi.basicApi.split('/api/v4')[0]
full_url = f"{base_url}/{path}"
logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}")
return full_url
def startLoading(self):
"""开始加载文本"""
logger.info(f"开始加载文本文件: {self.url}")
self.textLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
logger.debug(f"文本加载进度: {progress}%")
self.loadingCard.setText(f"正在加载文本... {progress}%")
def setTextContent(self, content):
"""设置文本内容"""
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.textEdit.setPlainText(content)
def handleError(self, error_msg):
"""处理加载错误"""
logger.error(f"文本预览失败URL: {self.url}, 错误: {error_msg}")
self.loadingCard.error()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
# 文本预览框会自动适应大小,无需特殊处理