# 书签能力方案讨论 ## 需求背景 `Doc/EPUB_MAINTENANCE.md` 已将“书签能力”列为 EPUB 阅读器下一阶段的高优先级能力之一。 当前阅读器已经具备: 1. EPUB 打开与解析 2. WebView / DTCoreText / fixed-layout 三条正文渲染路径 3. 目录跳转、阅读位置恢复、主题与字号调整 4. 高亮、划线、批注、搜索等阅读交互能力 但现阶段“位置记录”仍主要依赖: 1. 自动保存的上次阅读位置 2. 需要选中文本的高亮 / 批注能力 这会遗漏一种非常常见、也非常轻量的阅读诉求: 1. 只想标记当前位置 2. 稍后回来继续读 3. 不需要摘录正文 4. 不需要输入批注 因此,“书签能力”这项需求的目标,不是扩展现有标注功能,而是补上一条面向“位置收藏”的轻量链路,用来覆盖“只记录位置、不做摘录”的常见阅读需求。 ## 代码事实 ### 已有基础 当前仓库其实已经具备不少可直接复用的基础设施: 1. `RDEPUBLocation` 已能稳定表达 `href + progression + fragment` 2. `RDEPUBReaderController` 已具备“当前位置读取”和“跳转到位置”的主链路 3. `RDEPUBReaderPersistence` 已形成按 `bookIdentifier` 分书持久化的模式 4. 高亮管理页已经具备“列表展示 -> 点击跳转 -> 删除”的成熟交互范式 结论: 1. 书签不需要重新设计位置模型 2. 书签可以复用现有定位和跳转能力 3. 更需要新增的是独立模型、持久化存储位和 UI 入口 ### 当前实现存在的问题 #### 问题 1:当前只有单一阅读位置,没有“多书签”能力 `RDEPUBReaderPersistence` 当前只提供: 1. `loadLocation / saveLocation` 2. `loadHighlights / saveHighlights` 3. `loadReaderSettings / saveReaderSettings` 这意味着: 1. 系统只能保存“上次读到哪” 2. 不能保存“用户主动收藏的多个位置” 而这两者的语义并不相同: 1. 阅读位置是自动覆盖的 2. 书签是主动创建、可长期保留多个的 #### 问题 2:当前标注模型不适合直接承载书签 虽然看起来可以用 `RDEPUBHighlight` 勉强模拟书签,但这会带来明显问题: 1. 书签不一定有选中文本 2. 书签不应依赖 `rangeInfo` 3. 书签列表应以“位置”为中心,而不是以“摘录内容”为中心 4. 书签与高亮、划线、批注的展示和管理语义不同 因此,不建议把书签硬塞进现有高亮模型。 #### 问题 3:当前 UI 没有书签入口与状态反馈 从 `RDEPUBReaderBottomToolView` 当前结构来看,只有: 1. 目录 2. 批注列表 3. 添加标注 4. 设置 当前并没有: 1. 当前页添加 / 取消书签入口 2. 书签列表入口 3. 当前阅读位置是否已加书签的状态反馈 这意味着即便补了数据层,用户仍然无法自然感知和使用这项能力。 ## 需求拆解 建议把“书签能力”拆成四部分推进。 ### 1. 独立书签模型 需要新增一个面向“位置记录”的独立模型,而不是复用高亮模型。 ### 2. 独立书签持久化 需要新增书签读写接口,支持每本书存储多个书签。 ### 3. 书签入口与状态反馈 需要让用户在当前页能轻量创建 / 取消书签,并能看到当前位置的书签状态。 ### 4. 书签管理与跳转 需要补齐书签列表、点击跳转、删除等基本管理闭环。 ## 技术路线对比 ### 路线 A:把书签并入 `RDEPUBHighlight` 思路: 1. 给 `RDEPUBHighlight` 增加一个新的 style 或特殊标识 2. 将书签存入 highlights 3. 复用现有标注列表页 优点: 1. 表面上改动范围较小 2. 可以快速借用现有列表管理代码 风险: 1. 模型语义不清晰 2. 书签没有文本选区时会很别扭 3. 后续展示、筛选、持久化逻辑会越来越绕 4. 容易让“书签”和“标注”边界变得混乱 ### 路线 B:完全独立一条书签链路 思路: 1. 新建 `RDEPUBBookmark` 2. 新增 `loadBookmarks / saveBookmarks` 3. 新增书签列表页 4. 独立接入添加、删除、跳转、状态更新 优点: 1. 模型语义清晰 2. 更符合产品能力边界 3. 后续扩展空间更好 风险: 1. 会新增一部分 UI 与管理代码 2. 与高亮列表在形态上会有一定重复 ### 路线 C:模型独立,交互模式局部复用 思路: 1. 数据模型与持久化独立 2. 定位、跳转和列表交互模式尽量复用现有高亮链路 3. 第一阶段先做轻量闭环,再考虑更复杂管理能力 优点: 1. 语义清晰,同时控制改动范围 2. 能复用现有阅读位置链路和管理交互经验 3. 更适合当前仓库的演进方式 风险: 1. 仍需补一组新的数据与列表代码 2. 需要明确书签状态判定规则,避免重排后误判 ### 推荐结论 建议采用: **主路线:路线 C,模型独立,位置链路复用,UI 先做轻量闭环** 原因: 1. 书签和标注的语义不同,模型应保持独立 2. 当前仓库已有成熟的位置和跳转主链路,可以直接复用 3. 第一阶段先满足“记录位置”的核心需求,不需要把书签做得过重 ## 推荐方案 ### 方案总览 书签能力第一阶段建议形成如下结构: 1. 新增 `RDEPUBBookmark` 独立模型 2. 扩展 `RDEPUBReaderPersistence`,新增 bookmarks 读写 3. 在 `RDEPUBReaderController` 中接入书签状态、添加、删除、跳转能力 4. 增加轻量级书签入口和书签列表页 5. 支持退出重进后的书签持久化恢复 ### 推荐数据模型 建议书签模型至少包含以下字段: 1. `id` 2. `bookIdentifier` 3. `location` 4. `chapterTitle` 5. `displayText` 或摘要信息(可选) 6. `note`(可选) 7. `createdAt` 其中第一阶段真正必要的最小集合是: 1. `id` 2. `bookIdentifier` 3. `location` 4. `createdAt` 建议同时补上 `chapterTitle`,这样书签列表在展示时会更友好,也更接近真实阅读器使用习惯。 ### 推荐持久化结构 建议在 `RDEPUBReaderPersistence` 中新增: 1. `loadBookmarks(for:)` 2. `saveBookmarks(_:for:)` `RDEPUBUserDefaultsPersistence` 中新增: 1. `bookmarksPrefix` 第一阶段继续沿用当前: 1. `UserDefaults` 2. `JSONEncoder / JSONDecoder` 3. `bookIdentifier` 作用域隔离 这样能在最小改动下形成可用闭环。 ### 推荐 UI 入口 第一阶段建议至少补两类入口: 1. 当前页添加 / 取消书签入口 2. 书签列表入口 比较自然的放置方式是: 1. 顶部工具栏增加“当前页书签”切换按钮 2. 底部工具栏增加“书签列表”入口 这样可以把两种动作区分开: 1. “加书签”是当前上下文动作 2. “看书签列表”是全局管理动作 ### 推荐交互闭环 第一阶段建议用户路径收敛成下面这条主链路: 1. 用户在当前阅读位置点击书签按钮 2. 如果当前语义位置未加书签,则创建书签 3. 如果当前语义位置已加书签,则取消该书签 4. 用户可从书签列表查看当前书籍的全部书签 5. 点击任意书签后跳转到对应位置 6. 支持从列表中删除书签 ## 关键实现建议 ### Task K1:新增书签模型 文件: 1. `Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift` 改动: 1. 新增 `RDEPUBBookmark: Codable, Equatable` 2. 字段以 `location` 为核心,不依赖选区文本 完成定义: 1. 可以表达一本书中的一个可持久化书签位置 ### Task K2:扩展书签持久化协议 文件: 1. `Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift` 改动: 1. 协议增加 `loadBookmarks / saveBookmarks` 2. `RDEPUBUserDefaultsPersistence` 增加 `bookmarksPrefix` 完成定义: 1. 每本书可以稳定读取和保存多个书签 ### Task K3:控制器接入书签状态与操作 文件: 1. `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift` 改动: 1. 增加 `activeBookmarks` 2. 打开图书时加载 bookmarks 3. 增加 `addBookmark` 4. 增加 `removeBookmark` 5. 增加 `toggleBookmark` 6. 增加 `go(to: bookmark)` 7. 增加当前阅读位置是否已书签的状态计算 完成定义: 1. 书签与阅读主链路、跳转链路、持久化链路打通 ### Task K4:补书签 UI 与列表管理 文件: 1. `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift` 2. 新增 `Sources/RDReaderView/EPUBUI/RDEPUBReaderBookmarksViewController.swift` 3. 视情况补充顶部工具栏按钮 改动: 1. 添加书签入口 2. 添加书签列表入口 3. 点击书签跳转 4. 支持删除 5. 提供当前位置书签状态反馈 完成定义: 1. 用户能看到、添加、取消、查看和管理书签 ## 当前页书签判定建议 “当前位置是否已加书签”不建议简单按“完全相同的 `progression`”判断,因为: 1. 字号变化会触发重分页 2. 视口尺寸变化会导致 progression 细微漂移 3. WebView 和文本分页的进度精度并不完全一致 更合理的判定策略是: 1. 优先比较标准化后的 `href` 2. 如果存在 `fragment`,优先使用 `fragment` 作为更强锚点 3. reflowable 路径允许一定 progression 容差 4. fixed-layout 路径优先按页级位置判断 这样可以减少用户在重排、重进或轻微位置变化时的书签状态抖动。 ## 验收矩阵建议 第一阶段至少要覆盖以下验证维度: 1. 当前页可添加书签 2. 同一位置可取消书签 3. 书签列表展示正常 4. 点击书签可以跳转 5. 删除书签后列表更新正确 6. 退出重进后书签仍存在 7. 字号变化后书签仍能回到同章节或邻近语义位置 8. `webInteractive` 路径正常 9. `textReflowable` 路径正常 10. `webFixedLayout` 路径至少能按页恢复 ## 风险与注意点 ### 风险 1:把书签与高亮强行合并 这样短期看似省事,长期会让模型、持久化和 UI 语义都变重。 ### 风险 2:过度依赖精确 progression 匹配 如果书签命中规则太严格,重分页后会显得“不稳定”,用户体验会很差。 ### 风险 3:第一阶段把书签做得过重 书签能力的首要目标是“轻量记录位置”,不是一开始就做成复杂的收藏管理系统。 ## 推荐实施顺序 如果目标是“最小投入换最大需求闭环”,建议按下面顺序推进: 1. 先补独立 `RDEPUBBookmark` 模型 2. 先扩展 `RDEPUBReaderPersistence` 的书签读写能力 3. 再在 `RDEPUBReaderController` 接入书签主链路 4. 最后补 UI 入口和书签列表页 5. 待第一阶段稳定后,再考虑备注、排序、筛选等增强能力 ## 推荐结论 “书签能力”这项需求,建议最终收敛为下面这句话: **为 EPUB 阅读器补齐一条独立的轻量书签链路,以 `RDEPUBLocation` 为核心记录位置,支持添加、取消、列表管理、跳转与持久化恢复,用来覆盖“只记录位置、不做摘录”的常见阅读需求。**