Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions 107 devel/216_22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 216_22 PDF 预览控件悬停按钮修复

## 如何测试
1. 编译:`xmake b stem`
2. 打开启动页 `Template` 页面,点击任意模板卡片打开预览弹窗。
3. 确认首次打开时,鼠标悬停在预览区域上,左右翻页按钮和页码指示器正常显示。
4. 关闭弹窗后再次打开同一模板(走缓存路径),确认悬停按钮依然正常显示。
5. 测试翻页功能是否正常。
6. 测试缓存更新(HTTP 条件请求):
- **方案 A(修改服务器)**:修改服务器上的 PDF 文件,确认客户端能正确获取新版本。
- **方案 B(本地测试)- 修改缓存过期时间**:
1. 先打开一次模板预览,等待 PDF 下载完成并缓存。
2. 找到缓存目录(通常为 `~/.local/share/moganlab/system/cache/pdf_files/`)。
3. 修改 PDF 缓存文件的修改时间为很久以前:`touch -d "2020-01-01" xxx.pdf`
4. 关闭弹窗后再次打开同一模板预览。
5. 验证:由于文件被认为已"过期",会触发重新下载,在控制台能看到新的网络请求。
- **方案 C(本地测试)- 删除元数据文件**:
1. 先打开一次模板预览,等待 PDF 下载完成并缓存。
2. 删除 `.json` 元数据文件,但保留 `.pdf` 文件:`rm xxx.json`
3. 关闭弹窗后再次打开同一模板预览。
4. 验证:由于缺少 ETag 和 Last-Modified,请求不会带条件头,服务器返回 200,重新下载文件。
- **方案 D(本地测试)- 替换 PDF 文件**:
1. 先打开一次模板预览,等待 PDF 下载完成并缓存。
2. 用另一个 PDF 文件替换缓存的 PDF(保持文件名不变):`cp another.pdf xxx.pdf`
3. **关键:同时删除对应的 `.json` 文件**,否则服务器返回 304 不会重新下载。
4. 关闭弹窗后再次打开同一模板预览。
5. 验证:客户端会重新下载,覆盖你替换的文件。
- **方案 E(本地测试)- 修改 ETag**:
1. 先打开一次模板预览,等待 PDF 下载完成并缓存。
2. 找到对应的 `.json` 文件,修改 `etag` 字段为一个错误值(如 `"wrong-etag"`)。
3. 关闭弹窗后再次打开同一模板预览。
4. 验证:由于 ETag 不匹配,服务器返回 200,重新下载文件并更新缓存。
- **通用验证方法**:
- 观察控制台输出 `"[PDF Preview] Remote not modified, using cache"` 表示 304 缓存命中。
- 观察网络请求日志表示重新下载(200)。
7. 检查cache存放位置,是否有/pdf_files,而且每个pdf文件有对应的json文件。

## 2026/04/20 实现说明

### What
本次对 PDF 预览控件(`QTPdfPreviewWidget`)进行了重构,主要改动:
- **文件缓存系统**:将图片缓存替换为 PDF 文件缓存,支持 HTTP 条件请求(304 Not Modified)。
- **默认尺寸调整**:预览窗口默认尺寸从 460x400 调整为 600x600。
- **事件过滤器优化**:修复鼠标从预览容器移到按钮时按钮闪烁/隐藏的问题。

#### 修改文件

**src/Plugins/Qt/qt_pdf_preview_widget.cpp** (修改)
- 文件缓存集成:
- 使用 `PdfFileCache` 替代 `PdfPreviewCache`,缓存原始 PDF 文件而非渲染后的图片。
- 实现 HTTP 条件请求(`If-None-Match`, `If-Modified-Since`)支持。
- 新增 `onConditionalReplyFinished` 处理 304 响应。
- 默认尺寸调整:
- `kDefaultPreviewWidth` 从 460 改为 600。
- `kDefaultPreviewHeight` 从 400 改为 600。
- 简化 `updatePreviewSize` 逻辑,直接使用可用空间。
- 事件过滤器优化:
- 为所有相关控件(按钮、页码指示器、预览标签)安装事件过滤器。
- 启用鼠标跟踪以接收 `HoverMove` 事件。
- 使用 `underMouse()` 替代依赖 `Enter/Leave` 事件。
- 新增 `mouseInWidgetHierarchy()` 成员函数统一检查鼠标位置。
- 代码优化:
- 提取 `goToPage()` 公共方法,消除 `goToPreviousPage`/`goToNextPage` 重复代码。
- 网络错误时清理 `pdfData_` 避免残留。
- 移除未使用的 `QHBoxLayout` 头文件。
- 修复重复调用 `saveToCache` 的问题,确保元数据(ETag, Last-Modified)正确保存。
- **修复关键 Bug**:当服务器返回 200 OK(文件已更新)时,原代码因 `currentReply_` 被提前置空导致无法正确处理响应。新增 `processNetworkReply()` 私有方法,统一处理网络响应。
- **修复 Last-Modified 解析**:使用 `QLocale::c().toDateTime()` 配合自定义格式字符串解析 RFC 2822 日期(如 `Thu, 29 Jan 2026 11:32:54 GMT`),并添加 `Qt::RFC2822Date` 作为 fallback,确保在各种 Qt 版本下都能正确解析。

**src/Plugins/Qt/qt_pdf_preview_widget.hpp** (修改)
- 新增 `mouseInWidgetHierarchy()` 方法声明。
- 新增 `goToPage(int page)` 槽函数声明。
- 新增 `onConditionalReplyFinished` 槽函数声明。
- 新增 `processNetworkReply()` 私有方法声明,用于统一处理网络响应。

**src/Mogan/Cache/pdf_preview_cache.cpp** (删除)
- 删除旧版图片缓存实现,由 `pdf_file_cache.cpp` 替代。

**src/Mogan/Cache/pdf_preview_cache.hpp** (删除)
- 删除旧版图片缓存头文件,由 `pdf_file_cache.hpp` 替代。

**src/Mogan/Cache/pdf_file_cache.cpp** (新增)
- 实现基于文件的 PDF 缓存系统。
- 支持 HTTP 条件请求(ETag, Last-Modified)。
- 使用 MD5 哈希作为文件名。
- 支持可配置的缓存过期时间(默认 30 天)。
- 线程安全的单例模式实现。

**src/Mogan/Cache/pdf_file_cache.hpp** (新增)
- 定义 `PdfCacheEntry` 结构体(包含文件路径、ETag、最后修改时间等元数据)。
- 定义 `PdfFileCache` 单例类接口。

### Why
1. **文件缓存替代图片缓存**:图片缓存占用内存大且无法重新渲染,文件缓存支持任意 DPI 渲染且更节省内存。
2. **默认尺寸调整**:原尺寸过小,600x600 更适合预览 PDF 内容。
3. **按钮闪烁问题**:原事件过滤器只监控 `previewContainer_`,当鼠标从容器移到按钮时,容器的 `Leave` 事件会隐藏按钮。

### How
1. **文件缓存机制**:
- 下载的 PDF 文件保存到本地缓存目录。
- 再次请求时发送条件请求,若服务器返回 304 则直接使用缓存文件。
- 缓存元数据(ETag, Last-Modified)与文件一起存储。
2. **尺寸调整**:
- 修改默认常量值,简化 `updatePreviewSize` 中的尺寸计算逻辑。
3. **事件过滤器优化**:
- 为所有相关控件安装事件过滤器,统一处理悬停事件。
- 使用 `QTimer::singleShot(50, ...)` 延迟检查 `underMouse()`,避免子控件间切换时误判。
195 changes: 195 additions & 0 deletions 195 src/Mogan/Cache/pdf_file_cache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@

/******************************************************************************
* MODULE : pdf_file_cache.cpp
* DESCRIPTION: PDF file cache (download once, reuse locally)
* COPYRIGHT : (C) 2026 Yuki Lu
******************************************************************************/

#include "pdf_file_cache.hpp"

#include <QCryptographicHash>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>

#include "image_cache_base.hpp"

#include "string.hpp"

string get_env (string var);

// Singleton instance
static PdfFileCache* g_instance= nullptr;
static QMutex s_instanceMutex;

PdfFileCache::PdfFileCache (QObject* parent)
: QObject (parent), expirationDays_ (DEFAULT_EXPIRATION_DAYS) {}

PdfFileCache::~PdfFileCache () {
if (g_instance == this) {
g_instance= nullptr;
}
}

PdfFileCache*
PdfFileCache::instance () {
QMutexLocker locker (&s_instanceMutex);
if (!g_instance) {
g_instance= new PdfFileCache ();
}
return g_instance;
}

QString
PdfFileCache::cacheDirectory () {
// Use unified cache directory from ImageCacheUtils
return ImageCacheUtils::cacheSubdir (CACHE_SUBDIR);
}

QString
PdfFileCache::cacheFilePath (const QString& url) const {
QString hash=
QString (QCryptographicHash::hash (url.toUtf8 (), QCryptographicHash::Md5)
.toHex ());
return QDir (cacheDirectory ()).filePath (hash + ".pdf");
}

QString
PdfFileCache::metaFilePath (const QString& url) const {
QString hash=
QString (QCryptographicHash::hash (url.toUtf8 (), QCryptographicHash::Md5)
.toHex ());
return QDir (cacheDirectory ()).filePath (hash + ".json");
}

bool
PdfFileCache::isExpired (const QString& filePath) const {
QFileInfo info (filePath);
if (!info.exists ()) return true;
return ImageCacheUtils::isFileExpired (filePath, expirationDays_);
}

void
PdfFileCache::saveMetadata (const QString& url, const PdfCacheEntry& entry) {
QJsonObject obj;
obj["url"] = url;
obj["etag"] = entry.etag;
obj["lastModified"]= entry.lastModified.toString (Qt::ISODate);
obj["cachedAt"] = entry.cachedAt.toString (Qt::ISODate);

QFile file (metaFilePath (url));
if (file.open (QIODevice::WriteOnly)) {
file.write (QJsonDocument (obj).toJson ());
file.close ();
}
}

PdfCacheEntry
PdfFileCache::loadMetadata (const QString& url) const {
PdfCacheEntry entry;
QFile file (metaFilePath (url));
if (!file.open (QIODevice::ReadOnly)) {
return entry;
}

QJsonDocument doc= QJsonDocument::fromJson (file.readAll ());
file.close ();

if (!doc.isObject ()) return entry;

QJsonObject obj= doc.object ();
entry.filePath = cacheFilePath (url);
entry.etag = obj["etag"].toString ();
entry.lastModified=
QDateTime::fromString (obj["lastModified"].toString (), Qt::ISODate);
entry.cachedAt=
QDateTime::fromString (obj["cachedAt"].toString (), Qt::ISODate);

return entry;
}

PdfCacheEntry
PdfFileCache::getEntry (const QString& url) const {
QMutexLocker locker (&mutex_);

QString filePath= cacheFilePath (url);
if (!QFile::exists (filePath) || isExpired (filePath)) {
return PdfCacheEntry ();
}

PdfCacheEntry entry= loadMetadata (url);
if (!entry.isValid ()) {
// No metadata but file exists - return basic entry
entry.filePath= filePath;
entry.cachedAt= QFileInfo (filePath).lastModified ();
}

qDebug () << "[PdfFileCache] Cache hit:" << url;
return entry;
}

bool
PdfFileCache::contains (const QString& url) const {
return getEntry (url).isValid ();
}

QString
PdfFileCache::saveToCache (const QString& url, const QByteArray& data,
const QString& etag, const QDateTime& lastModified) {
if (data.isEmpty ()) return QString ();

QString filePath= cacheFilePath (url);

QMutexLocker locker (&mutex_);

QFile file (filePath);
if (!file.open (QIODevice::WriteOnly)) {
qWarning () << "[PdfFileCache] Failed to write cache file:" << filePath;
return QString ();
}

file.write (data);
file.close ();

// Save metadata
PdfCacheEntry entry;
entry.filePath = filePath;
entry.etag = etag;
entry.lastModified= lastModified;
entry.cachedAt = QDateTime::currentDateTime ();
saveMetadata (url, entry);

qDebug () << "[PdfFileCache] Saved to cache:" << filePath
<< "size:" << data.size () << "bytes";
return filePath;
}

void
PdfFileCache::clear () {
QMutexLocker locker (&mutex_);

QString dir= cacheDirectory ();
QDir cacheDir (dir);

// Remove PDF files
for (const QString& file :
cacheDir.entryList (QStringList () << "*.pdf", QDir::Files)) {
cacheDir.remove (file);
}
// Remove metadata files
for (const QString& file :
cacheDir.entryList (QStringList () << "*.json", QDir::Files)) {
cacheDir.remove (file);
}
qDebug () << "[PdfFileCache] Cleared all cached PDF files";
}

void
PdfFileCache::setExpirationDays (int days) {
QMutexLocker locker (&mutex_);
expirationDays_= days;
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.