From a5044818174994b43faafdba8ecae93d7a9b4711 Mon Sep 17 00:00:00 2001 From: meidlinger Date: Fri, 19 Jun 2026 16:52:51 +0800 Subject: [PATCH 1/3] =?UTF-8?q?Update=20plugin=20=E9=9A=90=E9=98=85?= =?UTF-8?q?=E7=9B=92=20v1.3.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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): 分离配置和书籍的导入导出,修复配置导入问题 --- plugins/hushreader/CHANGELOG.md | 14 + plugins/hushreader/README.md | 60 +- plugins/hushreader/package.json | 2 +- plugins/hushreader/public/hushreader.html | 59 ++ plugins/hushreader/public/plugin.json | 2 +- plugins/hushreader/public/preload/services.js | 13 + plugins/hushreader/src/App.vue | 36 +- .../src/components/Bookshelf/ContextMenu.vue | 5 + .../src/components/Bookshelf/ThemeToggle.vue | 66 +- .../src/components/Bookshelf/index.vue | 20 +- .../src/components/Settings/index.vue | 676 ++++++++++++++++-- plugins/hushreader/src/env.d.ts | 2 + plugins/hushreader/src/main.ts | 11 +- plugins/hushreader/src/stores/books.ts | 4 +- plugins/hushreader/src/stores/config.ts | 92 ++- 15 files changed, 946 insertions(+), 116 deletions(-) diff --git a/plugins/hushreader/CHANGELOG.md b/plugins/hushreader/CHANGELOG.md index be4c5bb6..0b99b45f 100644 --- a/plugins/hushreader/CHANGELOG.md +++ b/plugins/hushreader/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.3.3](https://github.com/me1dlinger/hushreader/releases/tag/v1.3.3) - 2026-06-19 + +### Added +- **自动检测系统深浅色模式**:在插件加载时,根据系统深浅色模式自动切换主题,支持手动切换主题 +- **资源管理器打开文件位置**:在书籍右键菜单新增"打开文件位置"选项,使用ztools.shellShowItemInFolder() +- **隐阅定时器**:新增设置项,打开定时器后可设置一个时长,隐阅时间到达之后自动关闭隐阅窗口并Toast提示用户 +- **备份和恢复所有阅读进度与配置**:备份和恢复所有阅读进度与配置。插件设置导出,所有书籍的元数据导出。实现设置导入,书籍数据导入 +- **多配置切换**:实现多配置切换、添加和删除(最少保留一个配置),导入的配置自动添加为新配置 + +### Fixed +- **配置无法导入的问题**:修复配置无法导入的问题 + +### Changed +- **分离配置和书籍的导入导出**:将配置和书籍的导入导出分离,配置导入导出时仅和配置相关,书籍导入导出时仅和书籍数据相关 ## [1.3.2](https://github.com/me1dlinger/hushreader/releases/tag/v1.3.2) - 2026-06-18 diff --git a/plugins/hushreader/README.md b/plugins/hushreader/README.md index 9789cae1..534623c2 100644 --- a/plugins/hushreader/README.md +++ b/plugins/hushreader/README.md @@ -10,14 +10,22 @@ *** +[更新日志](./CHANGELOG.md) + +*** + ## 功能一览 | 阅读体验 | 个性化 | 书架管理 | | :------------------ | :--------------- | :---------------- | -| 沉浸式悬浮窗口,可置于任意位置 | 背景透明度与整体透明度分离控制 | 支持按添加时间、书名、作者排序 | -| 滚轮翻页 / 快捷键翻页 / 自动翻页 | 自定义字体,支持添加系统字体 | 右键编辑元数据(标题、作者) | +| 沉浸式悬浮窗口,可置于任意位置 | 背景透明度与整体透明度分离控制 | 支持按添加时间、书名、作者、最近阅读排序 | +| 滚轮翻页 / 快捷键翻页 / 自动翻页 | 自定义字体,支持添加系统字体 | 右键编辑元数据(标题、作者、分类) | | 只显示完整行,文字不残缺 | 十六进制颜色输入 + 颜色选择器 | 阅读进度持久化,关闭再开继续读 | -| 文本预处理:压缩空行、清理空白 | 设置实时预览,取消可还原 | — | +| 文本预处理:压缩空行、清理空白 | 设置实时预览,取消可还原 | 拖拽导入 / 快捷文件导入(`导入书籍`) | +| 鼠标移出隐藏三模式(关闭/仅进度/全隐藏) | 亮色 / 暗色主题切换 | 书籍信息窗口(封面、简介、分类、阅读统计) | +| 鼠标移入显示延迟可调 | 列表书架模式 | 多选模式 + 批量重载/删除 | +| 百分比进度编辑跳转 | 窗口大小锁定 | 重载元数据 / 恢复封面 | +| 章节列表高亮当前进度 | — | 书籍分类筛选栏 | ## 截图 @@ -34,7 +42,8 @@ ### 安装 -下载release文件,在ZTools中导入 +- 已上架市场,ZTools插件市场搜索**隐阅盒**,点击**安装**按钮 +- 下载github-release文件,在ZTools**搜索框**完成导入 ### 开发 @@ -48,7 +57,7 @@ npm run build # 构建生产版本 在 ZTools 中输入以下关键词即可唤起: -`yyh` · `摸鱼阅读` · `隐阅盒` · `书架` · `hushreader` +`隐阅盒` · `hushreader` · `摸鱼阅读` · `书架` · `yyh` · `开始阅读` · `导入书籍`(支持拖入 txt/epub/mobi 文件) ## 技术细节 @@ -57,18 +66,27 @@ npm run build # 构建生产版本 阅读进度使用**字符偏移量**(`progressIndex`)而非页码保存,窗口大小或字体变化时进度不会丢失。 -**存储策略**:优先使用 `ztools.dbStorage`,回退到 `localStorage`。 +**存储策略**:轻量数据(书籍列表不含封面、阅读进度、配置)使用 `ztools.dbStorage`,回退到 `localStorage`;封面和章节内容使用 `ztools.db`(PouchDB 风格数据库),通过 `ztools.db.promises` API 管理 `cover_{bookId}`、`custom_cover_{bookId}`、`chapters_{bookId}` 等文档。 **保存时机**:翻页时 · 章节切换时 · 自动翻页 tick · 插件退出时 **每本书保存**: -| 字段 | 说明 | -| --------------- | -------- | -| `lastChapter` | 当前章节索引 | -| `progressIndex` | 章节内字符偏移量 | -| `lastReadAt` | 最后阅读时间戳 | -| `totalChapters` | 总章节数 | +| 字段 | 说明 | +| ------------------ | ----------- | +| `lastChapter` | 当前章节索引 | +| `progressIndex` | 章节内字符偏移量 | +| `lastReadAt` | 最后阅读时间戳 | +| `totalChapters` | 总章节数 | +| `firstReadAt` | 首次阅读时间戳 | +| `readingTimeMs` | 累计阅读时长(毫秒) | +| `readingSpeed` | 阅读速度(字/分钟) | +| `readingPercent` | 阅读百分比 | +| `updatedAt` | 更新时间戳 | +| `description` | 书籍简介 | +| `categories` | 分类数组 | +| `customCoverImage` | 自定义封面 | +| `fileModifiedAt` | 文件修改时间 | @@ -177,14 +195,14 @@ HTML/纯文本判断 → 分章解析或按 TXT 逻辑处理 ├── src/ │ ├── App.vue # 根组件 │ ├── main.ts # 应用入口 -│ ├── main.css # 应用样式 -│ ├── env.d.ts #环境变量类型定义 +│ ├── main.css # 应用样式 +│ ├── env.d.ts # 环境变量类型定义 │ ├── stores/ │ │ ├── books.ts # 书籍数据 + 持久化 │ │ ├── config.ts # 配置数据 + 持久化 │ │ └── reader.ts # 阅读器状态 + 分页 │ ├── utils/ -│ │ ├── db.ts # 数据库操作工具 +│ │ ├── db.ts # 数据库操作工具(封面/章节缓存) │ │ ├── txtParser.ts # TXT 解析 + 分页 + 预处理 │ │ ├── epubParser.ts # EPUB 解析 │ │ └── mobiParser.ts # MOBI 解析 + 加密检测 + 封面提取 @@ -192,17 +210,21 @@ HTML/纯文本判断 → 分章解析或按 TXT 逻辑处理 │ ├── Bookshelf/ # 书架组件 │ │ ├── index.vue │ │ ├── BookCard.vue -│ │ ├── ContextMenu.vue -│ │ ├── Modal.vue -│ │ └── Toast.vue +│ │ ├── BookInfoModal.vue # 书籍信息窗口 +│ │ ├── ContextMenu.vue # 右键菜单 +│ │ ├── Modal.vue # 通用弹窗 +│ │ ├── ThemeToggle.vue # 主题切换 +│ │ └── Toast.vue # Toast 提示 │ └── Settings/ # 设置面板 │ └── index.vue ├── .gitignore +├── CHANGELOG.md # 更新日志 ├── LICENSE ├── package.json ├── tsconfig.json ├── tsconfig.node.json -└── vite.config.ts +├── vite.config.ts +└── ztools.api.md # ZTools API 文档 ``` ## 开源协议 diff --git a/plugins/hushreader/package.json b/plugins/hushreader/package.json index 3d881799..2065aa5f 100644 --- a/plugins/hushreader/package.json +++ b/plugins/hushreader/package.json @@ -1,7 +1,7 @@ { "name": "hushreader", "private": true, - "version": "1.0.0", + "version": "1.3.3", "type": "module", "scripts": { "dev": "vite", diff --git a/plugins/hushreader/public/hushreader.html b/plugins/hushreader/public/hushreader.html index 8522bf52..ba2b277e 100644 --- a/plugins/hushreader/public/hushreader.html +++ b/plugins/hushreader/public/hushreader.html @@ -384,6 +384,26 @@ background: transparent; color: var(--hushreader-text-color); } + + .hushreader-timer { + position: absolute; + top: 4px; + left: 10px; + z-index: 4; + padding: 1px 6px; + border-radius: 3px; + font-size: 10px; + line-height: 1.3; + opacity: 0; + pointer-events: none; + color: var(--hushreader-text-color); + font-variant-numeric: tabular-nums; + transition: opacity 120ms ease; + } + + .show-timer .hushreader-timer { + opacity: 0.66; + } @@ -396,6 +416,7 @@
+
@@ -408,6 +429,7 @@ const root = document.getElementById("hushreader") const linesNode = document.getElementById("hushreaderLines") const metaNode = document.getElementById("hushreaderMeta") + const timerNode = document.getElementById("hushreaderTimer") const sizeHintNode = document.getElementById("hushreaderSizeHint") const contextMenuNode = document.getElementById("hushreaderContextMenu") @@ -427,6 +449,37 @@ let isKeyboardActive = false let isProgressEditing = false let isAutoPaging = false + let timerRemaining = null + let timerTickInterval = 0 + + function formatTimer(ms) { + if (ms == null || ms <= 0) return '' + const totalSec = Math.ceil(ms / 1000) + const min = Math.floor(totalSec / 60) + const sec = totalSec % 60 + return `${min}:${String(sec).padStart(2, '0')}` + } + + function startTimerTick() { + clearInterval(timerTickInterval) + if (timerRemaining == null || timerRemaining <= 0) { + timerNode.textContent = '' + root.classList.remove('show-timer') + return + } + root.classList.add('show-timer') + timerNode.textContent = formatTimer(timerRemaining) + timerTickInterval = setInterval(() => { + timerRemaining = Math.max(0, timerRemaining - 1000) + if (timerRemaining <= 0) { + clearInterval(timerTickInterval) + timerNode.textContent = '' + root.classList.remove('show-timer') + } else { + timerNode.textContent = formatTimer(timerRemaining) + } + }, 1000) + } function sendCommand(command) { if (window.ztools?.sendToParent) { @@ -719,6 +772,12 @@ isAutoPaging = Boolean(settings.autoFlipEnabled) + const newTimerRemaining = settings.timerEnabled ? settings.timerRemaining : null + if (newTimerRemaining !== timerRemaining) { + timerRemaining = newTimerRemaining + startTimerTick() + } + root.className = [ "hushreader-shell", settings.showHushreaderMeta ? "show-meta" : "", diff --git a/plugins/hushreader/public/plugin.json b/plugins/hushreader/public/plugin.json index ffed5746..158808ac 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.2", + "version": "1.3.3", "main": "index.html", "preload": "preload/services.js", "logo": "logo.png", diff --git a/plugins/hushreader/public/preload/services.js b/plugins/hushreader/public/preload/services.js index 5d0e82fd..8686cfca 100644 --- a/plugins/hushreader/public/preload/services.js +++ b/plugins/hushreader/public/preload/services.js @@ -170,5 +170,18 @@ window.services = { ) fs.writeFileSync(filePath, text, { encoding: 'utf-8' }) return filePath + }, + + writeFileToPath(filePath, content, encoding) { + const fullPath = path.resolve(filePath) + const dir = path.dirname(fullPath) + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(fullPath, content, { encoding: encoding || 'utf-8' }) + return fullPath + }, + + readFileFromPath(filePath) { + const fullPath = path.resolve(filePath) + return fs.readFileSync(fullPath, 'utf-8') } } diff --git a/plugins/hushreader/src/App.vue b/plugins/hushreader/src/App.vue index 51a1c95d..3a051038 100644 --- a/plugins/hushreader/src/App.vue +++ b/plugins/hushreader/src/App.vue @@ -56,6 +56,8 @@ const toastMsg = ref('') const toastType = ref<'info' | 'error' | 'success'>('info') let toastTimer = 0 let autoTimer = 0 +let readingTimer = 0 +let readingTimerStart = 0 let isAutoPageTickRunning = false let hushreaderWindow: AppBrowserWindow | null = null @@ -215,7 +217,9 @@ function getHushreaderPayload(bounds = getHushreaderWindowBounds()) { autoFlipEnabled: hushreaderCfg.value.autoFlipEnabled, fontFamily: hushreaderCfg.value.fontFamily, windowMovable: cfg.value.function.windowMovable, - windowSizeLocked: cfg.value.function.windowSizeLocked + windowSizeLocked: cfg.value.function.windowSizeLocked, + timerEnabled: cfg.value.other.timerEnabled, + timerRemaining: getReadingTimerRemaining() } } } @@ -289,7 +293,6 @@ function ensureHushreaderWindow(options?: { skipShow?: boolean }) { hasShadow: false, focusable: true, acceptFirstMouse: true, - alwayOnTop: true, alwaysOnTop: true, webPreferences: { devTools: false, @@ -313,6 +316,7 @@ function showHushreaderWindow() { function hideHushreaderWindow() { isReaderHidden.value = true blurHushreaderKeyboard() + stopReadingTimer() } function focusHushreaderKeyboard() { @@ -347,9 +351,35 @@ function toast(msg: string, type: 'info' | 'error' | 'success' = 'info') { toastTimer = window.setTimeout(() => { toastMsg.value = '' }, 3000) } +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) +} + +function stopReadingTimer() { + clearTimeout(readingTimer) + readingTimer = 0 + readingTimerStart = 0 +} + +function getReadingTimerRemaining(): number | null { + if (!readingTimerStart || !cfg.value.other.timerEnabled) return null + const elapsed = Date.now() - readingTimerStart + const total = cfg.value.other.timerMinutes * 60 * 1000 + return Math.max(0, total - elapsed) +} + function closePlugin() { isReaderHidden.value = true hushreaderActivated.value = false + stopReadingTimer() try { (window as any).ztools?.outPlugin?.() } catch { } } @@ -475,6 +505,7 @@ async function openBookAndHushreader(bookId: string) { hushreaderActivated.value = true isReaderHidden.value = false ensureHushreaderWindow() + startReadingTimer() nextTick(() => { pushHushreaderState() }) @@ -685,6 +716,7 @@ onMounted(async () => { onBeforeUnmount(() => { saveReadingProgress() window.clearInterval(autoTimer) + stopReadingTimer() offHushreaderCommand?.() }) diff --git a/plugins/hushreader/src/components/Bookshelf/ContextMenu.vue b/plugins/hushreader/src/components/Bookshelf/ContextMenu.vue index bad1a4ab..1b1b114b 100644 --- a/plugins/hushreader/src/components/Bookshelf/ContextMenu.vue +++ b/plugins/hushreader/src/components/Bookshelf/ContextMenu.vue @@ -6,6 +6,7 @@ const emit = defineEmits<{ 'book-info': [] 'chapter-list': [] 'change-path': [] + 'open-file-location': [] 'edit-metadata': [] 'reload-metadata': [] 'set-category': [] @@ -54,6 +55,10 @@ watch( 修改本地路径 +
  • + + 打开文件位置 +
  • 编辑元数据 diff --git a/plugins/hushreader/src/components/Bookshelf/ThemeToggle.vue b/plugins/hushreader/src/components/Bookshelf/ThemeToggle.vue index 65fca67e..e7a7112b 100644 --- a/plugins/hushreader/src/components/Bookshelf/ThemeToggle.vue +++ b/plugins/hushreader/src/components/Bookshelf/ThemeToggle.vue @@ -1,23 +1,64 @@ diff --git a/plugins/hushreader/src/components/Bookshelf/index.vue b/plugins/hushreader/src/components/Bookshelf/index.vue index b03b928e..287c4532 100644 --- a/plugins/hushreader/src/components/Bookshelf/index.vue +++ b/plugins/hushreader/src/components/Bookshelf/index.vue @@ -153,6 +153,18 @@ function confirmPath() { toast('路径已更新', 'success') } +function openFileLocation(bookId: string) { + closeContextMenu() + const book = bookStore.books.find(b => b.id === bookId) + if (!book) return + try { + const result = (window as any).ztools?.shellShowItemInFolder?.(book.filePath) + if (!result) toast('无法打开文件位置', 'error') + } catch { + toast('无法打开文件位置', 'error') + } +} + // Category modal const showCategoryModal = ref(false) const categoryModalBookId = ref(null) @@ -946,10 +958,10 @@ const cfg = computed(() => configStore.config) + @open-file-location="openFileLocation(contextMenuBook!)" @edit-metadata="openMetadataModal(contextMenuBook!)" + @reload-metadata="openReloadMetadata(contextMenuBook!)" @set-category="openCategoryModal(contextMenuBook!)" + @set-cover="openCoverPicker(contextMenuBook!)" @restore-cover="openRestoreCover(contextMenuBook!)" + @delete="openDeleteModal(contextMenuBook!)" @close="closeContextMenu" /> diff --git a/plugins/hushreader/src/components/Settings/index.vue b/plugins/hushreader/src/components/Settings/index.vue index c901b9e9..10dca207 100644 --- a/plugins/hushreader/src/components/Settings/index.vue +++ b/plugins/hushreader/src/components/Settings/index.vue @@ -1,12 +1,25 @@