2025-10-29 22:20:21 +08:00
|
|
|
|
# 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)
|
2025-10-29 22:27:39 +08:00
|
|
|
|
from app.core.api import miaoStarsBasicApi
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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)
|
2025-10-29 22:27:39 +08:00
|
|
|
|
# 处理URL,确保它是完整的URL
|
|
|
|
|
|
self.url = self._ensure_full_url(url)
|
|
|
|
|
|
logger.info(f"初始化图片预览框,URL: {self.url}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用优化的图片加载线程
|
2025-10-29 22:27:39 +08:00
|
|
|
|
self.imageLoaderThread = ImageLoaderThread(self.url)
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-10-29 22:27:39 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-10-29 22:20:21 +08:00
|
|
|
|
def startLoading(self):
|
|
|
|
|
|
"""开始加载图片"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"开始加载图片: {self.url}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.imageLoaderThread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def updateProgress(self, progress):
|
|
|
|
|
|
"""更新加载进度"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.debug(f"图片加载进度: {progress}%")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.loadingCard.setText(f"正在加载图片... {progress}%")
|
|
|
|
|
|
|
|
|
|
|
|
def setPreviewImg(self, img: QPixmap):
|
|
|
|
|
|
"""设置预览图片"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"图片加载成功,尺寸: {img.width()}x{img.height()}px")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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):
|
|
|
|
|
|
"""处理加载错误"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.error(f"图片预览失败: {msg}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.loadingCard.error()
|
|
|
|
|
|
self.previewLabel.hide()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 文本文档预览类
|
|
|
|
|
|
class PreviewTextBox(MessageBoxBase):
|
|
|
|
|
|
"""文本预览对话框"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, parent=None, url=None, _id=None):
|
|
|
|
|
|
super().__init__(parent=parent)
|
2025-10-29 22:27:39 +08:00
|
|
|
|
# 处理URL,确保它是完整的URL
|
|
|
|
|
|
self.url = self._ensure_full_url(url)
|
|
|
|
|
|
logger.info(f"初始化文本预览框,URL: {self.url}, 文件ID: {_id}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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):
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"保存文本文件修改,文件ID: {self._id}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
# 显示进度条并禁用按钮
|
|
|
|
|
|
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):
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"文本文件保存成功,文件ID: {self._id}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
InfoBar.success(
|
|
|
|
|
|
"成功",
|
|
|
|
|
|
"修改保存成功",
|
|
|
|
|
|
Qt.Orientation.Horizontal,
|
|
|
|
|
|
True,
|
|
|
|
|
|
1000,
|
|
|
|
|
|
InfoBarPosition.TOP_RIGHT,
|
|
|
|
|
|
self.window(),
|
|
|
|
|
|
)
|
|
|
|
|
|
# 隐藏进度条
|
|
|
|
|
|
self.saveProgressBar.hide()
|
|
|
|
|
|
QTimer.singleShot(700, self.accept)
|
|
|
|
|
|
|
|
|
|
|
|
def _errorSave(self, msg):
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.error(f"文本文件保存失败,文件ID: {self._id}, 错误: {msg}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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:
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"开始文本朗读,文件ID: {self._id}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.speech_controller.play_text(text)
|
|
|
|
|
|
self.isSpeaking = True
|
|
|
|
|
|
self.textSpeakButton.setText("暂停朗读")
|
|
|
|
|
|
else:
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"暂停文本朗读,文件ID: {self._id}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.speech_controller.stop_playback()
|
|
|
|
|
|
self.isSpeaking = False
|
|
|
|
|
|
self.textSpeakButton.setText("朗读文本")
|
|
|
|
|
|
|
2025-10-29 22:27:39 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-10-29 22:20:21 +08:00
|
|
|
|
def startLoading(self):
|
|
|
|
|
|
"""开始加载文本"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"开始加载文本文件: {self.url}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.textLoaderThread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def updateProgress(self, progress):
|
|
|
|
|
|
"""更新加载进度"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.debug(f"文本加载进度: {progress}%")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.loadingCard.setText(f"正在加载文本... {progress}%")
|
|
|
|
|
|
|
|
|
|
|
|
def setTextContent(self, content):
|
|
|
|
|
|
"""设置文本内容"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.info(f"文本文件加载成功,原始内容长度: {len(content)}字符")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.loadingCard.hide()
|
|
|
|
|
|
self.textEdit.show()
|
|
|
|
|
|
self.textSpeakButton.show()
|
|
|
|
|
|
self.saveButton.setEnabled(True)
|
|
|
|
|
|
# 限制显示的内容长度,避免性能问题
|
|
|
|
|
|
max_display_length = 100000 # 最多显示10万个字符
|
|
|
|
|
|
if len(content) > max_display_length:
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.warning(f"文本内容过长,已截断显示,原始长度: {len(content)}字符")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
content = (
|
|
|
|
|
|
content[:max_display_length]
|
|
|
|
|
|
+ f"\n\n... (内容过长,已截断前{max_display_length}个字符,完整内容请下载文件查看)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.textEdit.setPlainText(content)
|
|
|
|
|
|
|
|
|
|
|
|
def handleError(self, error_msg):
|
|
|
|
|
|
"""处理加载错误"""
|
2025-10-29 22:27:39 +08:00
|
|
|
|
logger.error(f"文本预览失败,URL: {self.url}, 错误: {error_msg}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.loadingCard.error()
|
|
|
|
|
|
|
|
|
|
|
|
def resizeEvent(self, event):
|
|
|
|
|
|
"""重写窗口大小改变事件"""
|
|
|
|
|
|
super().resizeEvent(event)
|
|
|
|
|
|
# 文本预览框会自动适应大小,无需特殊处理
|