Skip to content

Update plugin 隐阅盒 v1.3.2#267

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

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

Conversation

@me1dlinger

@me1dlinger me1dlinger commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

插件信息

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

本次变更

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 书籍都会被重新解析一次的问题。
  • 文件读取失败时导致元数据被静默清空:修复了在文件读取失败时,元数据被静默清空的问题,确保元数据完整。
  • 修复书籍信息编辑时分类交互缺陷:修复了在书籍信息编辑时,分类交互存在的缺陷。

截图 / 演示

image_91

自检清单

  • 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: 新增书籍信息弹窗与阅读数据追踪,优化元数据解析
@me1dlinger me1dlinger marked this pull request as ready for review June 18, 2026 08:51

@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 features to the HushReader plugin, including drag-and-drop book import, a shortcut import command, multi-select batch operations, and a detailed book information modal with reading statistics. The review feedback highlights critical issues that should be addressed: a logic defect in reading duration tracking that incorrectly accumulates idle time, a performance bottleneck where MOBI covers are repeatedly parsed due to temporary blob URLs, and potential metadata loss if file reading fails during reload. Additionally, the reviewer suggests fixing an interaction bug in the category editor and replacing unsafe non-null assertions with computed properties for better robustness.

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 356 to 396
function saveReadingProgress() {
const book = bookStore.currentBook
if (!book) return
bookStore.updateBook(book.id, {
const now = Date.now()
const updates: Partial<typeof book> = {
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 (book.lastReadAt && book.lastReadAt > 0) {
const elapsed = now - book.lastReadAt
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))
}
bookStore.updateBook(book.id, updates)
}

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

💡 阅读时长与速度统计逻辑缺陷

当前代码使用 book.lastReadAt 来计算两次保存之间的间隔时间 elapsed。由于 lastReadAt 是持久化存储的上一次保存时间(可能来自几天前的上一次阅读会话),这会导致以下两个严重问题:

  1. 新会话的第一页阅读时间丢失:当用户开启新一次阅读时,elapsed 通常会大于 30 分钟,从而触发 else 分支,导致新会话第一页的阅读时间完全没有被记录。
  2. 闲置/关闭期间的时间被错误计入:如果用户关闭插件并在 30 分钟内重新打开,elapsed 会小于 30 分钟,从而将插件关闭期间的闲置时间错误地累加到 readingTimeMs 中,导致阅读时长虚高,且阅读速度计算极度偏低。

🛠️ 解决方案

引入一个基于内存的会话级活跃时间戳 __hushreaderSessionLastActive。当检测到切换书籍或首次打开时,重置该时间戳。这样可以确保 elapsed 仅精确测量当前会话中的实际阅读时间。

function saveReadingProgress() {
  const book = bookStore.currentBook
  if (!book) return
  const now = Date.now()
  const updates: Partial<typeof book> = {
    lastChapter: readerStore.currentChapterIndex,
    progressIndex: readerStore.progressIndex,
    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)
}

Comment on lines +378 to 381
description: string
chapters: Chapter[]
coverUrl?: string
encrypted?: boolean

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

⚡ MOBI 封面缓存失效与重复解析性能问题

parseMobi 中,封面图片是通过 URL.createObjectURL(blob) 生成的 blob: URL 返回的。
blob: URL 是会话特定的,在应用重启后会失效。这会导致以下连锁反应:

  1. 应用重启后,书架加载缓存的 blob: URL 失败,触发 @error="repairCover"
  2. repairCover 检测到格式为 mobi,直接清空并删除了该封面缓存。
  3. 随后 resolveMobiCovers 发现该书没有封面,于是在每次启动时都重新读取并解析 MOBI 文件来生成新的 blob: URL。

这会导致每次打开书架时,所有 MOBI 书籍都会被重新解析一次,造成严重的 CPU 和磁盘 I/O 开销。

🛠️ 解决方案

建议将 extractCoverUrl 修改为同步将图片字节转换为 Base64 Data URL(类似于 EPUB 的处理方式),以便永久缓存到 IndexedDB 中。

请在 mobiParser.ts 中将 extractCoverUrl 函数修改为:

function extractCoverUrl(
  data: Uint8Array,
  recordOffsets: number[],
  firstImageIndex: number,
  exthRecords: EXTHRecord[]
): string | undefined {
  try {
    const coverRecord = findExthRecord(exthRecords, 201)
    if (!coverRecord) return undefined

    if (firstImageIndex === 0xffffffff) return undefined
    const coverRecordIndex = firstImageIndex + exthRawToUint(coverRecord.raw)
    const start = recordOffsets[coverRecordIndex]
    const end = recordOffsets[coverRecordIndex + 1] ?? data.length
    if (start === undefined || start >= data.length || end > data.length || start >= end) return undefined

    const imageBytes = data.slice(start, end)
    const mime = detectImageMime(imageBytes)
    if (!mime) return undefined

    // 同步转换为 Base64,确保可以被 IndexedDB 永久缓存
    let binary = ''
    const len = imageBytes.byteLength
    for (let i = 0; i < len; i++) {
      binary += String.fromCharCode(imageBytes[i])
    }
    return `data:${mime};base64,${btoa(binary)}`
  } catch {
    return undefined
  }
}

Comment on lines +368 to +433
async function reloadMetadata(bookId: string, silent = false) {
const book = bookStore.books.find(b => b.id === bookId)
if (!book) return

try {
let title = book.title
let author = book.author
let description = book.description || ''
let coverImage: string | undefined
let totalChapters: number | undefined

if (book.format === 'epub') {
const content = window.services?.readFileBinary?.(book.filePath)
if (content) {
const blob = new Blob([content], { type: 'application/epub+zip' })
const file = new File([blob], book.filePath.split(/[\\/]/).pop() ?? 'book.epub')
const result = await parseEpub(file)
title = result.title || title
author = result.author || author
description = result.description || description
totalChapters = result.chapters?.length
if (result.coverUrl && !configStore.config.other.plainTextCover) coverImage = result.coverUrl
if (result.chapters?.length) saveChapters(bookId, result.chapters).catch(() => {})
}
} else if (book.format === 'mobi') {
const content = window.services?.readFileBinary?.(book.filePath)
if (content) {
const blob = new Blob([content], { type: 'application/x-mobipocket-ebook' })
const file = new File([blob], book.filePath.split(/[\\/]/).pop() ?? 'book.mobi')
const result = await parseMobi(file)
if (!result.error) {
title = result.title || title
author = result.author || author
description = result.description || description
totalChapters = result.chapters?.length
if (result.coverUrl && !configStore.config.other.plainTextCover) coverImage = result.coverUrl
if (result.chapters?.length) saveChapters(bookId, result.chapters).catch(() => {})
}
}
} else {
const text = window.services?.readFile(book.filePath) ?? ''
const chapters = parseTxt(text, configStore.config.other.chapterRegex || undefined)
totalChapters = chapters.length
if (chapters.length) saveChapters(bookId, chapters).catch(() => {})
}

const fileModifiedAt = window.services?.getFileModifiedTime?.(book.filePath)
const updates: Partial<typeof book> = { title, author, description: description || undefined, totalChapters, fileModifiedAt, customCoverImage: undefined }

removeCustomCover(bookId).catch(() => {})

if (coverImage) {
updates.coverImage = coverImage
saveCover(bookId, coverImage).catch(() => {})
} else {
updates.coverImage = undefined
removeCover(bookId).catch(() => {})
}

bookStore.updateBook(bookId, updates)
if (!silent) toast(`《${title}》元数据已重载`, 'success')
} catch (e: any) {
if (!silent) toast(`重载失败:${e.message}`, 'error')
throw e
}
}

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

⚠️ 文件读取失败时导致元数据被静默清空

reloadMetadata 中,如果书籍文件暂时不可读(例如外部存储未挂载、文件被占用或路径失效),window.services?.readFileBinaryreadFile 会返回 undefined 或空值。
此时代码并不会抛出异常,而是会静默继续执行,并使用 undefined 覆盖原有的 coverImagecustomCoverImagetotalChapters,从而彻底抹除该书籍已有的封面和章节数

🛠️ 解决方案

在读取文件内容后,应当增加非空校验。如果读取失败,应立即抛出错误中断重载流程,避免破坏已有的元数据。

async function reloadMetadata(bookId: string, silent = false) {
  const book = bookStore.books.find(b => b.id === bookId)
  if (!book) return

  try {
    let title = book.title
    let author = book.author
    let description = book.description || ''
    let coverImage: string | undefined
    let totalChapters: number | undefined

    if (book.format === 'epub') {
      const content = window.services?.readFileBinary?.(book.filePath)
      if (!content) {
        throw new Error('无法读取文件,请检查文件是否存在或路径是否正确')
      }
      const blob = new Blob([content], { type: 'application/epub+zip' })
      const file = new File([blob], book.filePath.split(/[\\/]/).pop() ?? 'book.epub')
      const result = await parseEpub(file)
      title = result.title || title
      author = result.author || author
      description = result.description || description
      totalChapters = result.chapters?.length
      if (result.coverUrl && !configStore.config.other.plainTextCover) coverImage = result.coverUrl
      if (result.chapters?.length) saveChapters(bookId, result.chapters).catch(() => {})
    } else if (book.format === 'mobi') {
      const content = window.services?.readFileBinary?.(book.filePath)
      if (!content) {
        throw new Error('无法读取文件,请检查文件是否存在或路径是否正确')
      }
      const blob = new Blob([content], { type: 'application/x-mobipocket-ebook' })
      const file = new File([blob], book.filePath.split(/[\\/]/).pop() ?? 'book.mobi')
      const result = await parseMobi(file)
      if (result.error) { throw new Error(result.error) }
      title = result.title || title
      author = result.author || author
      description = result.description || description
      totalChapters = result.chapters?.length
      if (result.coverUrl && !configStore.config.other.plainTextCover) coverImage = result.coverUrl
      if (result.chapters?.length) saveChapters(bookId, result.chapters).catch(() => {})
    } else {
      const text = window.services?.readFile(book.filePath)
      if (text === undefined || text === null) {
        throw new Error('无法读取文件,请检查文件是否存在或路径是否正确')
      }
      const chapters = parseTxt(text, configStore.config.other.chapterRegex || undefined)
      totalChapters = chapters.length
      if (chapters.length) saveChapters(bookId, chapters).catch(() => {})
    }

    const fileModifiedAt = window.services?.getFileModifiedTime?.(book.filePath)
    const updates: Partial<typeof book> = { title, author, description: description || undefined, totalChapters, fileModifiedAt, customCoverImage: undefined }

    removeCustomCover(bookId).catch(() => {})

    if (coverImage) {
      updates.coverImage = coverImage
      saveCover(bookId, coverImage).catch(() => {})
    } else {
      updates.coverImage = undefined
      removeCover(bookId).catch(() => {})
    }

    bookStore.updateBook(bookId, updates)
    if (!silent) toast(`《${title}》元数据已重载`, 'success')
  } catch (e: any) {
    if (!silent) toast(`重载失败:${e.message}`, 'error')
    throw e
  }
}

Comment on lines +93 to +109
function saveEdit() {
const newCats = editNewCategory.value
.split(/[,,]/)
.map(s => s.trim())
.filter(Boolean)
const finalCats = [...new Set([...editCategories.value, ...newCats])]

const updates: Partial<Book> = {
title: editTitle.value.trim() || '未命名',
author: editAuthor.value.trim(),
description: editDescription.value.trim() || undefined,
categories: finalCats.length > 0 ? finalCats : undefined,
updatedAt: Date.now()
}
emit('saved', updates)
isEditing.value = false
}

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

🔍 新增分类交互缺陷与死代码

在当前实现中,editCategories.filter(c => !existingCategories.includes(c)) 用于过滤并展示新输入的分类标签。
然而,existingCategories 包含了书架中所有书籍的分类(包括当前书籍)。这意味着:

  1. 当前书籍已有的分类必然存在于 existingCategories 中。
  2. 用户在输入框 editNewCategory 中输入的新分类,在点击“保存”前并不会被添加到 editCategories 数组中。

因此,这个过滤条件永远会返回空数组,导致 .edit-category-tags 区域成为永远无法渲染的死代码

🛠️ 解决方案

实现一个 addCustomCategory 函数,当用户在输入框中输入分类并按下回车、逗号或失去焦点时,立即将其添加到 editCategories 中。这样新分类就能正确渲染为可删除的标签,且交互体验更佳。

function addCustomCategory() {
  const val = editNewCategory.value.trim()
  if (!val) return
  const cats = val.split(/[,,]/).map(s => s.trim()).filter(Boolean)
  cats.forEach(c => {
    if (!editCategories.value.includes(c)) {
      editCategories.value.push(c)
    }
  })
  editNewCategory.value = ''
}

function saveEdit() {
  addCustomCategory()
  const finalCats = [...new Set(editCategories.value)]

  const updates: Partial<Book> = {
    title: editTitle.value.trim() || '未命名',
    author: editAuthor.value.trim(),
    description: editDescription.value.trim() || undefined,
    categories: finalCats.length > 0 ? finalCats : undefined,
    updatedAt: Date.now()
  }
  emit('saved', updates)
  isEditing.value = false
}

<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<input v-model="editNewCategory" class="edit-input" placeholder="新分类,逗号分隔..." style="margin-top: 8px" />

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

🛠️ 绑定新分类输入框的添加事件

配合上述脚本修改,为新分类输入框绑定回车、逗号和失去焦点事件,以便动态将新分类转换为标签。

            <input
              v-model="editNewCategory"
              class="edit-input"
              placeholder="新分类,回车或逗号添加..."
              style="margin-top: 8px"
              @keydown.enter.prevent="addCustomCategory"
              @keydown.comma.prevent="addCustomCategory"
              @blur="addCustomCategory"
            />

Comment on lines +338 to +346
// Book info modal
const showBookInfo = ref(false)
const bookInfoId = ref<string | null>(null)

function openBookInfo(bookId: string) {
closeContextMenu()
bookInfoId.value = bookId
showBookInfo.value = true
}

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

🛡️ 避免非空断言,提升组件安全性

在模板中直接使用 bookStore.books.find(...)! 配合非空断言存在安全隐患。如果书籍在后台被删除或列表更新,可能会导致运行时崩溃。
建议使用 computed 属性来安全地获取当前选中的书籍对象。

// Book info modal
const showBookInfo = ref(false)
const bookInfoId = ref<string | null>(null)

const bookInfoBook = computed(() => bookStore.books.find(b => b.id === bookInfoId.value))

function openBookInfo(bookId: string) {
  closeContextMenu()
  bookInfoId.value = bookId
  showBookInfo.value = true
}

Comment on lines +1112 to +1117
<BookInfoModal
v-if="showBookInfo && bookInfoId"
:book="bookStore.books.find(b => b.id === bookInfoId)!"
@close="showBookInfo = false"
@saved="onBookInfoSaved"
/>

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

🛡️ 使用安全的计算属性传递书籍数据

配合上述脚本修改,将模板中的 :book 绑定改为安全的计算属性 bookInfoBook

    <!-- Book Info Modal -->
    <BookInfoModal
      v-if="showBookInfo && bookInfoBook"
      :book="bookInfoBook"
      @close="showBookInfo = false"
      @saved="onBookInfoSaved"
    />

- 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缺陷
@lzx8589561 lzx8589561 merged commit e55bf5f into ZToolsCenter:main Jun 18, 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