Update plugin 隐阅盒 v1.3.3#268
Conversation
- 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): 分离配置和书籍的导入导出,修复配置导入问题
There was a problem hiding this comment.
Code Review
This pull request introduces several features to the hushreader plugin, including a reading timer, configuration and book backup/restore (import/export), multi-configuration management, automatic system theme detection, and an 'Open file location' context menu option. The review feedback highlights several critical areas for improvement: fixing a performance issue with frequent timer recreation, resolving a reactivity bug in the theme toggle by converting system dark mode detection to a reactive ref, optimizing book import performance by batching database writes, preventing prototype pollution in the deepMerge utility, and adding defensive validation for the timer duration input.
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.
| const newTimerRemaining = settings.timerEnabled ? settings.timerRemaining : null | ||
| if (newTimerRemaining !== timerRemaining) { | ||
| timerRemaining = newTimerRemaining | ||
| startTimerTick() | ||
| } |
There was a problem hiding this comment.
在 hushreaderSetState 中,settings.timerRemaining 是通过 Date.now() 动态计算的,因此每次状态推送(如翻页、鼠标移入移出等)时,其值都会发生微小的变化。这会导致 newTimerRemaining !== timerRemaining 始终为 true,从而在每次状态更新时都频繁地清除并重新创建 setInterval 定时器。
这不仅会带来不必要的性能开销,还会导致倒计时显示出现抖动或卡顿。建议仅在时间偏差较大(例如大于 2 秒)或定时器状态发生根本改变时才重新初始化定时器。
| const newTimerRemaining = settings.timerEnabled ? settings.timerRemaining : null | |
| if (newTimerRemaining !== timerRemaining) { | |
| timerRemaining = newTimerRemaining | |
| startTimerTick() | |
| } | |
| const newTimerRemaining = settings.timerEnabled ? settings.timerRemaining : null | |
| if (newTimerRemaining !== timerRemaining) { | |
| const needsReset = timerRemaining == null || newTimerRemaining == null || Math.abs(newTimerRemaining - timerRemaining) > 2000 | |
| timerRemaining = newTimerRemaining | |
| if (needsReset) { | |
| startTimerTick() | |
| } | |
| } |
| const systemDark = computed(() => getSystemDark()) | ||
|
|
||
| const effectiveTheme = computed(() => { | ||
| if (theme.value === 'auto') return systemDark.value ? 'dark' : 'light' | ||
| return theme.value | ||
| }) |
There was a problem hiding this comment.
systemDark 目前被定义为 computed 属性,但它的计算函数 getSystemDark() 内部读取的是 window.matchMedia 或 ztools 等非响应式数据。在 Vue 中,没有响应式依赖的 computed 属性会被永久缓存,导致系统主题发生变化时,systemDark 和 effectiveTheme 无法自动更新。
建议将 systemDark 改为 ref,并在媒体查询监听器中动态更新它的值,从而完美触发 Vue 的响应式更新。
const systemDark = ref(getSystemDark())
const effectiveTheme = computed(() => {
if (theme.value === 'auto') return systemDark.value ? 'dark' : 'light'
return theme.value
})
| onMounted(() => { | ||
| if (typeof window.matchMedia === 'function') { | ||
| mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') | ||
| mediaHandler = () => { | ||
| if (theme.value === 'auto') applyTheme() | ||
| } | ||
| mediaQuery.addEventListener('change', mediaHandler) | ||
| } | ||
| }) |
There was a problem hiding this comment.
配合将 systemDark 改为 ref 的修改,在系统主题变化的事件监听器中,应当直接更新 systemDark.value。这样 effectiveTheme 的计算属性和对应的 watch 就会自动响应并应用新主题,无需手动调用 applyTheme()。
onMounted(() => {
if (typeof window.matchMedia === 'function') {
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaHandler = (e: MediaQueryListEvent) => {
systemDark.value = e.matches
}
mediaQuery.addEventListener('change', mediaHandler)
}
})
| if (Array.isArray(data.books)) { | ||
| let added = 0 | ||
| let skipped = 0 | ||
| for (const book of data.books) { | ||
| if (bookStore.books.some(b => b.filePath === book.filePath)) { | ||
| skipped++ | ||
| continue | ||
| } | ||
| bookStore.addBook(book) | ||
| added++ | ||
| } | ||
| bookStore.save() |
There was a problem hiding this comment.
在导入书籍的循环中,每次调用 bookStore.addBook(book) 都会触发一次同步的 save() 写入操作(保存到 dbStorage 或 localStorage)。如果用户导入的书籍数量较多,会产生大量连续的同步 I/O 写入,导致严重的界面卡顿甚至假死。
建议在循环中先将待导入的书籍收集起来,最后进行一次批量的 unshift 和 save() 写入,从而大幅提升导入性能。
if (Array.isArray(data.books)) {
let added = 0
let skipped = 0
const newBooks = []
for (const book of data.books) {
const filePathName = book.filePath.split(/[\\/]/).pop() ?? book.filePath
const isDuplicate = bookStore.books.some(b => {
if (b.filePath === book.filePath) return true
const existingName = b.filePath.split(/[\\/]/).pop() ?? b.filePath
return existingName === filePathName
}) || newBooks.some(b => {
if (b.filePath === book.filePath) return true
const existingName = b.filePath.split(/[\\/]/).pop() ?? b.filePath
return existingName === filePathName
})
if (isDuplicate) {
skipped++
continue
}
const now = Date.now()
newBooks.push({
...book,
id: `book_${now}_${Math.random().toString(36).slice(2)}`,
addedAt: now,
updatedAt: now
})
added++
}
if (newBooks.length > 0) {
bookStore.books.unshift(...newBooks)
bookStore.save()
}
| function deepMerge(target: any, source: any): any { | ||
| for (const key of Object.keys(source)) { | ||
| if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { | ||
| if (!target[key]) target[key] = {} | ||
| deepMerge(target[key], source[key]) | ||
| } else { | ||
| target[key] = source[key] | ||
| } | ||
| } | ||
| return target | ||
| } |
There was a problem hiding this comment.
在 deepMerge 函数中,直接遍历并合并 source 的所有属性。如果导入的备份 JSON 文件被恶意篡改,包含 __proto__ 或 constructor 等属性,可能会导致原型链污染(Prototype Pollution)漏洞,在 Electron 环境下甚至可能引发更严重的安全问题。
建议在合并时对这些敏感键名进行过滤防御。
function deepMerge(target: any, source: any): any {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor') continue
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!target[key]) target[key] = {}
deepMerge(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}
| <script setup lang="ts"> | ||
| import { computed, watch } from 'vue' | ||
| import { computed, watch, onMounted, onBeforeUnmount } from 'vue' |
| function startReadingTimer() { | ||
| stopReadingTimer() | ||
| if (!cfg.value.other.timerEnabled || !hushreaderActivated.value) return | ||
| readingTimerStart = Date.now() | ||
| const ms = cfg.value.other.timerMinutes * 60 * 1000 | ||
| readingTimer = window.setTimeout(() => { | ||
| hideHushreaderWindow() | ||
| toast(`阅读定时器已到 ${cfg.value.other.timerMinutes} 分钟`, 'info') | ||
| readingTimerStart = 0 | ||
| }, ms) | ||
| } |
There was a problem hiding this comment.
如果 cfg.value.other.timerMinutes 被用户输入了非法值(如空值、NaN 或小于等于 0 的数字),直接乘以系数会导致 ms 变为 NaN 或负数,这会让 setTimeout 产生非预期的行为。建议在启动定时器前进行安全的类型转换和边界防御检查。
function startReadingTimer() {
stopReadingTimer()
if (!cfg.value.other.timerEnabled || !hushreaderActivated.value) return
const minutes = Number(cfg.value.other.timerMinutes)
if (isNaN(minutes) || minutes <= 0) return
readingTimerStart = Date.now()
const ms = minutes * 60 * 1000
readingTimer = window.setTimeout(() => {
hideHushreaderWindow()
toast(`阅读定时器已到 ${minutes} 分钟`, 'info')
readingTimerStart = 0
}, ms)
}
- 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: 修复多项问题并调整阅读定时器通知逻辑
- 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错误
插件信息
本次变更
1.3.3 - 2026-06-19
Added
Changed
Fixed
截图 / 演示
自检清单
plugins/hushreader/目录此 PR 由 ztools-plugin-cli 自动管理:每次
ztools publish在分支上追加一个 commit,PR 链接保持不变。