diff --git a/plugins/hushreader/.gitignore b/plugins/hushreader/.gitignore index eb8e7331..e0b713c9 100644 --- a/plugins/hushreader/.gitignore +++ b/plugins/hushreader/.gitignore @@ -1,4 +1,6 @@ node_modules dist **/*.zip -test \ No newline at end of file +test +功能说明文档.md +docs/ diff --git a/plugins/hushreader/CHANGELOG.md b/plugins/hushreader/CHANGELOG.md index 76ee9a34..ee7eab6d 100644 --- a/plugins/hushreader/CHANGELOG.md +++ b/plugins/hushreader/CHANGELOG.md @@ -1,6 +1,34 @@ # Changelog All notable changes to this project will be documented in this file. + +## [1.4.0](https://github.com/me1dlinger/hushreader/releases/tag/v1.4.0) - 2026-06-23 + +### Added +- **已读完标记**:读完最后一章时自动标记为"已读完",书籍卡片封面右上角显示"已读完"角标 +- **标记为未读完**:右键菜单新增"标记为未读完"选项,可清除已读完状态(仅已读完书籍显示) +- **书架统计栏**:书架顶部新增统计栏,显示总书籍数、已读数、累计阅读时长 +- **书签功能**:隐阅窗口右键菜单新增"添加书签"选项,自动提取当前页首句作为书签内容 +- **书签快捷键**:隐阅窗口支持可配置的添加书签快捷键(默认 Shift+F) +- **书签列表**:书架书籍右键菜单新增"书签列表"选项,显示日期、阅读进度和内容预览 +- **书签跳转**:双击书签可打开隐阅窗口并跳转到书签位置 +- **书签删除**:书签列表中每条书签支持直接删除,无需确认 +- **销毁隐阅窗口**:新增可配置的销毁快捷键(默认 Shift+D),快速关闭隐阅窗口 +- **全文搜索跳转**:书籍右键菜单新增"搜索跳转"选项,可搜索全书内容,按句子展示结果,支持分页浏览、关键词高亮、双击跳转到对应位置 +- **批量设置分类**:多选模式下新增"批量设置分类"按钮,可统一为选中书籍设置分类 +- **卡片/列表视图切换**:书架头部新增视图切换按钮,支持卡片视图与列表视图一键切换,移除设置界面"其他设置"中的"列表书架模式"开关 + +### Changed +- **多选按钮图标**:多选按钮默认图标改为勾选框样式,与视图切换按钮做视觉区分 +- **列表模式已读完标识**:列表模式下已读完标识从封面移至文字信息行,避免挤占小封面空间 + +### Fixed +- **阅读百分比无法达到100%**:修复翻到最后一页时百分比达不到100%的问题,最后一页使用页末字符偏移计算进度 +- **恢复封面未删除自定义封面**:修复 TXT 书籍恢复封面时自定义封面未从 IndexedDB 删除,导致重启后仍加载自定义封面的问题 +- **多选模式已读完角标重叠**:修复多选模式下已读完角标与勾选框重叠的问题 +- **搜索 XSS 隐患**:修复全文搜索结果使用 v-html 渲染未转义内容导致的 XSS 安全隐患 +- **搜索性能问题**:修复全文搜索循环中每次迭代重复调用 toLowerCase() 导致大文件 OOM 和卡死的问题,将小写转换提取到循环外并限制句子边界查找窗口 + ## [1.3.3](https://github.com/me1dlinger/hushreader/releases/tag/v1.3.3) - 2026-06-19 ### Added diff --git a/plugins/hushreader/README.md b/plugins/hushreader/README.md index 534623c2..ad43f9cd 100644 --- a/plugins/hushreader/README.md +++ b/plugins/hushreader/README.md @@ -16,25 +16,45 @@ ## 功能一览 -| 阅读体验 | 个性化 | 书架管理 | -| :------------------ | :--------------- | :---------------- | -| 沉浸式悬浮窗口,可置于任意位置 | 背景透明度与整体透明度分离控制 | 支持按添加时间、书名、作者、最近阅读排序 | -| 滚轮翻页 / 快捷键翻页 / 自动翻页 | 自定义字体,支持添加系统字体 | 右键编辑元数据(标题、作者、分类) | -| 只显示完整行,文字不残缺 | 十六进制颜色输入 + 颜色选择器 | 阅读进度持久化,关闭再开继续读 | -| 文本预处理:压缩空行、清理空白 | 设置实时预览,取消可还原 | 拖拽导入 / 快捷文件导入(`导入书籍`) | -| 鼠标移出隐藏三模式(关闭/仅进度/全隐藏) | 亮色 / 暗色主题切换 | 书籍信息窗口(封面、简介、分类、阅读统计) | -| 鼠标移入显示延迟可调 | 列表书架模式 | 多选模式 + 批量重载/删除 | -| 百分比进度编辑跳转 | 窗口大小锁定 | 重载元数据 / 恢复封面 | -| 章节列表高亮当前进度 | — | 书籍分类筛选栏 | +### 隐阅窗口 +- 悬浮窗口,可置于任意位置 +- 翻页控制:滚轮翻页 / 快捷键翻页 / 自动翻页 +- 进度管理:百分比进度编辑跳转、章节列表高亮当前进度、书签功能(添加/列表/跳转/删除) +- 全文搜索:搜索全书内容,按句子展示结果,支持分页浏览、关键词高亮、双击跳转 +- 窗口行为:鼠标移出隐藏三模式(不隐藏/仅进度/全隐藏)、移入显示延迟可调、窗口大小锁定 +- 快捷操作:添加书签快捷键(Shift+F)、销毁窗口快捷键(Shift+D) + +### 书架管理 +- 书籍导入:拖拽导入 / 快捷文件导入(`导入书籍`) +- 信息管理:书籍信息窗口(封面、简介、分类、阅读统计)、右键编辑元数据(标题、作者、分类) +- 批量操作:多选模式 + 批量重载/删除/设置分类、重载元数据 / 恢复封面 +- 阅读状态:已读完标记 + 标记为未读完、书架统计栏(总书籍/已读/阅读时长) +- 筛选排序:书籍分类筛选栏、按添加时间/书名/作者/最近阅读排序 + +### 个性化设置 +- 外观控制:背景透明度与整体透明度分离控制、亮色/暗色主题切换、列表书架模式 +- 字体样式:自定义字体(支持添加系统字体)、十六进制颜色输入 + 颜色选择器 +- 配置管理:设置实时预览、取消可还原、多配置方案切换 + +### 技术特性 +- 只显示完整行,文字不残缺 +- 文本预处理:压缩空行、清理空白 +- 阅读进度持久化,关闭再开继续读 ## 截图 -![书架](https://files.seeusercontent.com/2026/06/17/qvX8/image_86.png) -![暗色模式](https://files.seeusercontent.com/2026/06/17/lu4H/image_85.png) -![书籍信息](https://files.seeusercontent.com/2026/06/18/D9di/image_90.png) -![隐阅窗口设置](https://files.seeusercontent.com/2026/06/17/eCu4/image_89.png) -![功能设置](https://files.seeusercontent.com/2026/06/17/0bqW/image_88.png) -![其他设置](https://files.seeusercontent.com/2026/06/17/y3uD/image_87.png) +![书架](https://files.seeusercontent.com/2026/06/24/Pqs5/image_105.png) +![列表书架](https://files.seeusercontent.com/2026/06/24/1xDq/image_110.png) +![暗色模式](https://files.seeusercontent.com/2026/06/24/iR3q/image_106.png) +![右键菜单](https://files.seeusercontent.com/2026/06/24/Mb0u/image_107.png) +![书籍信息](https://files.seeusercontent.com/2026/06/24/6oZd/image_109.png) +![章节列表](https://files.seeusercontent.com/2026/06/24/zc1T/image_111.png) +![书签列表](https://files.seeusercontent.com/2026/06/24/Sur5/image_112.png) +![搜索跳转](https://files.seeusercontent.com/2026/06/24/5Nhl/image_113.png) +![书籍分类](https://files.seeusercontent.com/2026/06/24/7xXx/image_114.png) +![隐阅窗口设置](https://files.seeusercontent.com/2026/06/24/Dl4e/image_115.png) +![功能设置](https://files.seeusercontent.com/2026/06/24/9Nle/image_117.png) +![其他设置](https://files.seeusercontent.com/2026/06/24/Wta6/image_118.png) ![隐阅效果](https://files.seeusercontent.com/2026/06/18/h1Jb/show.gif) @@ -87,6 +107,8 @@ npm run build # 构建生产版本 | `categories` | 分类数组 | | `customCoverImage` | 自定义封面 | | `fileModifiedAt` | 文件修改时间 | +| `isFinished` | 是否已读完 | +| `bookmarks` | 书签列表(章节索引、字符偏移、内容预览、创建时间) | diff --git a/plugins/hushreader/package.json b/plugins/hushreader/package.json index 2065aa5f..fe6a0b30 100644 --- a/plugins/hushreader/package.json +++ b/plugins/hushreader/package.json @@ -1,7 +1,7 @@ { "name": "hushreader", "private": true, - "version": "1.3.3", + "version": "1.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/plugins/hushreader/public/hushreader.html b/plugins/hushreader/public/hushreader.html index 000bc033..b6e48a0d 100644 --- a/plugins/hushreader/public/hushreader.html +++ b/plugins/hushreader/public/hushreader.html @@ -1015,6 +1015,8 @@ const binding = getKeyboardEventBinding(event) const prevPageKey = normalizeKeyBinding(latestSettings.prevPageKey, "ArrowLeft") const nextPageKey = normalizeKeyBinding(latestSettings.nextPageKey, "ArrowRight") + const addBookmarkKey = normalizeKeyBinding(latestSettings.addBookmarkKey, "Shift+F") + const destroyKey = normalizeKeyBinding(latestSettings.destroyKey, "Shift+D") if (binding && binding === nextPageKey) { event.preventDefault() @@ -1022,6 +1024,12 @@ } else if (binding && binding === prevPageKey) { event.preventDefault() sendCommand("prev") + } else if (binding && binding === addBookmarkKey) { + event.preventDefault() + sendCommand("add-bookmark") + } else if (binding && binding === destroyKey) { + event.preventDefault() + sendCommand("destroy") } }) @@ -1161,23 +1169,26 @@ document.addEventListener("contextmenu", (event) => { event.preventDefault() - if (!latestSettings.showHushreaderMeta) return - - const metaRect = metaNode.getBoundingClientRect() - const inMetaArea = event.clientX >= metaRect.left - 4 && event.clientX <= metaRect.right + 4 - && event.clientY >= metaRect.top - 4 && event.clientY <= metaRect.bottom + 4 - - if (!inMetaArea) return const items = [] - items.push({ label: "关闭隐阅窗口", command: "close-reader" }) - items.push({ label: "显示主窗口", command: "show-main" }) - if (isAutoPaging) { - items.push({ label: "停止自动翻页", command: "stop-auto" }) - } else { - items.push({ label: "开启自动翻页", command: "start-auto" }) + if (latestSettings.showHushreaderMeta) { + const metaRect = metaNode.getBoundingClientRect() + const inMetaArea = event.clientX >= metaRect.left - 4 && event.clientX <= metaRect.right + 4 + && event.clientY >= metaRect.top - 4 && event.clientY <= metaRect.bottom + 4 + if (inMetaArea) { + items.push({ label: "关闭隐阅窗口", command: "close-reader" }) + items.push({ label: "添加书签", command: "add-bookmark" }) + items.push({ label: "显示主窗口", command: "show-main" }) + if (isAutoPaging) { + items.push({ label: "停止自动翻页", command: "stop-auto" }) + } else { + items.push({ label: "开启自动翻页", command: "start-auto" }) + } + } } + if (items.length === 0) return + contextMenuNode.innerHTML = "" items.forEach((item) => { const btn = document.createElement("button") diff --git a/plugins/hushreader/public/plugin.json b/plugins/hushreader/public/plugin.json index 47e9bd2e..fed86cf4 100644 --- a/plugins/hushreader/public/plugin.json +++ b/plugins/hushreader/public/plugin.json @@ -4,7 +4,7 @@ "title": "隐阅盒", "description": "ZTools自己的摸鱼阅读,支持TXT/EPUB/MOBI格式,沉浸式阅读", "author": "meidlinger", - "version": "1.3.3", + "version": "1.4.0", "main": "index.html", "preload": "preload/services.js", "logo": "logo.png", diff --git a/plugins/hushreader/src/App.vue b/plugins/hushreader/src/App.vue index 8076d42b..012ff5e6 100644 --- a/plugins/hushreader/src/App.vue +++ b/plugins/hushreader/src/App.vue @@ -3,7 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } f import Bookshelf from './components/Bookshelf/index.vue' import Toast from './components/Bookshelf/Toast.vue' import { useReaderStore } from './stores/reader' -import { useBookStore } from './stores/books' +import { useBookStore, type Bookmark } from './stores/books' import { useConfigStore } from './stores/config' import { parseTxt } from './utils/txtParser' import { parseEpub } from './utils/epubParser' @@ -208,6 +208,8 @@ function getHushreaderPayload(bounds = getHushreaderWindowBounds()) { bgOpacity: hushreaderCfg.value.bgOpacity, prevPageKey: hushreaderCfg.value.prevPageKey, nextPageKey: hushreaderCfg.value.nextPageKey, + addBookmarkKey: hushreaderCfg.value.addBookmarkKey, + destroyKey: hushreaderCfg.value.destroyKey, showHushreaderMeta: hushreaderCfg.value.showHushreaderMeta, progressMode: hushreaderCfg.value.progressMode, hideOnMouseLeave: hushreaderCfg.value.hideOnMouseLeave, @@ -466,6 +468,10 @@ function saveReadingProgress() { updates.lastSaveReadChars = Math.round(totalChars * (readerStore.readingPercent / 100)) } + if (readerStore.readingPercent >= 100 && !book.finishedAt) { + updates.finishedAt = now + } + (window as any).__hushreaderSessionLastActive = now bookStore.updateBook(book.id, updates) } @@ -629,9 +635,45 @@ function handleHushreaderCommand(command: HushreaderCommand) { 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?.(); hushreaderWindow = null } else if (command === 'show-main') { (window as any).ztools?.showMainWindow?.() } else if (command === 'stop-auto') { isAutoPaging.value = false; hushreaderCfg.value.autoFlipEnabled = false } else if (command === 'start-auto') { if (currentBook.value) { isAutoPaging.value = true; hushreaderCfg.value.autoFlipEnabled = true } } + else if (command === 'add-bookmark') { + const book = currentBook.value + if (book) { + const pageText = hushreaderLines.value.join('') + const boundaryRe = /[。!?…!?\.\」\』\)\】\」]/ + let start = 0 + const boundary = boundaryRe.exec(pageText) + if (boundary) { + start = boundary.index + boundary[0].length + while (start < pageText.length && /[\s\u3000]/.test(pageText[start])) start++ + } + if (start >= pageText.length) start = 0 + const rest = pageText.slice(start) + const sentenceEndRe = /[。!?…!?\.]/ + let text = '' + const endMatch = sentenceEndRe.exec(rest) + if (endMatch) { + text = rest.slice(0, endMatch.index + endMatch[0].length).trim() + } else { + text = rest.trim().slice(0, 50) + } + if (text.length > 50) text = text.slice(0, 50) + '...' + const bookmark: Bookmark = { + id: `bm_${Date.now()}_${Math.random().toString(36).slice(2)}`, + chapterIndex: readerStore.currentChapterIndex, + charIndex: readerStore.currentPageSlice.startIndex, + readingPercent: readerStore.readingPercent, + text, + createdAt: Date.now() + } + const existing = book.bookmarks ?? [] + bookStore.updateBook(book.id, { bookmarks: [...existing, bookmark] }) + toast('书签已添加', 'success') + } + } else if (command === 'notification-close') { pendingNotificationCallback?.(); pendingNotificationCallback = null } } @@ -708,6 +750,8 @@ watch( hushreaderCfg.value.bgOpacity, hushreaderCfg.value.prevPageKey, hushreaderCfg.value.nextPageKey, + hushreaderCfg.value.addBookmarkKey, + hushreaderCfg.value.destroyKey, hushreaderCfg.value.showHushreaderMeta, hushreaderCfg.value.progressMode, hushreaderCfg.value.hideOnMouseLeave, diff --git a/plugins/hushreader/src/components/Bookshelf/BookCard.vue b/plugins/hushreader/src/components/Bookshelf/BookCard.vue index 35869d3b..74eb45c4 100644 --- a/plugins/hushreader/src/components/Bookshelf/BookCard.vue +++ b/plugins/hushreader/src/components/Bookshelf/BookCard.vue @@ -35,7 +35,7 @@ function formatDate(ts: number) { function progressText(book: Book): string { if (!book.lastReadAt) return '' - if (book.readingPercent != null) { + if (book.readingPercent != null) { return `${book.readingPercent}%` } if (book.totalChapters && book.lastChapter != null) { @@ -47,36 +47,29 @@ function progressText(book: Book): string {