399 lines
11 KiB
Markdown
399 lines
11 KiB
Markdown
# 书签能力方案讨论
|
||
|
||
## 需求背景
|
||
|
||
`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` 为核心记录位置,支持添加、取消、列表管理、跳转与持久化恢复,用来覆盖“只记录位置、不做摘录”的常见阅读需求。**
|