Skip to content

Update plugin 隐阅盒 v1.4.0#271

Merged
lzx8589561 merged 2 commits into
ZToolsCenter:mainfrom
me1dlinger:plugin/hushreader
Jun 25, 2026
Merged

Update plugin 隐阅盒 v1.4.0#271
lzx8589561 merged 2 commits into
ZToolsCenter:mainfrom
me1dlinger:plugin/hushreader

Conversation

@me1dlinger

@me1dlinger me1dlinger commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

插件信息

  • 名称: 隐阅盒
  • 插件ID: hushreader
  • 版本: 1.4.0
  • 描述: ZTools自己的摸鱼阅读,支持TXT/EPUB/MOBI格式,沉浸式阅读
  • 作者: meidlinger
  • 类型: 更新

本次变更

1.4.0 - 2026-06-23

Added

  • 已读完标记:读完最后一章时自动标记为"已读完",书籍卡片封面右上角显示"已读完"角标
  • 标记为未读完:右键菜单新增"标记为未读完"选项,可清除已读完状态(仅已读完书籍显示)
  • 书架统计栏:书架顶部新增统计栏,显示总书籍数、已读数、累计阅读时长
  • 书签功能:隐阅窗口右键菜单新增"添加书签"选项,自动提取当前页首句作为书签内容
  • 书签快捷键:隐阅窗口支持可配置的添加书签快捷键(默认 Shift+F)
  • 书签列表:书架书籍右键菜单新增"书签列表"选项,显示日期、阅读进度和内容预览
  • 书签跳转:双击书签可打开隐阅窗口并跳转到书签位置
  • 书签删除:书签列表中每条书签支持直接删除,无需确认
  • 销毁隐阅窗口:新增可配置的销毁快捷键(默认 Shift+D),快速关闭隐阅窗口
  • 全文搜索跳转:书籍右键菜单新增"搜索跳转"选项,可搜索全书内容,按句子展示结果,支持分页浏览、关键词高亮、双击跳转到对应位置
  • 批量设置分类:多选模式下新增"批量设置分类"按钮,可统一为选中书籍设置分类
  • 卡片/列表视图切换:书架头部新增视图切换按钮,支持卡片视图与列表视图一键切换,移除设置界面"其他设置"中的"列表书架模式"开关

Changed

  • 多选按钮图标:多选按钮默认图标改为勾选框样式,与视图切换按钮做视觉区分
  • 列表模式已读完标识:列表模式下已读完标识从封面移至文字信息行,避免挤占小封面空间

Fixed

  • 阅读百分比无法达到100%:修复翻到最后一页时百分比达不到100%的问题,最后一页使用页末字符偏移计算进度
  • 恢复封面未删除自定义封面:修复 TXT 书籍恢复封面时自定义封面未从 IndexedDB 删除,导致重启后仍加载自定义封面的问题
  • 多选模式已读完角标重叠:修复多选模式下已读完角标与勾选框重叠的问题
  • 搜索 XSS 隐患:修复全文搜索结果使用 v-html 渲染未转义内容导致的 XSS 安全隐患
  • 搜索性能问题:修复全文搜索循环中每次迭代重复调用 toLowerCase() 导致大文件 OOM 和卡死的问题,将小写转换提取到循环外并限制句子边界查找窗口

截图 / 演示

自检清单

  • plugin.json 的 name / title / version / description / author 字段均已检查
  • 已移除调试日志、未使用文件、敏感信息(.env、token、密钥等)
  • 本次 PR 的 diff 仅涉及 plugins/hushreader/ 目录
  • 已在本地 ZTools 客户端实际加载并测试过此插件,主要功能正常
  • 同意以仓库声明的开源协议发布此插件

此 PR 由 ztools-plugin-cli 自动管理:每次 ztools publish 在分支上追加一个 commit,PR 链接保持不变。

- feat: 初始化ZTools插件项目
- feat: 实现隐阅盒阅读器插件
- chore(gitignore): add ignore rules
- chore: 相关标识重命名
- chore: add changelog
- fix: 修复书架导入书籍异常未捕获、完善本地存储数据兼容逻辑
- docs: 更新README
- docs: 重写README
- feat: 发布1.1.0版本,新增多项功能并修复多个bug
- fix(窗口拖动): 修复阅读窗口拖动拉伸卡顿问题
- chore: bump plugin version to 1.1.1
- chore: 重命名部分称呼
- feat(setting): add plain cover option, remove epub cache storage
- feat: 新增MOBI电子书格式支持
- feat: 迁移封面和章节缓存到ztools.db
- feat: 新增主题模式切换功能,优化深色主题样式
- chore: release v1.3.0, update plugin info and changelog
- style: 优化窗口进度显示
- chore: 发布v1.3.1版本,更新功能与修复问题
- chore: bump plugin version to 1.3.1
- fix: 修复多个已知问题并优化部分功能
- style(settings): 优化设置项的封面提示文案
- fix(bookshelf): 修复纯色封面关闭后MOBI封面未恢复的问题
- docs: 更新README中的截图
- feat(bookshelf): 实现拖拽导入书籍功能
- feat(bookshelf): 增加快捷导入命令
- chore: update gitignore and changelog
- feat(tips): 增加提示文本 ,更新版本标识
- feat: 新增多选批量操作、重载元数据和恢复封面功能
- style(plugin.json): 调整插件命令列表的排序顺序
- feat: 新增书籍信息弹窗与阅读数据追踪,优化元数据解析
- refactor: 完成bug修复 ### 详细变更 1. 修复MOBI封面解析:将Blob URL替换为可持久化的Base64格式,解决IndexedDB缓存失效问题 2. 优化书籍导入查重逻辑:通过文件名匹配避免重复导入同书籍 3. 重构阅读时长与速度统计:新增会话计时器避免跨会话闲置时间被计入,修复首页阅读时间丢失问题 4. 完善元数据加载容错:文件读取失败时不再静默清空原有元数据,抛出明确错误提示 5. 优化书籍信息编辑交互:新增分类批量添加、快捷键触发功能,修复分类选择UI缺陷
- docs: update README
- release: bump version to 1.3.2 and update docs
- feat: 发布v1.3.3版本,新增多项功能与优化
- release: bump version to 1.3.3
- refactor(settings): 分离配置和书籍的导入导出,修复配置导入问题
- fix: 修复多项问题并调整阅读定时器通知逻辑
- fix(Settings): 为新书籍数组添加类型声明,修复build错误
- feat: 发布v1.4.0版本,新增书签、已读标记等功能
- feat(bookshelf): 新增全文搜索跳转和批量设置分类功能
- feat(bookshelf): 新增视图切换按钮,增加书记格式标识,优化已读完标识的显示逻辑
@me1dlinger me1dlinger marked this pull request as ready for review June 24, 2026 13:12

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several major features to the hushreader plugin, including a "read finished" marker, a bookshelf statistics bar, bookmarking capabilities with shortcuts, full-text search with jump-to-location, batch category setting, and a card/list view toggle. Key feedback points out a critical XSS vulnerability in the search results rendering, a performance bottleneck in the search loop for large books, a bug where marking a book as unfinished is immediately overridden, a window reference leak on destroy, and a logical improvement for the bookshelf statistics calculation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +532 to +536
function highlightKeyword(text: string, keyword: string): string {
if (!keyword) return text
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return text.replace(new RegExp(escaped, 'gi'), '<mark>$&</mark>')
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

在搜索结果展示中,直接使用 v-html 渲染未经过滤的图书内容和搜索关键词,存在跨站脚本攻击(XSS)的安全隐患。如果图书内容中包含恶意的 HTML 标签(例如 <script><img src=x onerror=...>),在 Electron 环境下运行可能会导致远程代码执行(RCE)等严重安全问题。\n\n建议在进行关键词高亮替换前,先对文本和关键词进行 HTML 实体转义。

function escapeHtml(text: string): string {\n  return text\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/"/g, '&quot;')\n    .replace(/'/g, '&#039;')\n}\n\nfunction highlightKeyword(text: string, keyword: string): string {\n  const safeText = escapeHtml(text)\n  if (!keyword) return safeText\n  const safeKeyword = escapeHtml(keyword)\n  const escaped = safeKeyword.replace(/[.*+?^${}()|[\\\/]\\\\]/g, '\\$&')\n  return safeText.replace(new RegExp(escaped, 'gi'), '<mark>$&</mark>')\n}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1

Comment on lines +593 to +613
const kwLower = keyword.toLowerCase()
const results: typeof searchResults.value = []
let pos = 0

while (pos < fullText.length) {
const kwIdx = fullText.toLowerCase().indexOf(kwLower, pos)
if (kwIdx === -1) break

let start = 0
const beforeKw = fullText.slice(0, kwIdx)
let lastBoundary = -1
let m: RegExpExecArray | null
boundaryRe.lastIndex = 0
while ((m = boundaryRe.exec(beforeKw)) !== null) {
lastBoundary = m.index + m[0].length
}
if (lastBoundary >= 0) {
start = lastBoundary
while (start < fullText.length && /[\s\u3000]/.test(fullText[start])) start++
}
if (start > kwIdx) start = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

executeSearchwhile 循环中,每次迭代都调用了 fullText.toLowerCase()。对于大体积的图书(例如数兆字节的 TXT 或 EPUB),这会导致在每次循环中都重新生成一个巨大的小写字符串,极易造成内存溢出(OOM)和界面卡死。\n\n此外,每次都从索引 0 开始截取 beforeKw 并用正则循环扫描,在匹配项较多时会导致 $O(N \times M)$ 的高复杂度计算。\n\n建议将 fullText.toLowerCase() 提取到循环外部,并限制向前查找句子边界的字符窗口(例如最多向前查找 200 个字符),从而将复杂度降低到 $O(1)$

    const kwLower = keyword.toLowerCase()\n    const fullTextLower = fullText.toLowerCase()\n    const results: typeof searchResults.value = []\n    let pos = 0\n\n    while (pos < fullText.length) {\n      const kwIdx = fullTextLower.indexOf(kwLower, pos)\n      if (kwIdx === -1) break\n\n      let start = 0\n      const searchStart = Math.max(0, kwIdx - 200)\n      const beforeKw = fullText.slice(searchStart, kwIdx)\n      let lastBoundary = -1\n      let m: RegExpExecArray | null\n      boundaryRe.lastIndex = 0\n      while ((m = boundaryRe.exec(beforeKw)) !== null) {\n        lastBoundary = m.index + m[0].length\n      }\n      if (lastBoundary >= 0) {\n        start = searchStart + lastBoundary\n        while (start < fullText.length && /[\\s\\u3000]/.test(fullText[start])) start++\n      } else {\n        start = searchStart\n      }\n      if (start > kwIdx) start = searchStart

Comment on lines +416 to +420
function markUnfinished(bookId: string) {
closeContextMenu()
bookStore.updateBook(bookId, { finishedAt: undefined })
toast('已标记为未读完', 'success')
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

markUnfinished 中,仅将 finishedAt 设置为 undefined 是不够的。因为书籍的阅读进度(progressIndex)仍然处于 100% 的完成状态。当用户再次打开该书或触发任何进度保存操作时,saveReadingProgress 会检测到进度为 100% 并立即重新将书籍标记为“已读完”。\n\n建议在标记为未读完时,将 progressIndex 重置为 0(即回到最后一章的起点),以使进度低于 100%,避免被自动重新标记。

function markUnfinished(bookId: string) {\n  closeContextMenu()\n  bookStore.updateBook(bookId, { \n    finishedAt: undefined,\n    progressIndex: 0\n  })\n  toast('已标记为未读完', 'success')\n}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没有这个问题
image_104

Comment thread plugins/hushreader/src/App.vue Outdated
else if (command === 'close') closePlugin()
else if (command === 'auto') toggleAutoPaging()
else if (command === 'close-reader') { isReaderHidden.value = true; blurHushreaderKeyboard() }
else if (command === 'destroy') { saveReadingProgress(); hushreaderActivated.value = false; stopReadingTimer(); hushreaderWindow?.close?.() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在执行 destroy 命令时,虽然调用了 hushreaderWindow?.close?.(),但并没有将 hushreaderWindow 引用置为 null。\n\n在 ensureHushreaderWindow 中,会通过 !hushreaderWindow.isDestroyed?.() 来判断窗口是否存活。如果底层代理对象不支持 isDestroyed 方法(返回 undefined),!undefined 会评估为 true,导致程序误认为旧窗口依然存活而拒绝创建新窗口,从而导致用户无法再次打开阅读器。\n\n建议在关闭窗口后显式将 hushreaderWindow 置为 null

  else if (command === 'destroy') { saveReadingProgress(); hushreaderActivated.value = false; stopReadingTimer(); hushreaderWindow?.close?.(); hushreaderWindow = null }

Comment on lines +1144 to +1146
const statsTotal = computed(() => bookStore.books.length)
const statsRead = computed(() => bookStore.books.filter(b => b.lastReadAt).length)
const statsReadingTimeMs = computed(() => bookStore.books.reduce((sum, b) => sum + (b.readingTimeMs || 0), 0))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在新增的书架统计栏中,statsRead 当前是通过 b.lastReadAt 来过滤已读图书的。这意味着任何只要被打开过一次的书籍都会被计入“已读数”。\n\n由于本次 PR 引入了完善的“已读完”(finishedAt)标记功能,将“已读数”统计口径修改为“已读完的书籍数量”(即 b.finishedAt 不为空)会更加符合用户的直觉和行业通用标准。

const statsTotal = computed(() => bookStore.books.length)\nconst statsRead = computed(() => bookStore.books.filter(b => b.finishedAt).length)\nconst statsReadingTimeMs = computed(() => bookStore.books.reduce((sum, b) => sum + (b.readingTimeMs || 0), 0))

- feat: 初始化ZTools插件项目
- feat: 实现隐阅盒阅读器插件
- chore(gitignore): add ignore rules
- chore: 相关标识重命名
- chore: add changelog
- fix: 修复书架导入书籍异常未捕获、完善本地存储数据兼容逻辑
- docs: 更新README
- docs: 重写README
- feat: 发布1.1.0版本,新增多项功能并修复多个bug
- fix(窗口拖动): 修复阅读窗口拖动拉伸卡顿问题
- chore: bump plugin version to 1.1.1
- chore: 重命名部分称呼
- feat(setting): add plain cover option, remove epub cache storage
- feat: 新增MOBI电子书格式支持
- feat: 迁移封面和章节缓存到ztools.db
- feat: 新增主题模式切换功能,优化深色主题样式
- chore: release v1.3.0, update plugin info and changelog
- style: 优化窗口进度显示
- chore: 发布v1.3.1版本,更新功能与修复问题
- chore: bump plugin version to 1.3.1
- fix: 修复多个已知问题并优化部分功能
- style(settings): 优化设置项的封面提示文案
- fix(bookshelf): 修复纯色封面关闭后MOBI封面未恢复的问题
- docs: 更新README中的截图
- feat(bookshelf): 实现拖拽导入书籍功能
- feat(bookshelf): 增加快捷导入命令
- chore: update gitignore and changelog
- feat(tips): 增加提示文本 ,更新版本标识
- feat: 新增多选批量操作、重载元数据和恢复封面功能
- style(plugin.json): 调整插件命令列表的排序顺序
- feat: 新增书籍信息弹窗与阅读数据追踪,优化元数据解析
- refactor: 完成bug修复 ### 详细变更 1. 修复MOBI封面解析:将Blob URL替换为可持久化的Base64格式,解决IndexedDB缓存失效问题 2. 优化书籍导入查重逻辑:通过文件名匹配避免重复导入同书籍 3. 重构阅读时长与速度统计:新增会话计时器避免跨会话闲置时间被计入,修复首页阅读时间丢失问题 4. 完善元数据加载容错:文件读取失败时不再静默清空原有元数据,抛出明确错误提示 5. 优化书籍信息编辑交互:新增分类批量添加、快捷键触发功能,修复分类选择UI缺陷
- docs: update README
- release: bump version to 1.3.2 and update docs
- feat: 发布v1.3.3版本,新增多项功能与优化
- release: bump version to 1.3.3
- refactor(settings): 分离配置和书籍的导入导出,修复配置导入问题
- fix: 修复多项问题并调整阅读定时器通知逻辑
- fix(Settings): 为新书籍数组添加类型声明,修复build错误
- feat: 发布v1.4.0版本,新增书签、已读标记等功能
- feat(bookshelf): 新增全文搜索跳转和批量设置分类功能
- feat(bookshelf): 新增视图切换按钮,增加书记格式标识,优化已读完标识的显示逻辑
- fix: 修复多个bug,重构搜索逻辑优化性能,修复XSS安全隐患、封面残留和多选布局重叠等问题,完善窗口销毁逻辑和书架统计规则
@lzx8589561 lzx8589561 merged commit 99a54b5 into ZToolsCenter:main Jun 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants