docs: 补充注释、修正过时文档、清理重复内容
源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
// RDEPUBReaderController+ContentDelegates.swift
|
||||
// EPUB 阅读器内容视图代理实现
|
||||
// 处理 Web 渲染路径(RDEPUBWebContentViewDelegate)和 Native Text 渲染路径
|
||||
// (RDEPUBTextContentViewDelegate)的位置更新、选区变化、链接跳转、错误日志等事件回调。
|
||||
|
||||
import UIKit
|
||||
|
||||
/// RDEPUBReaderController 内容代理扩展
|
||||
///
|
||||
/// 本文件实现 EPUB 阅读器的内容视图代理,处理 Web 渲染路径和 Native Text 渲染路径的
|
||||
/// 位置更新、选区变化、链接跳转、错误日志等事件回调。
|
||||
|
||||
// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径)
|
||||
|
||||
extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
/// Web 内容视图位置更新回调,同步阅读上下文与持久化位置
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) {
|
||||
guard readerView.currentPage >= 0,
|
||||
activePages.indices.contains(readerView.currentPage) else {
|
||||
@@ -24,6 +35,7 @@ extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
/// Web 内容视图选区变化回调,转换为统一选区模型
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) {
|
||||
if let selection {
|
||||
updateCurrentSelection(scopedSelection(selection, relativeToSpineIndex: spineIndex))
|
||||
@@ -32,10 +44,12 @@ extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Web 内容视图选区菜单操作回调
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {
|
||||
handleSelectionMenuAction(action, selection: currentSelection)
|
||||
}
|
||||
|
||||
/// Web 内容视图内部链接点击回调,执行页内跳转
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) {
|
||||
guard let readingSession,
|
||||
let pageNumber = readingSession.queueNavigation(
|
||||
@@ -48,11 +62,13 @@ extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
readerView.transitionToPage(pageNum: max(pageNumber - 1, 0), animated: true)
|
||||
}
|
||||
|
||||
/// Web 内容视图外部链接点击回调,使用系统浏览器打开
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateExternalLink url: URL) {
|
||||
delegate?.epubReader(self, didActivateExternalLink: url)
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
/// Web 内容视图 JavaScript 错误日志回调
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didLogJavaScriptError message: String) {
|
||||
print("EPUB JS Error: \(message)")
|
||||
}
|
||||
@@ -61,6 +77,7 @@ extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
// MARK: - 文本内容视图代理(Native Text 渲染路径)
|
||||
|
||||
extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
/// 文本内容视图选区变化回调,标准化后更新当前选区
|
||||
func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?) {
|
||||
guard let selection else {
|
||||
updateCurrentSelection(nil)
|
||||
@@ -69,6 +86,7 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
updateCurrentSelection(normalizedTextSelection(selection))
|
||||
}
|
||||
|
||||
/// 文本内容视图选区菜单操作回调
|
||||
func textContentView(
|
||||
_ contentView: RDEPUBTextContentView,
|
||||
didRequestSelectionAction action: RDEPUBAnnotationMenuAction,
|
||||
@@ -79,6 +97,7 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
contentView.clearSelection()
|
||||
}
|
||||
|
||||
/// 标准化文本选区的偏移范围,确保在有效内容长度内
|
||||
private func normalizedTextSelection(_ selection: RDEPUBSelection) -> RDEPUBSelection? {
|
||||
guard let textBook,
|
||||
let chapterData = textBook.chapterData(for: selection.location.href) else {
|
||||
@@ -103,6 +122,7 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据 EPUB 位置计算对应的页码
|
||||
func pageNumber(for location: RDEPUBLocation) -> Int? {
|
||||
if let textBook, let publication {
|
||||
if let anchor = location.rangeAnchor?.start {
|
||||
@@ -130,6 +150,7 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据页码解析对应的文本位置
|
||||
func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? {
|
||||
guard let textBook,
|
||||
let publication,
|
||||
@@ -148,6 +169,7 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
) ?? location
|
||||
}
|
||||
|
||||
/// 同步文本阅读状态到阅读会话(页码、位置、spine、章节等)
|
||||
func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) {
|
||||
guard let textBook,
|
||||
let page = textBook.page(at: pageNumber) else {
|
||||
@@ -164,6 +186,7 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
/// 从文本书籍生成原生文本快照(页面列表 + 章节信息)
|
||||
func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> RDEPUBNativeTextSnapshot {
|
||||
let chapters = textBook.chapterInfos
|
||||
let pages = textBook.pages.map {
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
// RDEPUBReaderController+DataSource.swift
|
||||
// EPUB 阅读器数据源与代理实现
|
||||
// 实现 RDReaderDataSource 和 RDReaderDelegate 协议,为阅读器视图提供页面数量、
|
||||
// 页面内容视图、页码变化通知、屏幕方向变化处理等核心数据与事件支持。
|
||||
|
||||
import UIKit
|
||||
|
||||
/// RDEPUBReaderController 数据源与代理扩展
|
||||
///
|
||||
/// 本文件实现 RDReaderDataSource 和 RDReaderDelegate 协议,为阅读器视图提供页面数量、
|
||||
/// 页面内容视图、页码变化通知、屏幕方向变化处理等核心数据与事件支持。
|
||||
|
||||
// MARK: - RDReaderView 数据源与代理
|
||||
|
||||
extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
/// 返回阅读器总页数
|
||||
public func pageCountOfReaderView(readerView: RDReaderView) -> Int {
|
||||
textBook?.pages.count ?? activePages.count
|
||||
}
|
||||
|
||||
/// 为指定页码创建或复用内容视图(优先文本渲染,回退 Web 渲染)
|
||||
public func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView {
|
||||
if let textBook, let page = textBook.page(at: pageNum + 1) {
|
||||
let contentView = (containerView as? RDEPUBTextContentView) ?? RDEPUBTextContentView()
|
||||
@@ -39,12 +51,14 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
return contentView
|
||||
}
|
||||
|
||||
/// 返回页面内容视图的重用标识符(区分文本与 Web 渲染)
|
||||
public func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String? {
|
||||
textBook == nil
|
||||
? NSStringFromClass(RDEPUBWebContentView.self)
|
||||
: NSStringFromClass(RDEPUBTextContentView.self)
|
||||
}
|
||||
|
||||
/// 获取指定文本页面上的高亮标注列表
|
||||
private func textHighlights(for page: RDEPUBTextPage) -> [RDEPUBHighlight] {
|
||||
if let textBook,
|
||||
let chapterData = textBook.chapterData(for: page.href) {
|
||||
@@ -60,18 +74,22 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据规范化 href 获取文本章节数据
|
||||
func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? {
|
||||
readerContext.textChapterData(forNormalizedHref: href)
|
||||
}
|
||||
|
||||
/// 返回阅读器顶部工具视图
|
||||
public func topToolView(readerView: RDReaderView) -> UIView? {
|
||||
topToolView
|
||||
}
|
||||
|
||||
/// 返回阅读器底部工具视图
|
||||
public func bottomToolView(readerView: RDReaderView) -> UIView? {
|
||||
bottomToolView
|
||||
}
|
||||
|
||||
/// 页码变化回调:清除选区、同步阅读状态、检测到达末尾
|
||||
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
||||
updateCurrentSelection(nil)
|
||||
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
||||
@@ -96,11 +114,13 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 屏幕方向即将变化回调,捕获待恢复的展示位置
|
||||
public func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool) {
|
||||
_ = isLandscape
|
||||
runtime.viewportMonitor.capturePendingPresentationRestoreLocation()
|
||||
}
|
||||
|
||||
/// 检测页面尺寸变化并触发重新分页(避免布局错乱)
|
||||
private func reconcileTextPaginationSizeIfNeeded(for pageNum: Int) {
|
||||
guard textBook != nil,
|
||||
!isRepaginating,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// RDEPUBReaderController+PublicAPI.swift
|
||||
// RDEPUBReaderController 的公开 API 扩展,提供书签、高亮、目录跳转、搜索等操作
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: - Public Reader Commands
|
||||
@@ -30,14 +33,22 @@ extension RDEPUBReaderController {
|
||||
runtime.clearSelection()
|
||||
}
|
||||
|
||||
/// 根据唯一标识获取书签
|
||||
/// - Parameter id: 书签 ID
|
||||
/// - Returns: 匹配的书签对象,不存在时返回 nil
|
||||
public func bookmark(withID id: String) -> RDEPUBBookmark? {
|
||||
runtime.bookmark(withID: id)
|
||||
}
|
||||
|
||||
/// 根据唯一标识获取高亮标注
|
||||
/// - Parameter id: 高亮 ID
|
||||
/// - Returns: 匹配的高亮对象,不存在时返回 nil
|
||||
public func highlight(withID id: String) -> RDEPUBHighlight? {
|
||||
runtime.highlight(withID: id)
|
||||
}
|
||||
|
||||
/// 获取当前页的原生文本语义摘要,用于调试和可访问性
|
||||
/// - Returns: 包含页码、断行原因、块类型等信息的摘要字符串,无内容时返回 nil
|
||||
public func nativeTextSemanticSummary() -> String? {
|
||||
guard let textBook,
|
||||
let page = textBook.page(at: max(readerView.currentPage + 1, 1)) ?? textBook.pages.first else {
|
||||
@@ -73,6 +84,13 @@ extension RDEPUBReaderController {
|
||||
runtime.addHighlight(from: selection, color: color, note: note)
|
||||
}
|
||||
|
||||
/// 添加指定样式的标注,从当前选中文本或指定选区创建
|
||||
/// - Parameters:
|
||||
/// - selection: 文本选区,默认使用 currentSelection
|
||||
/// - style: 标注样式(下划线、高亮等)
|
||||
/// - color: 标注颜色(CSS 格式),默认黄色
|
||||
/// - note: 可选批注文字
|
||||
/// - Returns: 创建的高亮对象,重复时返回 nil
|
||||
@discardableResult
|
||||
public func addAnnotation(
|
||||
from selection: RDEPUBSelection? = nil,
|
||||
@@ -83,40 +101,72 @@ extension RDEPUBReaderController {
|
||||
runtime.addAnnotation(from: selection, style: style, color: color, note: note)
|
||||
}
|
||||
|
||||
/// 插入或更新高亮标注(存在则更新,不存在则插入)
|
||||
/// - Parameter highlight: 要写入的高亮对象
|
||||
/// - Returns: 写入后的高亮对象
|
||||
@discardableResult
|
||||
public func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? {
|
||||
runtime.upsertHighlight(highlight)
|
||||
}
|
||||
|
||||
/// 移除指定高亮标注
|
||||
/// - Parameter id: 高亮 ID
|
||||
/// - Returns: 被移除的高亮对象,不存在时返回 nil
|
||||
@discardableResult
|
||||
public func removeHighlight(id: String) -> RDEPUBHighlight? {
|
||||
runtime.removeHighlight(id: id)
|
||||
}
|
||||
|
||||
/// 更新高亮标注的批注内容
|
||||
/// - Parameters:
|
||||
/// - id: 高亮 ID
|
||||
/// - note: 新的批注文字,传 nil 清除批注
|
||||
/// - Returns: 更新后的高亮对象,不存在时返回 nil
|
||||
@discardableResult
|
||||
public func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? {
|
||||
runtime.updateHighlightNote(id: id, note: note)
|
||||
}
|
||||
|
||||
/// 跳转到指定高亮标注所在位置
|
||||
/// - Parameters:
|
||||
/// - id: 高亮 ID
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否跳转成功
|
||||
@discardableResult
|
||||
public func go(toHighlightID id: String, animated: Bool = true) -> Bool {
|
||||
runtime.go(toHighlightID: id, animated: animated)
|
||||
}
|
||||
|
||||
/// 移除当前书籍的所有高亮标注
|
||||
public func removeAllHighlights() {
|
||||
runtime.removeAllHighlights()
|
||||
}
|
||||
|
||||
/// 跳转到目录项对应的阅读位置
|
||||
/// - Parameters:
|
||||
/// - item: EPUB 原生目录项
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否跳转成功
|
||||
@discardableResult
|
||||
public func go(toTableOfContentsItem item: EPUBTableOfContentsItem, animated: Bool = true) -> Bool {
|
||||
go(toTableOfContentsHref: item.href, animated: animated)
|
||||
}
|
||||
|
||||
/// 跳转到目录项对应的阅读位置
|
||||
/// - Parameters:
|
||||
/// - item: RDReader 目录项
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否跳转成功
|
||||
@discardableResult
|
||||
public func go(toTableOfContentsItem item: RDEPUBReaderTableOfContentsItem, animated: Bool = true) -> Bool {
|
||||
go(toTableOfContentsHref: item.href, animated: animated)
|
||||
}
|
||||
|
||||
/// 通过目录 href 跳转到对应阅读位置,支持带锚点的 href
|
||||
/// - Parameters:
|
||||
/// - href: 目录资源路径(可含 #fragment)
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否跳转成功
|
||||
@discardableResult
|
||||
public func go(toTableOfContentsHref href: String, animated: Bool = true) -> Bool {
|
||||
guard publication != nil else { return false }
|
||||
@@ -133,42 +183,61 @@ extension RDEPUBReaderController {
|
||||
return restoreReadingLocation(location, animated: animated)
|
||||
}
|
||||
|
||||
/// 在当前位置添加书签
|
||||
/// - Parameter note: 可选批注文字
|
||||
/// - Returns: 创建的书签对象,位置已存在书签时返回 nil
|
||||
@discardableResult
|
||||
public func addBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
runtime.addBookmark(note: note)
|
||||
}
|
||||
|
||||
/// 切换当前位置的书签状态(有则移除,无则添加)
|
||||
/// - Parameter note: 添加时可附带的批注文字
|
||||
/// - Returns: 添加时返回新书签,移除时返回被移除的书签
|
||||
@discardableResult
|
||||
public func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
runtime.toggleBookmark(note: note)
|
||||
}
|
||||
|
||||
/// 移除指定书签
|
||||
/// - Parameter id: 书签 ID
|
||||
/// - Returns: 被移除的书签对象,不存在时返回 nil
|
||||
@discardableResult
|
||||
public func removeBookmark(id: String) -> RDEPUBBookmark? {
|
||||
runtime.removeBookmark(id: id)
|
||||
}
|
||||
|
||||
/// 跳转到指定书签所在位置
|
||||
/// - Parameters:
|
||||
/// - id: 书签 ID
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否跳转成功
|
||||
@discardableResult
|
||||
public func go(toBookmarkID id: String, animated: Bool = true) -> Bool {
|
||||
runtime.go(toBookmarkID: id, animated: animated)
|
||||
}
|
||||
|
||||
/// 执行全文搜索,自动跳转到第一个匹配项
|
||||
/// 执行全文搜索,匹配结果非空时自动跳转到第一个匹配项,无匹配时清除搜索状态
|
||||
/// - Parameter keyword: 搜索关键词,空字符串会清除搜索
|
||||
public func search(keyword: String) {
|
||||
runtime.search(keyword: keyword)
|
||||
}
|
||||
|
||||
/// 跳转到下一个搜索匹配项
|
||||
/// - Returns: 是否存在下一个匹配项并跳转成功
|
||||
@discardableResult
|
||||
public func searchNext() -> Bool {
|
||||
runtime.searchNext()
|
||||
}
|
||||
|
||||
/// 跳转到上一个搜索匹配项
|
||||
/// - Returns: 是否存在上一个匹配项并跳转成功
|
||||
@discardableResult
|
||||
public func searchPrevious() -> Bool {
|
||||
runtime.searchPrevious()
|
||||
}
|
||||
|
||||
/// 清除搜索状态和高亮
|
||||
public func clearSearch() {
|
||||
runtime.clearSearch()
|
||||
}
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
// RDEPUBReaderController+RenderSupport.swift
|
||||
// EPUB 阅读器渲染支持辅助方法
|
||||
// 提供获取当前布局上下文、偏好设置、页面尺寸、渲染样式、文本布局配置,
|
||||
// 以及构建渲染请求、管理分页宿主视图和回退位置查询等功能。
|
||||
|
||||
import UIKit
|
||||
|
||||
/// RDEPUBReaderController 渲染支持扩展
|
||||
///
|
||||
/// 本文件提供阅读器渲染所需的辅助方法,包括获取当前布局上下文、偏好设置、页面尺寸、
|
||||
/// 渲染样式、文本布局配置,以及构建渲染请求和回退位置查询等功能。
|
||||
|
||||
extension RDEPUBReaderController {
|
||||
/// 获取当前导航器布局上下文(视口尺寸、方向等)
|
||||
func currentLayoutContext() -> RDEPUBNavigatorLayoutContext {
|
||||
readerContext.currentLayoutContext()
|
||||
}
|
||||
|
||||
/// 获取当前阅读偏好设置
|
||||
func currentPreferences() -> RDEPUBPreferences {
|
||||
readerContext.currentPreferences()
|
||||
}
|
||||
|
||||
/// 获取当前文本页面尺寸
|
||||
func currentTextPageSize() -> CGSize {
|
||||
readerContext.currentTextPageSize()
|
||||
}
|
||||
|
||||
/// 获取当前文本渲染样式(字体、行高等)
|
||||
func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||
readerContext.currentTextRenderStyle()
|
||||
}
|
||||
|
||||
/// 获取指定页面尺寸下的文本布局配置
|
||||
func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
|
||||
readerContext.currentTextLayoutConfig(pageSize: pageSize)
|
||||
}
|
||||
|
||||
/// 获取已解析的文本渲染器实例
|
||||
func resolvedTextRenderer() -> RDEPUBTextRenderer {
|
||||
readerContext.resolvedTextRenderer()
|
||||
}
|
||||
|
||||
/// 确保分页宿主视图已添加到视图层级(位于屏幕外用于预计算分页)
|
||||
func ensurePaginationHostView() -> UIView {
|
||||
let viewportSize = currentLayoutContext().viewportSize
|
||||
let hostFrame = CGRect(x: -viewportSize.width - 32, y: 0, width: viewportSize.width, height: viewportSize.height)
|
||||
@@ -36,6 +53,7 @@ extension RDEPUBReaderController {
|
||||
return paginationHostView
|
||||
}
|
||||
|
||||
/// 为指定页面索引构建渲染请求(含位置、高亮、搜索状态)
|
||||
func request(for pageIndex: Int) -> RDEPUBRenderRequest? {
|
||||
guard let publication, activePages.indices.contains(pageIndex) else {
|
||||
return nil
|
||||
@@ -52,11 +70,13 @@ extension RDEPUBReaderController {
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取指定页面索引的回退位置(当无法确定精确位置时使用)
|
||||
func fallbackLocation(for pageIndex: Int) -> RDEPUBLocation? {
|
||||
guard activePages.indices.contains(pageIndex) else { return nil }
|
||||
return readingSession?.fallbackLocation(for: activePages[pageIndex], bookIdentifier: currentBookIdentifier)
|
||||
}
|
||||
|
||||
/// 获取指定固定布局页面上的高亮标注列表
|
||||
private func highlights(for page: EPUBPage) -> [RDEPUBHighlight] {
|
||||
guard let publication else { return [] }
|
||||
if let spread = page.fixedSpread {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
// RDEPUBReaderController+RuntimeBridge.swift
|
||||
// EPUB 阅读器运行时桥接层
|
||||
// 将阅读器控制器的公开行为委托给 runtime 对象执行,涵盖配置应用、
|
||||
// 出版物加载与分页、阅读位置恢复、加载状态管理、错误处理等生命周期操作。
|
||||
|
||||
import UIKit
|
||||
|
||||
/// RDEPUBReaderController 运行时桥接扩展
|
||||
///
|
||||
/// 本文件将阅读器控制器的公开行为委托给 runtime 对象执行,涵盖配置应用、
|
||||
/// 出版物加载与分页、阅读位置恢复、加载状态管理、错误处理等生命周期操作。
|
||||
|
||||
extension RDEPUBReaderController {
|
||||
/// 应用阅读器视图配置,处理显示类型/翻页方向/横屏双页等变更并恢复位置
|
||||
func applyReaderViewConfiguration() {
|
||||
let resolvedDirection = resolvedPageDirection()
|
||||
let presentationDidChange = readerView.currentDisplayType != configuration.displayType
|
||||
@@ -21,14 +32,17 @@ extension RDEPUBReaderController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动初始加载流程(如果尚未加载)
|
||||
func startInitialLoadIfNeeded() {
|
||||
runtime.startInitialLoadIfNeeded()
|
||||
}
|
||||
|
||||
/// 加载 EPUB 出版物
|
||||
func loadPublication() {
|
||||
runtime.loadPublication()
|
||||
}
|
||||
|
||||
/// 应用已解析的出版物数据(解析器、出版物模型、书签、高亮等)
|
||||
func applyParsedPublication(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
@@ -47,14 +61,17 @@ extension RDEPUBReaderController {
|
||||
)
|
||||
}
|
||||
|
||||
/// 对出版物执行分页计算
|
||||
func paginatePublication(restoreLocation: RDEPUBLocation?) {
|
||||
runtime.paginatePublication(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 应用文本书籍数据并恢复阅读位置
|
||||
func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) {
|
||||
runtime.applyTextBook(textBook, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 应用分页快照(页面列表与章节信息)
|
||||
func applyPaginationSnapshot(
|
||||
_ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]),
|
||||
restoreLocation: RDEPUBLocation?
|
||||
@@ -62,35 +79,43 @@ extension RDEPUBReaderController {
|
||||
runtime.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 完成分页流程并恢复阅读位置
|
||||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||||
runtime.finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 保留当前位置重新执行分页
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
runtime.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
|
||||
/// 恢复到指定阅读位置,返回是否成功
|
||||
@discardableResult
|
||||
func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool {
|
||||
runtime.restoreReadingLocation(location, animated: animated)
|
||||
}
|
||||
|
||||
/// 获取当前可见页面的位置
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
runtime.currentVisibleLocation()
|
||||
}
|
||||
|
||||
/// 获取持久化存储的阅读位置
|
||||
func persistenceLocation() -> RDEPUBLocation? {
|
||||
readerContext.persistenceLocation()
|
||||
}
|
||||
|
||||
/// 持久化当前阅读位置
|
||||
func persist(location: RDEPUBLocation) {
|
||||
readerContext.persist(location: location)
|
||||
}
|
||||
|
||||
/// 更新当前文本选区状态
|
||||
func updateCurrentSelection(_ selection: RDEPUBSelection?) {
|
||||
runtime.annotationCoordinator.updateCurrentSelection(selection)
|
||||
}
|
||||
|
||||
/// 将选区限定到指定 spine 范围内
|
||||
func scopedSelection(
|
||||
_ selection: RDEPUBSelection,
|
||||
relativeToSpineIndex spineIndex: Int?
|
||||
@@ -98,53 +123,65 @@ extension RDEPUBReaderController {
|
||||
runtime.annotationCoordinator.scopedSelection(selection, relativeToSpineIndex: spineIndex)
|
||||
}
|
||||
|
||||
/// 刷新可见内容并保留当前位置
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
runtime.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
/// 重建外部文本书籍
|
||||
func rebuildExternalTextBook() {
|
||||
runtime.rebuildExternalTextBook()
|
||||
}
|
||||
|
||||
/// 更新阅读器 UI 装饰层(导航栏、工具栏等)
|
||||
func updateReaderChrome() {
|
||||
runtime.updateReaderChrome()
|
||||
}
|
||||
|
||||
/// 更新书签按钮状态
|
||||
func updateBookmarkChrome() {
|
||||
runtime.updateBookmarkChrome()
|
||||
}
|
||||
|
||||
/// 展示书签管理界面
|
||||
func presentBookmarksManager() {
|
||||
runtime.presentBookmarksManager()
|
||||
}
|
||||
|
||||
/// 展示高亮标注管理界面
|
||||
func presentHighlightsManager() {
|
||||
runtime.presentHighlightsManager()
|
||||
}
|
||||
|
||||
/// 展示标注创建界面
|
||||
func presentAnnotationCreation() {
|
||||
runtime.presentAnnotationCreation()
|
||||
}
|
||||
|
||||
/// 处理选区菜单操作(复制、标注、分享等)
|
||||
func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) {
|
||||
runtime.handleSelectionMenuAction(action, selection: selection)
|
||||
}
|
||||
|
||||
/// 展示阅读设置界面
|
||||
func presentSettings() {
|
||||
runtime.presentSettings()
|
||||
}
|
||||
|
||||
/// 使用闭包更新阅读器配置
|
||||
func updateConfiguration(_ update: (inout RDEPUBReaderConfiguration) -> Void) {
|
||||
var nextConfiguration = configuration
|
||||
update(&nextConfiguration)
|
||||
configuration = nextConfiguration
|
||||
}
|
||||
|
||||
/// 设置屏幕亮度并持久化
|
||||
func setScreenBrightness(_ brightness: CGFloat) {
|
||||
currentBrightness = max(0, min(1, brightness))
|
||||
persistReaderSettingsIfNeeded()
|
||||
}
|
||||
|
||||
/// 持久化阅读器设置(亮度、配置等)
|
||||
func persistReaderSettingsIfNeeded() {
|
||||
let settings = RDEPUBReaderSettings.capture(
|
||||
configuration: configuration,
|
||||
@@ -153,6 +190,7 @@ extension RDEPUBReaderController {
|
||||
persistence?.saveReaderSettings(settings)
|
||||
}
|
||||
|
||||
/// 判断配置变更是否需要重新分页
|
||||
func requiresRepagination(
|
||||
from oldConfiguration: RDEPUBReaderConfiguration,
|
||||
to newConfiguration: RDEPUBReaderConfiguration
|
||||
@@ -169,6 +207,7 @@ extension RDEPUBReaderController {
|
||||
oldConfiguration.textRenderingEngine != newConfiguration.textRenderingEngine
|
||||
}
|
||||
|
||||
/// 判断配置变更是否需要刷新可见内容
|
||||
func requiresVisibleRefresh(
|
||||
from oldConfiguration: RDEPUBReaderConfiguration,
|
||||
to newConfiguration: RDEPUBReaderConfiguration
|
||||
@@ -178,14 +217,17 @@ extension RDEPUBReaderController {
|
||||
oldConfiguration.darkImageBlendRatio != newConfiguration.darkImageBlendRatio
|
||||
}
|
||||
|
||||
/// 展示目录界面
|
||||
func presentTableOfContents() {
|
||||
runtime.presentTableOfContents()
|
||||
}
|
||||
|
||||
/// 处理返回操作
|
||||
func handleBackAction() {
|
||||
runtime.handleBackAction()
|
||||
}
|
||||
|
||||
/// 处理错误:停止分页、隐藏加载指示器、显示错误信息并通知代理
|
||||
func handle(error: Error) {
|
||||
isRepaginating = false
|
||||
hideLoading()
|
||||
@@ -197,19 +239,23 @@ extension RDEPUBReaderController {
|
||||
delegate?.epubReader(self, didFailWithError: error)
|
||||
}
|
||||
|
||||
/// 显示加载指示器
|
||||
func showLoading() {
|
||||
errorLabel.isHidden = true
|
||||
loadingIndicator.startAnimating()
|
||||
}
|
||||
|
||||
/// 隐藏加载指示器
|
||||
func hideLoading() {
|
||||
loadingIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
/// 获取当前视口签名(用于检测视口变化)
|
||||
func currentViewportSignature() -> RDEPUBViewportSignature? {
|
||||
runtime.currentViewportSignature()
|
||||
}
|
||||
|
||||
/// 检测并处理视口变化(旋转、尺寸变化等)
|
||||
func handleViewportChangeIfNeeded(
|
||||
reason: RDEPUBViewportChangeReason,
|
||||
viewportSignature: RDEPUBViewportSignature? = nil
|
||||
@@ -217,18 +263,21 @@ extension RDEPUBReaderController {
|
||||
runtime.handleViewportChangeIfNeeded(reason: reason, viewportSignature: viewportSignature)
|
||||
}
|
||||
|
||||
/// 获取指定页面的搜索结果展示状态
|
||||
func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? {
|
||||
runtime.searchPresentation(for: page)
|
||||
}
|
||||
|
||||
/// 根据出版物阅读方向解析页面翻页方向
|
||||
private func resolvedPageDirection() -> RDReaderView.PageDirection {
|
||||
publication?.readingProgression == .rtl ? .rightToLeft : .leftToRight
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 手势识别器代理(用于 NavigationController 返回手势)
|
||||
// MARK: - 手势识别器代理
|
||||
|
||||
extension RDEPUBReaderController: UIGestureRecognizerDelegate {
|
||||
/// 手势识别器代理,始终返回 true 允许手势触发
|
||||
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
// RDEPUBReaderController+TableOfContents.swift
|
||||
// EPUB 阅读器目录功能
|
||||
// 提供目录(Table of Contents)相关功能,包括解析当前页面对应的目录项
|
||||
// 以及将嵌套目录树结构扁平化为线性列表。
|
||||
|
||||
import Foundation
|
||||
|
||||
/// RDEPUBReaderController 目录扩展
|
||||
///
|
||||
/// 本文件提供目录(Table of Contents)相关功能,包括解析当前页面对应的目录项
|
||||
/// 以及将嵌套目录树结构扁平化为线性列表。
|
||||
|
||||
extension RDEPUBReaderController {
|
||||
/// 解析当前阅读位置对应的目录项,按页码或 href 匹配
|
||||
func resolvedCurrentTableOfContentsItem() -> RDEPUBReaderTableOfContentsItem? {
|
||||
let items = flattenedTableOfContents
|
||||
guard !items.isEmpty else { return nil }
|
||||
@@ -46,6 +57,7 @@ extension RDEPUBReaderController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将嵌套目录树递归扁平化为线性列表,并计算每项目标页码
|
||||
func flattenedTableOfContentsItems(
|
||||
from items: [EPUBTableOfContentsItem],
|
||||
depth: Int = 0
|
||||
|
||||
@@ -3,11 +3,17 @@ import UIKit
|
||||
/// 视口签名结构体,用于检测视口尺寸或安全区是否发生显著变化
|
||||
/// 当变化超过阈值时触发重新分页
|
||||
struct RDEPUBViewportSignature: Equatable {
|
||||
/// 视口宽度(pt)
|
||||
let width: CGFloat
|
||||
/// 视口高度(pt)
|
||||
let height: CGFloat
|
||||
/// 顶部安全区域高度
|
||||
let safeTop: CGFloat
|
||||
/// 左侧安全区域宽度
|
||||
let safeLeft: CGFloat
|
||||
/// 底部安全区域高度
|
||||
let safeBottom: CGFloat
|
||||
/// 右侧安全区域宽度
|
||||
let safeRight: CGFloat
|
||||
|
||||
func differsSignificantly(from other: RDEPUBViewportSignature, threshold: CGFloat = 1) -> Bool {
|
||||
@@ -22,8 +28,11 @@ struct RDEPUBViewportSignature: Equatable {
|
||||
|
||||
/// 视口变化的原因枚举,用于决定延迟处理的策略
|
||||
enum RDEPUBViewportChangeReason {
|
||||
/// 视图布局变化(如 Safe Area 更新、分屏调整等)
|
||||
case viewLayout
|
||||
/// 屏幕方向旋转过渡
|
||||
case orientationTransition
|
||||
}
|
||||
|
||||
/// 原生文本渲染路径的分页快照,包含所有页面和章节信息
|
||||
typealias RDEPUBNativeTextSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import UIKit
|
||||
|
||||
/// WebView 路径的原生装饰覆盖层。
|
||||
/// EPUB WebView 路径的原生装饰覆盖层。
|
||||
/// 用于在 WebView 上方叠加高亮、下划线等视觉装饰。
|
||||
/// 对标 WXRead 的页面级装饰绘制思路:JS 只负责提供 rect,实际视觉绘制由原生 CGContext 完成。
|
||||
final class RDEPUBWebDecorationOverlayView: UIView {
|
||||
private var decorations: [RDEPUBTextOverlayDecoration] = []
|
||||
@@ -17,6 +18,8 @@ final class RDEPUBWebDecorationOverlayView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 应用装饰数据并触发重绘
|
||||
/// - Parameter decorations: 装饰项数组(高亮、下划线等)
|
||||
func applyDecorations(_ decorations: [RDEPUBTextOverlayDecoration]) {
|
||||
self.decorations = decorations.filter { !$0.rects.isEmpty }
|
||||
setNeedsDisplay()
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 阅读器标注协调器:负责高亮、批注和书签的增删改查操作。
|
||||
///
|
||||
/// 职责:
|
||||
/// - 管理高亮(highlight)与批注(annotation)的创建、更新和删除
|
||||
/// - 管理书签(bookmark)的添加、切换和删除
|
||||
/// - 处理文本选中后的菜单操作(复制、高亮、批注)
|
||||
/// - 弹出高亮管理器和书签管理器界面
|
||||
final class RDEPUBReaderAnnotationCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -11,16 +18,19 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
context.controller
|
||||
}
|
||||
|
||||
/// 根据 ID 查找书签。
|
||||
func bookmark(withID id: String) -> RDEPUBBookmark? {
|
||||
guard let controller else { return nil }
|
||||
return controller.activeBookmarks.first { $0.id == id }
|
||||
}
|
||||
|
||||
/// 根据 ID 查找高亮。
|
||||
func highlight(withID id: String) -> RDEPUBHighlight? {
|
||||
guard let controller else { return nil }
|
||||
return controller.activeHighlights.first { $0.id == id }
|
||||
}
|
||||
|
||||
/// 更新当前文本选区状态,并同步刷新底部工具栏的高亮按钮可用性。
|
||||
func updateCurrentSelection(_ selection: RDEPUBSelection?) {
|
||||
guard let controller else { return }
|
||||
controller.currentSelection = selection?.isEmpty == false ? selection : nil
|
||||
@@ -34,6 +44,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
controller.delegate?.epubReader(controller, didChangeSelection: controller.currentSelection)
|
||||
}
|
||||
|
||||
/// 基于当前选区添加高亮标记,自动去重并持久化。
|
||||
@discardableResult
|
||||
func addHighlight(
|
||||
from selection: RDEPUBSelection? = nil,
|
||||
@@ -43,6 +54,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
addAnnotation(from: selection, style: .highlight, color: color, note: note)
|
||||
}
|
||||
|
||||
/// 基于选区创建标注(高亮/划线/批注),支持指定样式、颜色和备注。
|
||||
@discardableResult
|
||||
func addAnnotation(
|
||||
from selection: RDEPUBSelection? = nil,
|
||||
@@ -85,6 +97,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return newHighlight
|
||||
}
|
||||
|
||||
/// 插入或更新高亮(upsert),按 ID 匹配已有记录。
|
||||
@discardableResult
|
||||
func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? {
|
||||
guard let controller else { return nil }
|
||||
@@ -101,6 +114,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return scopedHighlight
|
||||
}
|
||||
|
||||
/// 根据 ID 删除高亮并持久化。
|
||||
@discardableResult
|
||||
func removeHighlight(id: String) -> RDEPUBHighlight? {
|
||||
guard let controller else { return nil }
|
||||
@@ -112,6 +126,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return removed
|
||||
}
|
||||
|
||||
/// 更新指定高亮的批注备注内容。
|
||||
@discardableResult
|
||||
func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? {
|
||||
guard let controller else { return nil }
|
||||
@@ -123,6 +138,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return controller.activeHighlights[index]
|
||||
}
|
||||
|
||||
/// 跳转到指定高亮所在位置。
|
||||
@discardableResult
|
||||
func go(toHighlightID id: String, animated: Bool = true) -> Bool {
|
||||
guard let controller else { return false }
|
||||
@@ -132,6 +148,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return controller.restoreReadingLocation(highlight.location, animated: animated)
|
||||
}
|
||||
|
||||
/// 清除所有高亮标记。
|
||||
func removeAllHighlights() {
|
||||
guard let controller else { return }
|
||||
guard !controller.activeHighlights.isEmpty else { return }
|
||||
@@ -139,6 +156,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
persistHighlightsAndRefreshContent()
|
||||
}
|
||||
|
||||
/// 将选区位置相对于指定 spine 索引进行规范化。
|
||||
func scopedSelection(
|
||||
_ selection: RDEPUBSelection,
|
||||
relativeToSpineIndex spineIndex: Int?
|
||||
@@ -166,6 +184,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
)
|
||||
}
|
||||
|
||||
/// 弹出高亮管理器,支持查看、编辑备注和删除高亮。
|
||||
func presentHighlightsManager() {
|
||||
guard let controller else { return }
|
||||
guard controller.configuration.allowsHighlights else { return }
|
||||
@@ -196,6 +215,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
controller.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
/// 弹出标注创建面板(高亮/划线/批注选择)。
|
||||
func presentAnnotationCreation() {
|
||||
guard let controller else { return }
|
||||
guard controller.configuration.allowsHighlights,
|
||||
@@ -205,6 +225,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
presentAnnotationActionSheet(for: currentSelection)
|
||||
}
|
||||
|
||||
/// 处理文本选中后的菜单操作:复制、高亮、批注。
|
||||
func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) {
|
||||
guard let selection else { return }
|
||||
switch action {
|
||||
@@ -218,6 +239,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// 在当前位置添加书签,自动去重。
|
||||
@discardableResult
|
||||
func addBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
guard let controller else { return nil }
|
||||
@@ -239,6 +261,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return newBookmark
|
||||
}
|
||||
|
||||
/// 切换当前位置的书签状态:已存在则移除,不存在则添加。
|
||||
@discardableResult
|
||||
func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
guard let controller else { return nil }
|
||||
@@ -254,6 +277,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return addBookmark(note: note)
|
||||
}
|
||||
|
||||
/// 根据 ID 删除书签并持久化。
|
||||
@discardableResult
|
||||
func removeBookmark(id: String) -> RDEPUBBookmark? {
|
||||
guard let controller else { return nil }
|
||||
@@ -265,6 +289,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return removed
|
||||
}
|
||||
|
||||
/// 跳转到指定书签所在位置。
|
||||
@discardableResult
|
||||
func go(toBookmarkID id: String, animated: Bool = true) -> Bool {
|
||||
guard let controller else { return false }
|
||||
@@ -274,12 +299,14 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return controller.restoreReadingLocation(bookmark.location, animated: animated)
|
||||
}
|
||||
|
||||
/// 同步顶部书签按钮和底部书签列表按钮的 UI 状态。
|
||||
func updateBookmarkChrome() {
|
||||
guard let controller else { return }
|
||||
controller.topToolView.setBookmarkSelected(currentBookmark() != nil)
|
||||
controller.bottomToolView.setBookmarksEnabled(!controller.activeBookmarks.isEmpty)
|
||||
}
|
||||
|
||||
/// 弹出书签管理器,支持查看、跳转和删除书签。
|
||||
func presentBookmarksManager() {
|
||||
guard let controller else { return }
|
||||
guard !controller.activeBookmarks.isEmpty else { return }
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 阅读器界面组装协调器:负责阅读器初次启动时的 UI 搭建。
|
||||
///
|
||||
/// 职责:
|
||||
/// - 组装阅读器视图层次结构(readerView、loadingIndicator、errorLabel)
|
||||
/// - 注册内容视图类型
|
||||
/// - 处理外部纯文本图书的启动收尾逻辑
|
||||
final class RDEPUBReaderAssemblyCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -7,6 +13,7 @@ final class RDEPUBReaderAssemblyCoordinator {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// 组装阅读器界面:添加 readerView、loadingIndicator、errorLabel 到控制器视图,并配置顶部工具栏。
|
||||
func assembleInterface() {
|
||||
guard let controller = context.controller,
|
||||
let readerView = context.readerView else { return }
|
||||
@@ -18,6 +25,7 @@ final class RDEPUBReaderAssemblyCoordinator {
|
||||
controller.delegate?.epubReader(controller, configureTopToolView: controller.topToolView)
|
||||
}
|
||||
|
||||
/// 外部纯文本图书启动时,加载已保存的书签、高亮和阅读位置,完成分页收尾。
|
||||
func finishExternalTextBookLaunchIfNeeded() {
|
||||
guard let runtime = context.runtime,
|
||||
context.isExternalTextBook else {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 阅读器 Chrome 协调器:负责顶部/底部工具栏的创建、更新和交互处理。
|
||||
///
|
||||
/// 职责:
|
||||
/// - 创建并配置顶部工具栏(返回、书签按钮)
|
||||
/// - 创建并配置底部工具栏(目录、书签、高亮、设置按钮)
|
||||
/// - 同步工具栏的主题和状态
|
||||
/// - 弹出设置面板和目录面板
|
||||
/// - 处理返回按钮的关闭逻辑
|
||||
final class RDEPUBReaderChromeCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -11,6 +19,7 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
context.controller
|
||||
}
|
||||
|
||||
/// 创建顶部工具栏视图,绑定返回和书签切换回调。
|
||||
func makeTopToolView() -> RDEPUBReaderTopToolView {
|
||||
let toolView = RDEPUBReaderTopToolView()
|
||||
toolView.onBack = { [weak self] in
|
||||
@@ -22,6 +31,7 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
return toolView
|
||||
}
|
||||
|
||||
/// 创建底部工具栏视图,绑定目录、书签、高亮、设置等回调。
|
||||
func makeBottomToolView() -> RDEPUBReaderBottomToolView {
|
||||
let toolView = RDEPUBReaderBottomToolView()
|
||||
toolView.onShowTableOfContents = { [weak self] in
|
||||
@@ -42,6 +52,7 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
return toolView
|
||||
}
|
||||
|
||||
/// 同步更新顶部和底部工具栏的主题、标题、按钮可用性等状态。
|
||||
func updateReaderChrome() {
|
||||
guard let controller else { return }
|
||||
controller.topToolView.apply(theme: controller.configuration.theme)
|
||||
@@ -67,6 +78,7 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
controller.updateBookmarkChrome()
|
||||
}
|
||||
|
||||
/// 弹出阅读设置面板(字号、字体、行距、分栏、主题、亮度等)。
|
||||
func presentSettings() {
|
||||
guard let controller else { return }
|
||||
guard controller.configuration.showsSettingsPanel else { return }
|
||||
@@ -101,6 +113,7 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
controller.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
/// 弹出目录列表面板,支持点击跳转到指定章节。
|
||||
func presentTableOfContents() {
|
||||
guard let controller else { return }
|
||||
guard controller.configuration.showsTableOfContents else { return }
|
||||
@@ -124,6 +137,7 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
controller.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
/// 处理返回按钮点击,自动判断 pop 或 dismiss 方式关闭阅读器。
|
||||
func handleBackAction() {
|
||||
guard let controller else { return }
|
||||
close(controller)
|
||||
|
||||
@@ -11,37 +11,61 @@ import UIKit
|
||||
final class RDEPUBReaderContext {
|
||||
// MARK: - 引用
|
||||
|
||||
/// 弱引用阅读器控制器,用于 UIKit 呈现操作。
|
||||
weak var controller: RDEPUBReaderController?
|
||||
/// 弱引用阅读器视图,用于布局和页面状态查询。
|
||||
weak var readerView: RDReaderView?
|
||||
/// 依赖注入容器,提供解析器、分页器等工厂方法。
|
||||
var dependencies: RDEPUBReaderDependencies = .live
|
||||
/// 便捷访问当前控制器的运行时协调器集合。
|
||||
var runtime: RDEPUBReaderRuntime? {
|
||||
controller?.runtime
|
||||
}
|
||||
|
||||
// MARK: - 业务状态
|
||||
|
||||
/// EPUB 解析器实例。
|
||||
var parser: RDEPUBParser?
|
||||
/// 解析后的出版物模型。
|
||||
var publication: RDEPUBPublication?
|
||||
/// 当前阅读会话,管理页面和章节状态。
|
||||
var readingSession: RDEPUBReadingSession?
|
||||
/// 原生文本排版生成的图书模型(仅文本重排模式)。
|
||||
var textBook: RDEPUBTextBook?
|
||||
/// 当前书籍的所有书签。
|
||||
var activeBookmarks: [RDEPUBBookmark] = []
|
||||
/// 当前书籍的所有高亮标注。
|
||||
var activeHighlights: [RDEPUBHighlight] = []
|
||||
/// 当前打开书籍的唯一标识。
|
||||
var currentBookIdentifier: String?
|
||||
/// 分页操作令牌,用于取消过期的异步分页任务。
|
||||
var paginationToken = UUID()
|
||||
/// Web 内容分页计算器。
|
||||
var paginator: RDEPUBPaginator?
|
||||
/// 全文搜索状态。
|
||||
var searchState: RDEPUBSearchState?
|
||||
/// 上次文本分页时的页面尺寸,用于检测是否需要重新分页。
|
||||
var lastTextPaginationPageSize: CGSize?
|
||||
/// 当前用户文本选区。
|
||||
var currentSelection: RDEPUBSelection?
|
||||
|
||||
// MARK: - 控制器状态(从 controller 下沉)
|
||||
|
||||
/// 阅读器配置(字号、字体、主题等)。
|
||||
var configuration: RDEPUBReaderConfiguration = .default
|
||||
/// 持久化策略,负责书签、高亮、阅读位置的存取。
|
||||
var persistence: RDEPUBReaderPersistence?
|
||||
/// 当前打开的 EPUB 文件 URL。
|
||||
var epubURL: URL = URL(string: "about:blank")!
|
||||
/// 是否正在重新分页。
|
||||
var isRepaginating: Bool = false
|
||||
/// 是否已完成首次加载。
|
||||
var didStartInitialLoad: Bool = false
|
||||
/// 是否为外部传入的纯文本图书。
|
||||
var isExternalTextBook: Bool = false
|
||||
/// 外部纯文本文件的 URL。
|
||||
var textFileURL: URL?
|
||||
/// 文本图书缓存,避免重复排版。
|
||||
var textBookCache = RDEPUBTextBookCache()
|
||||
|
||||
// MARK: - 初始化
|
||||
@@ -53,6 +77,7 @@ final class RDEPUBReaderContext {
|
||||
|
||||
// MARK: - 便捷方法
|
||||
|
||||
/// 根据当前 readerView 和控制器尺寸构建布局上下文。
|
||||
func currentLayoutContext() -> RDEPUBNavigatorLayoutContext {
|
||||
let containerSize = readerView?.bounds.size ?? .zero
|
||||
let viewSize = controller?.view.bounds.size ?? containerSize
|
||||
@@ -66,10 +91,12 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据当前配置生成阅读偏好设置。
|
||||
func currentPreferences() -> RDEPUBPreferences {
|
||||
configuration.makePreferences()
|
||||
}
|
||||
|
||||
/// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。
|
||||
func currentTextPageSize() -> CGSize {
|
||||
let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil
|
||||
if let readerView, let pageNum {
|
||||
@@ -81,6 +108,7 @@ final class RDEPUBReaderContext {
|
||||
return currentLayoutContext().viewportSize
|
||||
}
|
||||
|
||||
/// 根据当前配置生成文本渲染样式(字体、行距、颜色)。
|
||||
func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||
let font = configuration.fontChoice.font(ofSize: configuration.fontSize)
|
||||
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
|
||||
@@ -92,6 +120,7 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据页面尺寸和配置生成文本排版参数。
|
||||
func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
|
||||
return RDEPUBTextLayoutConfig(
|
||||
frameWidth: max(pageSize.width, 1),
|
||||
@@ -108,61 +137,75 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取当前配置对应的文本渲染器实例。
|
||||
func resolvedTextRenderer() -> RDEPUBTextRenderer {
|
||||
dependencies.makeTextRenderer(configuration.textRenderingEngine)
|
||||
}
|
||||
|
||||
/// 当前活跃的页面列表。
|
||||
var activePages: [EPUBPage] {
|
||||
readingSession?.activePages ?? []
|
||||
}
|
||||
|
||||
/// 当前活跃的章节信息列表。
|
||||
var activeChapters: [EPUBChapterInfo] {
|
||||
readingSession?.activeChapters ?? []
|
||||
}
|
||||
|
||||
/// 屏幕亮度代理属性,读写均转发给系统环境。
|
||||
var currentBrightness: CGFloat {
|
||||
get { dependencies.environment.currentBrightness }
|
||||
set { dependencies.environment.currentBrightness = newValue }
|
||||
}
|
||||
|
||||
/// 用新快照替换当前活跃的分页快照。
|
||||
func replaceActiveSnapshot(_ snapshot: RDEPUBReadingSession.PaginationSnapshot) {
|
||||
readingSession?.setActiveSnapshot(snapshot)
|
||||
}
|
||||
|
||||
/// 清除当前活跃快照,重置运行时状态。
|
||||
func clearActiveSnapshot() {
|
||||
readingSession?.resetRuntimeState()
|
||||
}
|
||||
|
||||
/// 工厂方法:创建 EPUB 解析器。
|
||||
func makeParser() -> RDEPUBParser {
|
||||
dependencies.makeParser()
|
||||
}
|
||||
|
||||
/// 工厂方法:创建 Web 内容分页计算器。
|
||||
func makePaginator() -> RDEPUBPaginator {
|
||||
dependencies.makePaginator()
|
||||
}
|
||||
|
||||
/// 工厂方法:创建 EPUB 文本图书构建器。
|
||||
func makeTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder {
|
||||
dependencies.makeTextBookBuilder(resolvedTextRenderer(), textBookCache, layoutConfig)
|
||||
}
|
||||
|
||||
/// 工厂方法:创建纯文本图书构建器。
|
||||
func makePlainTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder {
|
||||
dependencies.makePlainTextBookBuilder(resolvedTextRenderer(), layoutConfig)
|
||||
}
|
||||
|
||||
/// 获取当前可见页面的阅读位置。
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
controller?.currentVisibleLocation()
|
||||
}
|
||||
|
||||
/// 从持久化存储加载上次保存的阅读位置。
|
||||
func persistenceLocation() -> RDEPUBLocation? {
|
||||
guard let currentBookIdentifier else { return nil }
|
||||
return persistence?.loadLocation(for: currentBookIdentifier)
|
||||
}
|
||||
|
||||
/// 将阅读位置持久化到存储。
|
||||
func persist(location: RDEPUBLocation) {
|
||||
guard let currentBookIdentifier else { return }
|
||||
persistence?.saveLocation(location, for: currentBookIdentifier)
|
||||
}
|
||||
|
||||
/// 根据规范化 href 获取文本章节数据。
|
||||
func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? {
|
||||
guard let textBook, let publication else { return nil }
|
||||
let normalizedHref = publication.resourceResolver.normalizedHref(href) ?? href
|
||||
@@ -171,38 +214,47 @@ final class RDEPUBReaderContext {
|
||||
.flatMap { textBook.chapterData(for: $0.href) }
|
||||
}
|
||||
|
||||
/// 显示加载指示器。
|
||||
func showLoading() {
|
||||
controller?.showLoading()
|
||||
}
|
||||
|
||||
/// 隐藏加载指示器。
|
||||
func hideLoading() {
|
||||
controller?.hideLoading()
|
||||
}
|
||||
|
||||
/// 将错误转发给控制器处理。
|
||||
func handle(error: Error) {
|
||||
controller?.handle(error: error)
|
||||
}
|
||||
|
||||
/// 触发工具栏状态更新。
|
||||
func updateReaderChrome() {
|
||||
controller?.updateReaderChrome()
|
||||
}
|
||||
|
||||
/// 刷新可见内容并保持当前阅读位置不变。
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
controller?.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
/// 恢复到指定阅读位置。
|
||||
func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool {
|
||||
controller?.restoreReadingLocation(location, animated: animated) ?? false
|
||||
}
|
||||
|
||||
/// 重新分页并保持当前阅读位置。
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
controller?.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
|
||||
/// 将当前配置应用到阅读器视图。
|
||||
func applyReaderViewConfiguration() {
|
||||
controller?.applyReaderViewConfiguration()
|
||||
}
|
||||
|
||||
/// 同步书签按钮的 UI 状态。
|
||||
func updateBookmarkChrome() {
|
||||
controller?.updateBookmarkChrome()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
// RDEPUBReaderDependencies.swift
|
||||
// EPUB 阅读器依赖注入配置,定义显示环境协议与核心组件工厂
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 阅读器显示环境协议,抽象屏幕亮度和视口尺寸以支持测试和多平台适配
|
||||
public protocol RDEPUBReaderDisplayEnvironment: AnyObject {
|
||||
/// 当前屏幕亮度,取值范围 0.0 ~ 1.0
|
||||
var currentBrightness: CGFloat { get set }
|
||||
/// 后备视口尺寸,用于无法获取实际视图尺寸时的布局计算
|
||||
var fallbackViewportSize: CGSize { get }
|
||||
}
|
||||
|
||||
/// 基于 UIScreen 的默认显示环境实现,直接读写系统屏幕亮度
|
||||
public final class RDEPUBUIScreenEnvironment: RDEPUBReaderDisplayEnvironment {
|
||||
public init() {}
|
||||
|
||||
@@ -18,14 +25,29 @@ public final class RDEPUBUIScreenEnvironment: RDEPUBReaderDisplayEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
/// EPUB 阅读器核心依赖容器,通过工厂闭包注入各组件以便替换和测试
|
||||
public struct RDEPUBReaderDependencies {
|
||||
/// 显示环境实例
|
||||
public var environment: any RDEPUBReaderDisplayEnvironment
|
||||
/// EPUB 解析器工厂
|
||||
public var makeParser: () -> RDEPUBParser
|
||||
/// 分页器工厂
|
||||
public var makePaginator: () -> RDEPUBPaginator
|
||||
/// 富文本书籍构建器工厂
|
||||
public var makeTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder
|
||||
/// 纯文本书籍构建器工厂
|
||||
public var makePlainTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder
|
||||
/// 文本渲染器工厂
|
||||
public var makeTextRenderer: (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer
|
||||
|
||||
/// 初始化依赖容器
|
||||
/// - Parameters:
|
||||
/// - environment: 显示环境实例
|
||||
/// - makeParser: EPUB 解析器工厂闭包
|
||||
/// - makePaginator: 分页器工厂闭包
|
||||
/// - makeTextBookBuilder: 富文本书籍构建器工厂闭包
|
||||
/// - makePlainTextBookBuilder: 纯文本书籍构建器工厂闭包
|
||||
/// - makeTextRenderer: 文本渲染器工厂闭包
|
||||
public init(
|
||||
environment: any RDEPUBReaderDisplayEnvironment,
|
||||
makeParser: @escaping () -> RDEPUBParser,
|
||||
@@ -42,6 +64,7 @@ public struct RDEPUBReaderDependencies {
|
||||
self.makeTextRenderer = makeTextRenderer
|
||||
}
|
||||
|
||||
/// 默认生产环境依赖,使用系统屏幕环境和标准组件实现
|
||||
public static var live: RDEPUBReaderDependencies {
|
||||
RDEPUBReaderDependencies(
|
||||
environment: RDEPUBUIScreenEnvironment(),
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
/// EPUB 阅读器加载协调器:负责 EPUB 文件的解析和出版物初始化。
|
||||
///
|
||||
/// 职责:
|
||||
/// - 判断是否需要执行首次加载
|
||||
/// - 后台解析 EPUB 文件并构建 Publication 模型
|
||||
/// - 将解析结果应用到阅读器上下文并触发分页
|
||||
final class RDEPUBReaderLoadCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -7,6 +13,7 @@ final class RDEPUBReaderLoadCoordinator {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// 检查条件后启动首次加载,确保只执行一次且视图已布局。
|
||||
func startInitialLoadIfNeeded() {
|
||||
guard let controller = context.controller,
|
||||
let readerView = context.readerView,
|
||||
@@ -19,6 +26,7 @@ final class RDEPUBReaderLoadCoordinator {
|
||||
loadPublication()
|
||||
}
|
||||
|
||||
/// 后台解析 EPUB 文件,加载书签、高亮和阅读位置,完成后回调主线程。
|
||||
func loadPublication() {
|
||||
guard let controller = context.controller else { return }
|
||||
context.showLoading()
|
||||
@@ -57,6 +65,7 @@ final class RDEPUBReaderLoadCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将解析完成的出版物应用到上下文,设置书签/高亮/会话,并触发分页。
|
||||
func applyParsedPublication(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
/// EPUB 阅读器位置协调器:负责阅读位置的恢复、查询和持久化。
|
||||
///
|
||||
/// 职责:
|
||||
/// - 根据保存的位置恢复阅读进度
|
||||
/// - 获取当前可见页面的阅读位置
|
||||
/// - 从持久化存储加载已保存位置
|
||||
/// - 持久化当前位置并通知委托
|
||||
final class RDEPUBReaderLocationCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -7,6 +14,7 @@ final class RDEPUBReaderLocationCoordinator {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// 恢复到指定阅读位置,返回是否成功跳转。
|
||||
@discardableResult
|
||||
func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool {
|
||||
guard let controller = context.controller,
|
||||
@@ -30,6 +38,7 @@ final class RDEPUBReaderLocationCoordinator {
|
||||
return true
|
||||
}
|
||||
|
||||
/// 获取当前可见页面对应的阅读位置。
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
guard let controller = context.controller,
|
||||
let readerView = context.readerView else {
|
||||
@@ -41,6 +50,7 @@ final class RDEPUBReaderLocationCoordinator {
|
||||
return context.readingSession?.currentReadingLocation(bookIdentifier: context.currentBookIdentifier)
|
||||
}
|
||||
|
||||
/// 从持久化存储加载上次保存的阅读位置。
|
||||
func persistenceLocation() -> RDEPUBLocation? {
|
||||
guard let controller = context.controller,
|
||||
let currentBookIdentifier = context.currentBookIdentifier else {
|
||||
@@ -49,6 +59,7 @@ final class RDEPUBReaderLocationCoordinator {
|
||||
return controller.persistence?.loadLocation(for: currentBookIdentifier)
|
||||
}
|
||||
|
||||
/// 持久化阅读位置,并通知委托更新目录项和书签状态。
|
||||
func persist(location: RDEPUBLocation) {
|
||||
guard let controller = context.controller,
|
||||
let currentBookIdentifier = context.currentBookIdentifier else { return }
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
/// EPUB 阅读器分页协调器:负责出版物的分页计算和页面数据更新。
|
||||
///
|
||||
/// 职责:
|
||||
/// - 根据出版物类型(文本重排/Fixed Layout/Web 内容)选择分页策略
|
||||
/// - 后台构建文本图书模型并应用分页快照
|
||||
/// - 重新分页时保持当前阅读位置
|
||||
/// - 刷新可见内容并保持位置
|
||||
/// - 重建外部纯文本图书
|
||||
final class RDEPUBReaderPaginationCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -7,6 +15,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// 对出版物执行分页:文本重排走 TextBookBuilder,Fixed Layout 直接生成快照,Web 内容走 Paginator。
|
||||
func paginatePublication(restoreLocation: RDEPUBLocation?) {
|
||||
guard let controller = context.controller,
|
||||
let parser = context.parser,
|
||||
@@ -79,6 +88,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用文本图书模型:生成分页快照并完成分页流程。
|
||||
func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) {
|
||||
guard let controller = context.controller else { return }
|
||||
context.textBook = textBook
|
||||
@@ -93,6 +103,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 应用分页快照(Fixed Layout 或 Web 内容),并完成分页流程。
|
||||
func applyPaginationSnapshot(
|
||||
_ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]),
|
||||
restoreLocation: RDEPUBLocation?
|
||||
@@ -109,6 +120,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 分页完成后的收尾:刷新视图、恢复阅读位置、处理待定视口变更。
|
||||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||||
guard let controller = context.controller,
|
||||
let readerView = context.readerView else { return }
|
||||
@@ -125,6 +137,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
context.runtime?.viewportMonitor.processPendingChangeAfterPagination()
|
||||
}
|
||||
|
||||
/// 重新分页并保持当前阅读位置(优先使用待恢复位置,其次当前位置,最后持久化位置)。
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
guard context.publication != nil else { return }
|
||||
let restoreLocation = context.runtime?.viewportMonitor.consumePendingPresentationRestoreLocation()
|
||||
@@ -133,6 +146,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
paginatePublication(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 刷新可见内容并保持当前阅读位置不变。
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
guard let readerView = context.readerView else { return }
|
||||
let restoreLocation = context.currentVisibleLocation() ?? context.persistenceLocation()
|
||||
@@ -142,6 +156,7 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// 重建外部纯文本图书(布局变更后重新排版)。
|
||||
func rebuildExternalTextBook() {
|
||||
guard let controller = context.controller,
|
||||
let textFileURL = controller.textFileURL else { return }
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 阅读器运行时总协调器。
|
||||
/// 统一持有并分发给加载、分页、定位、搜索、工具栏、批注、视口监测等子协调器,
|
||||
/// 作为阅读器控制器的门面(Facade),简化外部调用。
|
||||
final class RDEPUBReaderRuntime {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -15,18 +18,22 @@ final class RDEPUBReaderRuntime {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// 创建顶部工具栏视图
|
||||
func makeTopToolView() -> RDEPUBReaderTopToolView {
|
||||
chromeCoordinator.makeTopToolView()
|
||||
}
|
||||
|
||||
/// 创建底部工具栏视图
|
||||
func makeBottomToolView() -> RDEPUBReaderBottomToolView {
|
||||
chromeCoordinator.makeBottomToolView()
|
||||
}
|
||||
|
||||
/// 若尚未加载则启动首次加载流程
|
||||
func startInitialLoadIfNeeded() {
|
||||
loadCoordinator.startInitialLoadIfNeeded()
|
||||
}
|
||||
|
||||
/// 重新加载当前书籍,清空解析器、分页、批注等状态后从头初始化
|
||||
func reloadBook() {
|
||||
guard let readerView = context.readerView else { return }
|
||||
context.didStartInitialLoad = false
|
||||
@@ -44,10 +51,20 @@ final class RDEPUBReaderRuntime {
|
||||
startInitialLoadIfNeeded()
|
||||
}
|
||||
|
||||
/// 跳转到指定阅读位置
|
||||
/// - Parameters:
|
||||
/// - location: 目标位置
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 跳转是否成功
|
||||
func go(to location: RDEPUBLocation, animated: Bool = false) -> Bool {
|
||||
locationCoordinator.restoreReadingLocation(location, animated: animated)
|
||||
}
|
||||
|
||||
/// 跳转到指定页码
|
||||
/// - Parameters:
|
||||
/// - pageNumber: 目标页码(从 1 开始)
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 跳转是否成功
|
||||
@discardableResult
|
||||
func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool {
|
||||
guard let controller = context.controller,
|
||||
@@ -73,6 +90,7 @@ final class RDEPUBReaderRuntime {
|
||||
return true
|
||||
}
|
||||
|
||||
/// 清除当前选区
|
||||
func clearSelection() {
|
||||
annotationCoordinator.updateCurrentSelection(nil)
|
||||
}
|
||||
@@ -168,20 +186,24 @@ final class RDEPUBReaderRuntime {
|
||||
annotationCoordinator.handleSelectionMenuAction(action, selection: selection)
|
||||
}
|
||||
|
||||
/// 按关键词搜索全文
|
||||
func search(keyword: String) {
|
||||
searchCoordinator.search(keyword: keyword)
|
||||
}
|
||||
|
||||
/// 跳转到下一个搜索匹配项
|
||||
@discardableResult
|
||||
func searchNext() -> Bool {
|
||||
searchCoordinator.searchNext()
|
||||
}
|
||||
|
||||
/// 跳转到上一个搜索匹配项
|
||||
@discardableResult
|
||||
func searchPrevious() -> Bool {
|
||||
searchCoordinator.searchPrevious()
|
||||
}
|
||||
|
||||
/// 清除搜索状态
|
||||
func clearSearch() {
|
||||
searchCoordinator.clearSearch()
|
||||
}
|
||||
@@ -190,26 +212,32 @@ final class RDEPUBReaderRuntime {
|
||||
searchCoordinator.searchPresentation(for: page)
|
||||
}
|
||||
|
||||
/// 更新阅读器工具栏显示状态
|
||||
func updateReaderChrome() {
|
||||
chromeCoordinator.updateReaderChrome()
|
||||
}
|
||||
|
||||
/// 弹出阅读设置面板
|
||||
func presentSettings() {
|
||||
chromeCoordinator.presentSettings()
|
||||
}
|
||||
|
||||
/// 弹出目录面板
|
||||
func presentTableOfContents() {
|
||||
chromeCoordinator.presentTableOfContents()
|
||||
}
|
||||
|
||||
/// 处理返回操作
|
||||
func handleBackAction() {
|
||||
chromeCoordinator.handleBackAction()
|
||||
}
|
||||
|
||||
/// 启动 Publication 加载流程
|
||||
func loadPublication() {
|
||||
loadCoordinator.loadPublication()
|
||||
}
|
||||
|
||||
/// 将已解析的 Publication 应用到控制器
|
||||
func applyParsedPublication(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
@@ -228,14 +256,17 @@ final class RDEPUBReaderRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
/// 对 Publication 执行分页计算
|
||||
func paginatePublication(restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.paginatePublication(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 应用外部 TextBook 并恢复阅读位置
|
||||
func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.applyTextBook(textBook, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 应用分页快照并恢复阅读位置
|
||||
func applyPaginationSnapshot(
|
||||
_ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]),
|
||||
restoreLocation: RDEPUBLocation?
|
||||
@@ -243,35 +274,43 @@ final class RDEPUBReaderRuntime {
|
||||
paginationCoordinator.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 完成分页流程并恢复阅读位置
|
||||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 重新分页并保持当前阅读位置不变
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
paginationCoordinator.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
|
||||
/// 刷新当前可见内容,保持阅读位置不变
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
paginationCoordinator.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
/// 重建外部 TextBook 数据
|
||||
func rebuildExternalTextBook() {
|
||||
paginationCoordinator.rebuildExternalTextBook()
|
||||
}
|
||||
|
||||
/// 恢复到指定阅读位置
|
||||
@discardableResult
|
||||
func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool {
|
||||
locationCoordinator.restoreReadingLocation(location, animated: animated)
|
||||
}
|
||||
|
||||
/// 获取当前可见页面的阅读位置
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
locationCoordinator.currentVisibleLocation()
|
||||
}
|
||||
|
||||
/// 获取当前视口签名快照
|
||||
func currentViewportSignature() -> RDEPUBViewportSignature? {
|
||||
viewportMonitor.currentViewportSignature()
|
||||
}
|
||||
|
||||
/// 视口变化时检查是否需要重新分页
|
||||
func handleViewportChangeIfNeeded(
|
||||
reason: RDEPUBViewportChangeReason,
|
||||
viewportSignature: RDEPUBViewportSignature? = nil
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
/// 搜索协调器,负责管理全文搜索的执行、结果导航和搜索状态通知。
|
||||
final class RDEPUBReaderSearchCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -11,6 +12,8 @@ final class RDEPUBReaderSearchCoordinator {
|
||||
context.controller
|
||||
}
|
||||
|
||||
/// 按关键词执行全文搜索,匹配结果自动导航到首个命中位置
|
||||
/// - Parameter keyword: 搜索关键词
|
||||
func search(keyword: String) {
|
||||
guard let controller else { return }
|
||||
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -34,16 +37,21 @@ final class RDEPUBReaderSearchCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转到下一个搜索匹配项
|
||||
/// - Returns: 是否成功跳转
|
||||
@discardableResult
|
||||
func searchNext() -> Bool {
|
||||
advanceSearch(by: 1)
|
||||
}
|
||||
|
||||
/// 跳转到上一个搜索匹配项
|
||||
/// - Returns: 是否成功跳转
|
||||
@discardableResult
|
||||
func searchPrevious() -> Bool {
|
||||
advanceSearch(by: -1)
|
||||
}
|
||||
|
||||
/// 清除搜索状态并刷新当前可见内容
|
||||
func clearSearch() {
|
||||
guard let controller else { return }
|
||||
controller.searchState = nil
|
||||
@@ -51,6 +59,9 @@ final class RDEPUBReaderSearchCoordinator {
|
||||
controller.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
/// 构建指定页面的搜索结果展示信息,用于高亮渲染
|
||||
/// - Parameter page: 目标页面
|
||||
/// - Returns: 搜索展示数据,若无搜索状态则返回 nil
|
||||
func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? {
|
||||
guard let controller else { return nil }
|
||||
guard let searchState = controller.searchState,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import UIKit
|
||||
|
||||
/// 视口监测器,监听视图布局和屏幕旋转等视口变化事件,
|
||||
/// 检测变化是否显著,必要时触发重新分页或内容重建。
|
||||
final class RDEPUBReaderViewportMonitor {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
@@ -16,6 +18,7 @@ final class RDEPUBReaderViewportMonitor {
|
||||
context.controller
|
||||
}
|
||||
|
||||
/// 视图布局完成后检查视口是否发生变化,首次布局时触发初始加载
|
||||
func viewDidLayoutSubviews() {
|
||||
guard let controller else { return }
|
||||
guard let viewportSignature = currentViewportSignature() else { return }
|
||||
@@ -38,6 +41,8 @@ final class RDEPUBReaderViewportMonitor {
|
||||
handleViewportChangeIfNeeded(reason: .viewLayout, viewportSignature: viewportSignature)
|
||||
}
|
||||
|
||||
/// 屏幕旋转前捕获当前阅读位置,旋转完成后检测视口变化并处理
|
||||
/// - Parameter coordinator: 转场协调器
|
||||
func viewWillTransition(with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
guard let controller else { return }
|
||||
guard controller.didStartInitialLoad else { return }
|
||||
@@ -52,6 +57,7 @@ final class RDEPUBReaderViewportMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置所有视口状态,用于重新加载书籍
|
||||
func resetForReload() {
|
||||
lastAppliedViewportSignature = currentViewportSignature()
|
||||
pendingViewportChangeReason = nil
|
||||
@@ -59,16 +65,19 @@ final class RDEPUBReaderViewportMonitor {
|
||||
isWaitingForViewportTransitionCompletion = false
|
||||
}
|
||||
|
||||
/// 消费并返回待恢复的阅读位置(一次性读取后清空)
|
||||
func consumePendingPresentationRestoreLocation() -> RDEPUBLocation? {
|
||||
defer { pendingPresentationRestoreLocation = nil }
|
||||
return pendingPresentationRestoreLocation
|
||||
}
|
||||
|
||||
/// 主动捕获当前阅读位置到待恢复队列,供后续视口变化后恢复使用
|
||||
func capturePendingPresentationRestoreLocation() {
|
||||
guard let controller else { return }
|
||||
pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation()
|
||||
}
|
||||
|
||||
/// 分页完成后处理挂起的视口变化(如有),避免分页过程中重复触发
|
||||
func processPendingChangeAfterPagination() {
|
||||
guard let pendingReason = pendingViewportChangeReason else { return }
|
||||
pendingViewportChangeReason = nil
|
||||
@@ -77,6 +86,7 @@ final class RDEPUBReaderViewportMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前视口签名,包含容器尺寸和安全区域信息
|
||||
func currentViewportSignature() -> RDEPUBViewportSignature? {
|
||||
guard let controller else { return nil }
|
||||
let containerSize = controller.readerView.bounds.size == .zero ? controller.view.bounds.size : controller.readerView.bounds.size
|
||||
@@ -92,6 +102,7 @@ final class RDEPUBReaderViewportMonitor {
|
||||
)
|
||||
}
|
||||
|
||||
/// 检测视口签名是否发生显著变化,若变化则触发重新分页或重建外部 TextBook
|
||||
func handleViewportChangeIfNeeded(
|
||||
reason: RDEPUBViewportChangeReason,
|
||||
viewportSignature: RDEPUBViewportSignature? = nil
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import UIKit
|
||||
|
||||
/// 自定义 UITextView,替换系统默认的 UIMenuItem 为自定义操作(拷贝、高亮、批注)
|
||||
/// 可选文本视图,继承自 UITextView。
|
||||
/// 替换系统默认的 UIMenuItem 为自定义操作(拷贝、高亮、批注),通过 `onSelectionAction` 回调通知外部。
|
||||
final class RDEPUBSelectableTextView: UITextView {
|
||||
/// 选择菜单操作回调
|
||||
/// 选择菜单操作回调,当用户选择拷贝、高亮或批注时触发
|
||||
var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)?
|
||||
|
||||
/// 判断指定菜单项是否可执行,仅允许自定义的拷贝、高亮、批注操作
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
switch action {
|
||||
case #selector(rd_copy(_:)),
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import UIKit
|
||||
|
||||
/// 前景覆盖层,负责绘制高亮、搜索命中和当前选区。
|
||||
/// 原生文本渲染路径的批注覆盖层,负责绘制用户高亮、搜索命中高亮和当前选区装饰。
|
||||
final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
||||
/// 将用户高亮批注以富文本属性形式应用到页面内容上
|
||||
/// - Parameters:
|
||||
/// - highlights: 高亮批注数组
|
||||
/// - content: 待修改的富文本
|
||||
/// - page: 目标文本页
|
||||
/// - contentBaseOffset: 内容在全局偏移中的起始位置
|
||||
func applyHighlights(
|
||||
_ highlights: [RDEPUBHighlight],
|
||||
to content: NSMutableAttributedString,
|
||||
@@ -38,6 +44,12 @@ final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将搜索命中高亮以富文本背景色形式应用到页面内容上
|
||||
/// - Parameters:
|
||||
/// - content: 待修改的富文本
|
||||
/// - page: 目标文本页
|
||||
/// - searchState: 当前搜索状态
|
||||
/// - contentBaseOffset: 内容在全局偏移中的起始位置
|
||||
func applySearchHighlights(
|
||||
to content: NSMutableAttributedString,
|
||||
page: RDEPUBTextPage,
|
||||
@@ -65,6 +77,13 @@ final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建页面的背景和前景装饰数组(搜索高亮为背景,下划线批注为前景)
|
||||
/// - Parameters:
|
||||
/// - page: 目标文本页
|
||||
/// - highlights: 高亮批注数组
|
||||
/// - searchState: 当前搜索状态
|
||||
/// - interactionController: 用于计算选区矩形的交互控制器
|
||||
/// - Returns: 分离的背景和前景装饰数组
|
||||
func buildDecorations(
|
||||
page: RDEPUBTextPage,
|
||||
highlights: [RDEPUBHighlight],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import UIKit
|
||||
|
||||
/// 背景覆盖层,负责绘制页内装饰和位于正文下方的提示层。
|
||||
/// 原生文本渲染路径的页级背景覆盖层。
|
||||
/// 继承自 RDEPUBSelectionOverlayView,负责绘制位于正文下方的页内装饰(如搜索高亮背景、批注背景等)。
|
||||
final class RDEPUBTextPageDecorationView: RDEPUBSelectionOverlayView {}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import UIKit
|
||||
|
||||
/// UIColor 十六进制颜色扩展,提供从 HEX 字符串创建颜色的便捷方法。
|
||||
extension UIColor {
|
||||
/// 从十六进制字符串创建颜色
|
||||
/// - Parameters:
|
||||
/// - rdHexString: 颜色 HEX 字符串,支持带或不带 "#" 前缀(如 "#FF5733" 或 "FF5733"),必须为 6 位
|
||||
/// - alpha: 透明度(0.0 ~ 1.0)
|
||||
convenience init?(rdHexString: String, alpha: CGFloat) {
|
||||
var value = rdHexString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
value = value.replacingOccurrences(of: "#", with: "")
|
||||
|
||||
Reference in New Issue
Block a user