# 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, fileId=None): super().__init__(parent=parent) self.fileId = fileId # 保存文件ID self.url = url # 暂存原始URL,可能需要作为fallback logger.info(f"初始化图片预览框,文件ID: {self.fileId}, 原始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) # 延迟启动加载,避免阻塞UI初始化 from PyQt6.QtCore import QTimer QTimer.singleShot(100, self._initImageLoading) 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 _initImageLoading(self): """初始化图片加载,优先使用getFileUrl方法获取临时URL""" if self.fileId: logger.info(f"使用文件URI {self.fileId} 获取临时预览URL") try: # 使用getFileUrl方法获取临时URL from app.core.api import miaoStarsBasicApi response = miaoStarsBasicApi.getFileUrl(self.fileId, redirect=False) # 详细记录响应内容 logger.debug(f"getFileUrl响应: {response}") # 处理getFileUrl的返回值,支持多种格式 if isinstance(response, str): # 直接返回URL字符串的情况 preview_url = response.strip('` ') logger.info(f"成功获取临时预览URL: {preview_url}") self.url = preview_url elif isinstance(response, dict): # 检查是否有code字段表示错误 if response.get('code') == -1: error_msg = response.get('msg', '获取临时URL失败') logger.warning(f"获取临时URL失败: {error_msg},将使用原始URL作为fallback") # 检查是否有urls字段(Cloudreve API返回格式) elif 'urls' in response and isinstance(response['urls'], list) and len(response['urls']) > 0: preview_url = response['urls'][0].get('url', '').strip('` ') logger.info(f"成功获取临时预览URL: {preview_url}") self.url = preview_url # 检查是否有data字段(Cloudreve V4 API标准格式) elif 'data' in response: data = response['data'] if isinstance(data, dict): # 如果data是字典,检查是否包含urls数组 if 'urls' in data and isinstance(data['urls'], list) and len(data['urls']) > 0: preview_url = data['urls'][0].get('url', '').strip('` ') logger.info(f"成功从data中获取临时预览URL: {preview_url}") self.url = preview_url else: # 尝试将data直接转为字符串作为fallback preview_url = str(data).strip('` ') logger.warning(f"data字段不包含urls数组,使用原始data字符串: {preview_url}") self.url = preview_url else: # data不是字典,直接转为字符串 preview_url = str(data).strip('` ') logger.info(f"成功获取临时预览URL: {preview_url}") self.url = preview_url else: error_msg = response.get('msg', '获取临时URL失败') logger.warning(f"获取临时URL失败: {error_msg},将使用原始URL作为fallback") if self.url: self.url = self._ensure_full_url(self.url) logger.info(f"使用处理后的fallback URL: {self.url}") else: logger.error("没有可用的URL,无法加载图片") self.handleError("没有可用的图片URL") return except Exception as e: logger.error(f"获取预览URL时发生异常: {str(e)}") # 尝试使用备用方式处理 if self.url: self.url = self._ensure_full_url(self.url) logger.info(f"异常后使用处理后的fallback URL: {self.url}") else: logger.error("异常后没有可用的URL,无法加载图片") self.handleError(f"获取URL异常: {str(e)}") return else: # 如果没有fileId,尝试处理原始URL if self.url: self.url = self._ensure_full_url(self.url) logger.info(f"使用处理后的原始URL: {self.url}") else: logger.error("没有提供文件ID或URL,无法加载图片") self.handleError("没有提供图片URL") return # 创建图片加载线程 self.imageLoaderThread = ImageLoaderThread(self.url) self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg) self.imageLoaderThread.errorOccurred.connect(self.handleError) self.imageLoaderThread.progressUpdated.connect(self.updateProgress) # 开始加载 self.imageLoaderThread.start() logger.info(f"开始加载图片: {self.url}") # startLoading方法已被_initImageLoading替代 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) # 确保在创建线程前使用处理后的URL self.textLoaderThread = TextLoaderThread(self.url, fileId=_id) 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): """开始加载文本""" # 如果有fileId,尝试获取临时URL if self._id: logger.info(f"使用文件URI {self._id} 获取临时文本URL") response = miaoStarsBasicApi.getFileUrl(self._id, redirect=False) # 处理getFileUrl的返回值,支持多种格式 preview_url = None if isinstance(response, dict): # 检查是否是Cloudreve V4 API格式 (data.urls) if response.get('code') == 0 and 'data' in response: data = response['data'] # 格式1: data.urls数组 if isinstance(data, dict) and 'urls' in data and isinstance(data['urls'], list) and len(data['urls']) > 0: preview_url = data['urls'][0].get('url', '').strip('` ') # 格式2: data直接包含URL elif isinstance(data, str): preview_url = data.strip('` ') # 检查是否直接包含urls字段 elif 'urls' in response and isinstance(response['urls'], list) and len(response['urls']) > 0: preview_url = response['urls'][0].get('url', '').strip('` ') # 如果成功获取到URL if preview_url: self.url = preview_url logger.info(f"成功获取临时文本URL: {self.url}") else: error_msg = response.get('msg', '获取临时URL失败') logger.warning(f"获取临时URL失败: {error_msg},将使用处理后的原始URL") # 更新线程的URL和fileId self.textLoaderThread = TextLoaderThread(self.url, fileId=self._id) self.textLoaderThread.textLoaded.connect(self.setTextContent) self.textLoaderThread.errorOccurred.connect(self.handleError) self.textLoaderThread.progressUpdated.connect(self.updateProgress) 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) # 文本预览框会自动适应大小,无需特殊处理