Skip to content
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
31 changes: 29 additions & 2 deletions TeXmacs/misc/themes/liii-night.css
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,33 @@ QLabel#startup-tab-page-desc {
color: #aaaaaa;
}

/* 分类按钮 */
QPushButton#startup-tab-category-btn {
background: transparent;
border: none;
color: #aaaaaa;
}

QPushButton#startup-tab-category-btn:hover {
background: #4a4f57;
color: #ffffff;
}

QPushButton#startup-tab-category-btn:checked {
background: #215a6a;
color: white;
}

/* 模板卡片 */
QFrame#startup-tab-template-card {
background: #3a3a3a;
border: 1px solid #4a4f57;
}

QFrame#startup-tab-template-card:hover {
border: 1px solid #2791ad;
}

/* 模板使用按钮 - Template Use Button */
QPushButton#template-use-btn {
color: white;
Expand All @@ -1060,11 +1087,11 @@ QPushButton#template-use-btn {
}

QWidget#centralWidget QPushButton#template-use-btn {
background-color: #2791ad;
background-color: #215a6a;
}

QWidget#centralWidget QPushButton#template-use-btn:hover {
background-color: #215a6a;
background-color: #2791ad;
}

QWidget#centralWidget QPushButton#template-cancel-btn {
Expand Down
27 changes: 27 additions & 0 deletions TeXmacs/misc/themes/liii.css
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,33 @@ QLabel#startup-tab-page-desc {
color: #666666;
}

/* 分类按钮 */
QPushButton#startup-tab-category-btn {
background: transparent;
border: none;
color: #666666;
}

QPushButton#startup-tab-category-btn:hover {
background: #F0F0F0;
color: #333333;
}

QPushButton#startup-tab-category-btn:checked {
background: #215a6a;
color: white;
}

/* 模板卡片 */
QFrame#startup-tab-template-card {
background: white;
border: 1px solid #E5E5EA;
}

QFrame#startup-tab-template-card:hover {
border: 1px solid #2791ad;
}

/* 模板使用按钮 - Template Use Button */
QPushButton#template-use-btn {
background-color: #2791ad;
Expand Down
140 changes: 140 additions & 0 deletions devel/216_25.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# 216_25 模板页面缩略图缓存与 UI 优化

## 如何测试
1. 编译:`xmake b stem`
2. 打开启动页 `Template` 页面,确认模板卡片显示正常:
- 卡片有圆角、阴影效果。
- 分类按钮样式正确(未选中灰色,选中深墨绿 `#215a6a`)。
- 缩略图显示为顶部裁剪、宽度拉满的效果。
3. 验证缩略图缓存:
- **首次打开**:观察控制台,应看到 `[TemplatePage] Download:` 或 `[TemplatePage] Validate:` 日志。
- 关闭软件,重新打开同一页面:
- 已验证过的 URL 应显示 `[TemplatePage] Use cache:`,且**无网络请求**。
- 未验证过的 URL(新会话首次)显示 `[TemplatePage] Validate:`,随后可能看到 `[TemplatePage] Cache fresh:`(304)。
- 切换不同分类、切换到其他标签页(如 Recent)再切回 Template:
- 已显示的缩略图**不应再触发网络请求**。
4. 验证缓存更新:
- **方案 A(修改服务器)**:修改服务器上的缩略图文件,下次启动验证时应返回 200,更新缓存。
- **方案 B(本地测试)- 修改 ETag**:
1. 打开模板页面,等待缩略图下载/验证完成。
2. 找到缓存目录(通常为 `~/.local/share/moganlab/system/cache/thumbnails/`)。
3. 打开 `thumbnail-index.json`,找到对应 URL 的 `etag` 字段,修改为一个错误值(如 `"wrong-etag"`)。
4. 关闭软件重新打开,进入 Template 页面。
5. 验证:由于 ETag 不匹配,服务器返回 200,重新下载缩略图并更新缓存。控制台应显示 `[TemplatePage] Update cache:`。
- **方案 C(本地测试)- 删除索引文件**:
1. 删除 `thumbnail-index.json`,但保留 `.jpg` 缓存文件。
2. 重新打开软件进入 Template 页面。
3. 验证:缺少 ETag 元数据,请求不会带 `If-None-Match`,服务器返回 200,重新下载。
- **方案 D(本地测试)- 过期缓存**:
1. 修改某个 `.jpg` 缓存文件的修改时间为很久以前:`touch -d "2020-01-01" xxx.jpg`
2. 重新打开软件。
3. 验证:由于文件被认为已"过期",会触发重新下载。
5. 检查缓存目录结构:
- 确认 `~/.local/share/moganlab/system/cache/thumbnails/` 下有 `.jpg` 图片文件和 `thumbnail-index.json` 元数据索引文件。
- 确认没有 per-file 的 `.meta` 文件(已改为统一 JSON 索引)。
6. 测试响应式布局:
- 调整窗口宽度,确认列数自适应变化(1~6 列)。
- 窗口较小时不应只显示 1 列(早期 bug)。
7. 测试预览弹窗:
- 点击任意模板卡片,确认弹窗初始尺寸正确(macOS 上不会出现高度过小的问题)。
- 预览区域使用 `QTPdfPreviewWidget` 正常加载 PDF。
- 点击 "Use Template" 和 "Cancel" 按钮正常。
8. 验证 PDF 预览缓存(与缩略图类似的会话级 ETag 验证):
- **首次打开**某模板的预览弹窗:
- 若本地已有 PDF 缓存,预览应直接渲染(不先显示 "No Preview")。
- 控制台应输出 `[PDF Preview] Validate:`,且请求携带 `If-None-Match`。
- **关闭弹窗后,再次点击同一模板**(同一会话内):
- 控制台应输出 `[PDF Preview] Use cache:`,且**无网络请求**。
- **关闭软件,重新打开**,再次进入同一模板预览:
- 新会话首次访问,应再次输出 `[PDF Preview] Validate:`,正确发送 ETag。
- 若服务器返回 304,则后续同一会话内继续使用缓存。
- 验证缓存更新(本地测试):
1. 找到 `PdfFileCache` 缓存目录(通常为 `~/.local/share/moganlab/system/cache/pdf/`)。
2. 打开索引文件(如 `pdf-index.json` 或类似元数据),找到对应 URL 的 `etag` 字段,修改为错误值。
3. 关闭软件重新打开,进入该模板预览。
4. 验证:ETag 不匹配导致服务器返回 200,重新下载并更新缓存。控制台应显示 `[PDF Preview] Update cache:`。

## 2026/04/22 实现说明

### What
本次对模板页面(`QTTemplatePage`)进行了重构和优化,主要改动:

- **缩略图缓存系统**:基于 `ThumbnailCache` 实现内存 LRU + 磁盘持久化缓存,支持 HTTP ETag 条件请求。
- **会话级验证控制**:引入 `validatedUrls_` 集合,确保每个 URL 在每个会话中只发一次条件请求,后续直接使用缓存。
- **UI 美化**:圆角卡片、阴影效果、分类按钮样式、缩略图顶部裁剪显示。
- **响应式网格**:根据窗口宽度自动计算列数(1~6 列)。
- **macOS 弹窗尺寸修复**:`setMinimumSize` 后添加 `resize` 确保初始尺寸正确。
- **PDF 预览缓存优化**:基于 `PdfFileCache` 实现会话级验证,先显示缓存再后台校验,避免"No Preview"闪烁。

#### 修改文件

**src/Plugins/Qt/qt_template_page.cpp** (修改)
- **缩略图加载逻辑**:
- `loadThumbnail()`:缓存命中时**先显示缓存图**,再决定是否后台验证。
- 引入 `validatedUrls_` 判断:已验证过的 URL 直接显示,不再发请求。
- 未验证的缓存发送 `If-None-Match` 条件请求,304 时仅标记为已验证,200 时更新缓存和 UI。
- **网络请求队列**:`processThumbnailQueue()` 控制最多 6 个并发请求。
- **UI 样式**:
- 卡片使用 `QGraphicsDropShadowEffect` 添加阴影。
- 卡片圆角、边框、hover 变色(`#2791ad`)。
- 分类按钮选中状态背景色为 `#215a6a`。
- 缩略图使用 `Qt::KeepAspectRatioByExpanding` + `copy(x, 0, w, h)` 实现顶部裁剪。
- **预览弹窗尺寸**:`showTemplatePreview()` 中使用正方形预览区域,确保各平台显示一致。
- **macOS 弹窗修复**:`showTemplatePreview()` 中 `dialog->resize()` 紧跟 `setMinimumSize()`。
- **响应式布局**:`calculateColumnCount()` 在 viewport 未就绪时返回默认值 4,避免首屏显示 1 列。
- **防重复刷新**:`gridNeedsRefresh_` 标志避免 `onTemplatesLoaded` 和 `showEvent` 双重刷新。
- **网络错误时保留缓存图**:`onThumbnailLoaded` 中错误分支仅在 `req.label->pixmap().isNull()` 时才显示 `"Preview"` placeholder,避免覆盖已有缓存缩略图。
- **无预览 URL 时立即清空**:`clearPreview("No Preview")` 仅在 `previewUrl.isEmpty()` 时调用,避免有缓存 PDF 时先显示无预览。

**src/Plugins/Qt/qt_template_page.hpp** (修改)
- `ThumbnailRequest` 结构体新增 `cachedEtag` 字段。
- `QTTemplatePage` 新增 `validatedUrls_` 成员(`QSet<QString>`)。
- 新增 `gridNeedsRefresh_` 布尔标志。

**src/Plugins/Qt/qt_pdf_preview_widget.cpp** (修改)
- **默认文案**:`setupUI()` 默认从 `"No Preview"` 改为 `"Loading..."`,避免首次打开时先闪出无预览状态。
- **即时加载缓存**:`loadFromUrl()` 中若 `PdfFileCache` 命中,立即调用 `loadFromFile()` 渲染,**不再等待网络请求**。
- **会话级验证**:引入 `static QSet<QString> s_validatedPdfUrls`。
- 首次会话遇到缓存 -> 发 `If-None-Match` 条件请求。
- 304 / 200 均标记为已验证,同一会话内后续打开直接 `Use cache`,**不再发请求**。
- 新会话(软件重启)重新验证一次。
- **日志示例**:
```
[PDF Preview] Update cache: "https://cdn.../report-example.pdf"
[PDF Preview] Use cache: "https://cdn.../report-example.pdf"
```

**src/Mogan/Cache/thumbnail_cache.cpp** (修改)
- 实现基于 `QCache` 的内存 LRU 缓存(默认 50MB)。
- 磁盘缓存使用统一 `thumbnail-index.json` 索引文件(替代早期 per-file `.meta` 方案)。
- `getEntry()` / `put()` 支持 ETag 和 Last-Modified 读写。
- `loadIndex()` / `saveIndex()` 管理 JSON 索引。
- `cleanupExpired()` 清理过期文件(默认 30 天)。

**src/Mogan/Cache/thumbnail_cache.hpp** (修改)
- 新增 `ThumbnailCacheEntry` 结构体(`pixmap`, `etag`, `lastModified`, `isValid()`)。
- 新增 `getEntry()`, `preload()`, `memoryCacheSize()`, `diskCacheSize()`, `memoryHits()` 等接口。

**src/Mogan/Cache/image_cache_base.hpp** (修改)
- `ImageCacheEntry` 新增 `etag` 和 `lastModified` 字段。

### Why
1. **ETag 条件请求**:避免每次切换分类都重复下载缩略图,同时保证软件重启后能检测到服务器上的更新。
2. **会话级验证**:用户明确要求"请求应该就是软件打开的时候直接发起一次就可以了,没必要反复来"。缩略图和 PDF 预览均引入会话级 `validatedUrls_` / `s_validatedPdfUrls`,同一会话内仅验证一次。
3. **统一 JSON 索引**:替代 per-file `.meta`,减少文件数量,简化管理。
4. **UI 美化**:提升模板页面的视觉质感,与整体设计风格保持一致。
5. **顶部裁剪**:缩略图原比例显示时高度不一,裁剪为统一尺寸后网格更整齐。
6. **PDF 即时加载**:避免用户先看到 "No Preview" 再加载缓存的闪烁体验,有缓存时直接渲染。

### How
1. **缓存命中时先显示、后验证**:`loadThumbnail()` 中无论是否已验证,都先把缓存的 `pixmap` 设置到 `label`,避免用户看到 "Loading..."。PDF 预览同理,有缓存直接 `loadFromFile()`。
2. **条件请求流程**:
- 首次会话遇到缓存 -> 发 `If-None-Match`。
- 304 -> 标记 `validatedUrls_` / `s_validatedPdfUrls`,UI 不变。
- 200 -> 渲染新图、更新缓存、更新 UI、标记已验证。
3. **统一索引文件**:
- `diskIndex_` 维护在内存中的 `QHash<QString, QJsonObject>`。
- 构造时 `loadIndex()` 读取 `thumbnail-index.json`。
- `put()` 时通过 `QMetaObject::invokeMethod(..., Qt::QueuedConnection)` 异步保存图片和索引,避免死锁。
4. **响应式列数**:`availableWidth < cardSpace` 时返回 4(而非 1),解决首屏布局问题。
5. **错误时保留缓存**:网络请求失败(非 200/304)时,若 `label` 已有缓存图则保持显示,仅在没有缓存图时才显示 placeholder。
8 changes: 6 additions & 2 deletions src/Mogan/Cache/image_cache_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ struct ImageCacheEntry {
QString key;
QDateTime cachedAt;
qint64 cost; // Memory cost (bytes)
QString etag;
QDateTime lastModified;

ImageCacheEntry () : cost (0) {}
ImageCacheEntry (const QPixmap& px, const QString& k, qint64 c)
ImageCacheEntry (const QPixmap& px, const QString& k, qint64 c,
const QString& e = QString (),
const QDateTime& lm= QDateTime ())
: pixmap (px), key (k), cachedAt (QDateTime::currentDateTime ()),
cost (c) {}
cost (c), etag (e), lastModified (lm) {}
};

/**
Expand Down
Loading
Loading