diff --git a/plugins/hushreader/.gitignore b/plugins/hushreader/.gitignore index b8b54725..eb8e7331 100644 --- a/plugins/hushreader/.gitignore +++ b/plugins/hushreader/.gitignore @@ -1,3 +1,4 @@ node_modules dist -**/*.zip \ No newline at end of file +**/*.zip +test \ No newline at end of file diff --git a/plugins/hushreader/CHANGELOG.md b/plugins/hushreader/CHANGELOG.md index ccce34d0..be4c5bb6 100644 --- a/plugins/hushreader/CHANGELOG.md +++ b/plugins/hushreader/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. +## [1.3.2](https://github.com/me1dlinger/hushreader/releases/tag/v1.3.2) - 2026-06-18 + +### Added +- **拖拽导入书籍**:将书籍文件拖入书架界面时,显示拖拽悬停覆盖层提示"导入书籍",松手后弹出确认弹窗列出待导入文件,确认后解析书籍并保存元数据,取消则放弃导入 +- **快捷文件导入**:支持通过 `hushreader-import` 命令从 ztools 快捷导入书籍文件,`onPluginEnter` 触发时自动解析并添加到书架 +- **TXT 章节正则提示气泡**:设置页"TXT 章节识别正则"标签旁新增 ⓘ 提示图标,悬停/聚焦时显示默认规则说明 +- **重载元数据**:右键菜单新增"重载元数据"选项,重新解析书籍的章节、封面、标题、作者等信息并更新,同时清除自定义封面 +- **恢复封面**:右键菜单新增"恢复封面"选项,EPUB/MOBI 删除自定义封面并重新从文件加载封面,TXT 删除自定义封面恢复为纯色封面 +- **多选模式**:书架头部新增多选按钮,点击进入多选模式,可逐个勾选书籍或通过分类栏"全选"按钮批量选中当前分类下所有书籍 +- **批量操作**:多选模式下底部显示操作栏,支持批量重载元数据和批量删除,批量删除前弹出确认弹窗 + +- **书籍信息窗口**:右键菜单新增"书籍信息"选项,打开信息窗口展示封面与标题作者、简介、分类、阅读信息(导入/更新/首次阅读/最近阅读时间、阅读时长、阅读速度);支持编辑模式可上传自定义封面、编辑标题/作者/简介、选择已有分类或新建分类 +- **EPUB/MOBI 简介解析**:导入和重载元数据时从 EPUB metadata.description 和 MOBI EXTH record 101 提取书籍简介 +- **阅读进度追踪**:保存阅读进度时记录首次阅读时间(firstReadAt)、累计阅读时长(readingTimeMs)、阅读速度(readingSpeed) +- **updatedAt 时间戳**:书籍新增时设置 updatedAt = addedAt,编辑元数据或上传封面时更新 updatedAt,分类变更不更新 updatedAt + +### Fixed +- **修复拖拽文件时的路径保存错误**:修复了在拖拽文件导入书籍时,路径保存错误导致相同书籍可以重复导入的问题 +- **阅读时长与速度统计逻辑修复**:修复了新会话第一页阅读时间丢失和闲置/关闭期间时间被错误计入的问题,确保阅读时长和速度统计准确。 +- **MOBI 封面缓存失效与重复解析性能问题**:修复了每次打开书架时,所有 MOBI 书籍都会被重新解析一次的问题。 +- **文件读取失败时导致元数据被静默清空**:修复了在文件读取失败时,元数据被静默清空的问题,确保元数据完整。 +- **修复书籍信息编辑时分类交互缺陷**:修复了在书籍信息编辑时,分类交互存在的缺陷。 + ## [1.3.1](https://github.com/me1dlinger/hushreader/releases/tag/v1.3.1) - 2026-06-17 ### Added diff --git a/plugins/hushreader/README.md b/plugins/hushreader/README.md index b7ca6d62..9789cae1 100644 --- a/plugins/hushreader/README.md +++ b/plugins/hushreader/README.md @@ -1,6 +1,8 @@
-# 隐阅盒 · HushReader +![HushReader Logo](public/logo.png) + +# [隐阅盒 · HushReader](https://github.com/me1dlinger/hushreader) 适配 [ZTools](https://github.com/ZToolsCenter/ZTools) 的阅读插件,支持 TXT / EPUB / MOBI 格式 @@ -19,12 +21,14 @@ ## 截图 -
- - - - -
+![书架](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/18/h1Jb/show.gif) + ## 快速开始 diff --git a/plugins/hushreader/public/plugin.json b/plugins/hushreader/public/plugin.json index f3822f18..ffed5746 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.1", + "version": "1.3.2", "main": "index.html", "preload": "preload/services.js", "logo": "logo.png", @@ -17,11 +17,11 @@ "explain": "打开书架", "icon": "logo.png", "cmds": [ - "yyh", - "摸鱼阅读", "隐阅盒", "书架", - "hushreader" + "摸鱼阅读", + "hushreader", + "yyh" ] }, { @@ -31,6 +31,25 @@ "cmds": [ "开始阅读" ] + }, + { + "code": "hushreader-import", + "explain": "导入本地书籍", + "icon": "logo.png", + "cmds": [ + "导入书籍", + { + "type": "files", + "fileType": "file", + "extensions": [ + "txt", + "epub", + "mobi" + ], + "maxLength": 1, + "label": "导入到 隐阅盒" + } + ] } ], "platform": [ diff --git a/plugins/hushreader/src/App.vue b/plugins/hushreader/src/App.vue index 43c6bc93..51a1c95d 100644 --- a/plugins/hushreader/src/App.vue +++ b/plugins/hushreader/src/App.vue @@ -350,19 +350,58 @@ function toast(msg: string, type: 'info' | 'error' | 'success' = 'info') { function closePlugin() { isReaderHidden.value = true hushreaderActivated.value = false - try { (window as any).ztools?.outPlugin?.() } catch {} + try { (window as any).ztools?.outPlugin?.() } catch { } } function saveReadingProgress() { const book = bookStore.currentBook if (!book) return - bookStore.updateBook(book.id, { + const now = Date.now() + const updates: Partial = { lastChapter: readerStore.currentChapterIndex, progressIndex: readerStore.progressIndex, - lastReadAt: Date.now(), + lastReadAt: now, totalChapters: readerStore.chapters.length, readingPercent: readerStore.readingPercent - }) + } + if (!book.firstReadAt) { + updates.firstReadAt = now + } + + // 初始化或重置会话计时器,避免将跨会话的闲置时间计入阅读时长 + if ((window as any).__hushreaderSessionBookId !== book.id) { + (window as any).__hushreaderSessionBookId = book.id; + (window as any).__hushreaderSessionLastActive = now + } + + const lastActive = (window as any).__hushreaderSessionLastActive || now + const elapsed = now - lastActive + + if (elapsed > 0 && elapsed < 30 * 60 * 1000) { + const totalMs = (book.readingTimeMs || 0) + elapsed + updates.readingTimeMs = totalMs + const totalChars = readerStore.chapters.reduce((sum, ch) => sum + ch.content.length, 0) + const currentReadChars = Math.round(totalChars * (readerStore.readingPercent / 100)) + const prevReadChars = book.lastSaveReadChars || 0 + const deltaChars = Math.max(0, currentReadChars - prevReadChars) + updates.lastSaveReadChars = currentReadChars + const elapsedMinutes = elapsed / 60000 + if (elapsedMinutes > 0 && deltaChars > 0) { + const sessionSpeed = deltaChars / elapsedMinutes + const prevSpeed = book.readingSpeed || 0 + if (prevSpeed > 0) { + updates.readingSpeed = Math.round(prevSpeed * 0.6 + sessionSpeed * 0.4) + } else { + updates.readingSpeed = Math.round(sessionSpeed) + } + } + } else { + const totalChars = readerStore.chapters.reduce((sum, ch) => sum + ch.content.length, 0) + updates.lastSaveReadChars = Math.round(totalChars * (readerStore.readingPercent / 100)) + } + + (window as any).__hushreaderSessionLastActive = now + bookStore.updateBook(book.id, updates) } function getFileModifiedTime(filePath: string): number | null { @@ -420,7 +459,7 @@ async function openBookAndHushreader(bookId: string) { if (!chapters || fileChanged) { chapters = await parseBookAndGetChapters(book) if (!chapters) return - saveChapters(bookId, chapters).catch(() => {}) + saveChapters(bookId, chapters).catch(() => { }) } readerStore.setChapters(chapters) @@ -624,20 +663,20 @@ watch( } ) -onMounted(() => { - configStore.load() - bookStore.load() - ;(window as any).ztools?.onPluginEnter?.((action: any) => { - route.value = action.code - enterAction.value = action - }) - ;(window as any).ztools?.onPluginOut?.((processExit: boolean) => { - if (processExit) { - saveReadingProgress() - hushreaderActivated.value = false - hushreaderWindow?.close?.() - } - }) +onMounted(async () => { + await configStore.load() + await bookStore.load() + ; (window as any).ztools?.onPluginEnter?.((action: any) => { + route.value = action.code + enterAction.value = action + }) + ; (window as any).ztools?.onPluginOut?.((processExit: boolean) => { + if (processExit) { + saveReadingProgress() + hushreaderActivated.value = false + hushreaderWindow?.close?.() + } + }) offHushreaderCommand = (window as any).services?.onHushreaderCommand?.(handleHushreaderCommand) if (!route.value) route.value = 'bookshelf' @@ -651,8 +690,6 @@ onBeforeUnmount(() => { diff --git a/plugins/hushreader/src/components/Bookshelf/BookCard.vue b/plugins/hushreader/src/components/Bookshelf/BookCard.vue index 87b44676..35869d3b 100644 --- a/plugins/hushreader/src/components/Bookshelf/BookCard.vue +++ b/plugins/hushreader/src/components/Bookshelf/BookCard.vue @@ -5,12 +5,15 @@ import type { Book } from '../../stores/books' const props = defineProps<{ book: Book listMode?: boolean + selectionMode?: boolean + selected?: boolean }>() const emit = defineEmits<{ click: [] contextmenu: [e: MouseEvent] 'cover-error': [] + 'toggle-select': [] }>() const imgError = ref(false) @@ -46,10 +49,14 @@ function progressText(book: Book): string {