feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,65 +1,51 @@
|
||||
import UIKit
|
||||
|
||||
/// 阅读器共享状态中心:所有 coordinator 通过 context 访问业务状态和便捷方法。
|
||||
///
|
||||
/// context 持有:
|
||||
/// - 业务状态(parser、publication、textBook、pages 等)
|
||||
/// - UI 配置(configuration、brightness)
|
||||
/// - 持久化策略(persistence)
|
||||
/// - 便捷方法(renderStyle、layoutConfig 等)
|
||||
/// - 弱引用 controller(仅用于 UIKit 呈现操作)
|
||||
final class RDEPUBReaderContext {
|
||||
|
||||
private let activityLock = NSLock()
|
||||
|
||||
private var lastUserNavigationTimestamp: CFAbsoluteTime = 0
|
||||
|
||||
// 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?
|
||||
/// 原生文本排版生成的图书模型(仅文本重排模式)。
|
||||
/// 章节模式下为 nil,内容通过 ChapterRuntimeStore 访问。
|
||||
|
||||
var textBook: RDEPUBTextBook?
|
||||
/// 全书轻量页码映射(章节模式)。约 100KB/1000章,不持有 NSAttributedString。
|
||||
|
||||
var bookPageMap: RDEPUBBookPageMap?
|
||||
/// 当前书籍的所有书签。
|
||||
|
||||
var activeBookmarks: [RDEPUBBookmark] = []
|
||||
/// 当前书籍的所有高亮标注。
|
||||
|
||||
var activeHighlights: [RDEPUBHighlight] = []
|
||||
/// 当前打开书籍的唯一标识。
|
||||
|
||||
var currentBookIdentifier: String?
|
||||
/// 分页操作令牌,用于取消过期的异步分页任务。
|
||||
|
||||
var paginationToken = UUID()
|
||||
/// Web 内容分页计算器。
|
||||
|
||||
var paginator: RDEPUBPaginator?
|
||||
/// 全文搜索状态。
|
||||
|
||||
var searchState: RDEPUBSearchState?
|
||||
/// 后台解析完成的完整 BookPageMap,等待用户下次导航时应用。
|
||||
/// 避免后台解析完成时直接替换 map 导致当前阅读位置跳转。
|
||||
|
||||
var pendingFullPageMap: RDEPUBBookPageMap?
|
||||
/// 上次文本分页时的页面尺寸,用于检测是否需要重新分页。
|
||||
|
||||
var lastTextPaginationPageSize: CGSize?
|
||||
/// 后台元数据解析耗时(毫秒),仅包含 OperationQueue 并行阶段。
|
||||
|
||||
var lastMetadataParseWallClockMs: Int = 0
|
||||
/// 后台元数据解析使用的并发数。
|
||||
|
||||
var lastMetadataParseConcurrency: Int = 0
|
||||
/// 当前用户文本选区(对外只读语义,底层由 selectionState 推导)。
|
||||
|
||||
var currentSelection: RDEPUBSelection? {
|
||||
get { selectionState.selection }
|
||||
set {
|
||||
@@ -70,38 +56,30 @@ final class RDEPUBReaderContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 统一选区状态模型,收口所有选区相关状态变更。
|
||||
|
||||
var selectionState: RDEPUBSelectionState = .idle
|
||||
|
||||
// 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: - 初始化
|
||||
var persistence: RDEPUBReaderPersistence?
|
||||
|
||||
var epubURL: URL = URL(string: "about:blank")!
|
||||
|
||||
var isRepaginating: Bool = false
|
||||
|
||||
var didStartInitialLoad: Bool = false
|
||||
|
||||
var isExternalTextBook: Bool = false
|
||||
|
||||
var textFileURL: URL?
|
||||
|
||||
var textBookCache = RDEPUBTextBookCache()
|
||||
|
||||
init(controller: RDEPUBReaderController) {
|
||||
self.controller = controller
|
||||
self.readerView = controller.readerView
|
||||
}
|
||||
|
||||
// MARK: - 便捷方法
|
||||
|
||||
/// 根据当前 readerView 和控制器尺寸构建布局上下文。
|
||||
func currentLayoutContext() -> RDEPUBNavigatorLayoutContext {
|
||||
let containerSize = readerView?.bounds.size ?? .zero
|
||||
let viewSize = controller?.view.bounds.size ?? containerSize
|
||||
@@ -115,12 +93,10 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据当前配置生成阅读偏好设置。
|
||||
func currentPreferences() -> RDEPUBPreferences {
|
||||
configuration.makePreferences()
|
||||
}
|
||||
|
||||
/// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。
|
||||
func currentTextPageSize() -> CGSize {
|
||||
if Thread.isMainThread {
|
||||
let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil
|
||||
@@ -149,7 +125,6 @@ final class RDEPUBReaderContext {
|
||||
return dependencies.environment.fallbackViewportSize
|
||||
}
|
||||
|
||||
/// 根据当前配置生成文本渲染样式(字体、行距、颜色)。
|
||||
func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||
let font = configuration.fontChoice.font(ofSize: configuration.fontSize)
|
||||
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
|
||||
@@ -161,7 +136,6 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据页面尺寸和配置生成文本排版参数。
|
||||
func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
|
||||
return RDEPUBTextLayoutConfig(
|
||||
frameWidth: max(pageSize.width, 1),
|
||||
@@ -169,7 +143,7 @@ final class RDEPUBReaderContext {
|
||||
edgeInsets: configuration.reflowableContentInsets,
|
||||
numberOfColumns: configuration.numberOfColumns,
|
||||
columnGap: configuration.columnGap,
|
||||
// 小说正文更看重尽量铺满页面,避免页尾出现明显留白。
|
||||
|
||||
avoidOrphans: false,
|
||||
avoidWidows: false,
|
||||
avoidPageBreakInsideEnabled: true,
|
||||
@@ -179,48 +153,39 @@ 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)
|
||||
}
|
||||
@@ -252,8 +217,6 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 使用预计算的 contentHash 构建缓存键,避免重复读取 HTML 和计算 SHA-256。
|
||||
/// 后台批量解析必须走此版本。
|
||||
func chapterCacheKey(forSpineIndex spineIndex: Int, precomputedContentHash: String) -> RDEPUBChapterCacheKey {
|
||||
chapterCacheKey(
|
||||
forSpineIndex: spineIndex,
|
||||
@@ -262,8 +225,6 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 使用固定的渲染签名与预计算 contentHash 构建缓存键。
|
||||
/// 适合后台任务在启动时冻结分页参数后复用,避免 live context 漂移。
|
||||
func chapterCacheKey(
|
||||
forSpineIndex spineIndex: Int,
|
||||
precomputedContentHash: String,
|
||||
@@ -277,7 +238,6 @@ final class RDEPUBReaderContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// 当前渲染参数签名,所有章节共享同一值。
|
||||
func currentRenderSignature() -> String {
|
||||
let style = currentTextRenderStyle()
|
||||
let pageSize = currentTextPageSize()
|
||||
@@ -311,36 +271,30 @@ final class RDEPUBReaderContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// 工厂方法:创建纯文本图书构建器。
|
||||
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)
|
||||
}
|
||||
|
||||
/// 记录最近一次用户翻页/跳转行为,用于后台分页让路。
|
||||
func markUserNavigationActivity() {
|
||||
activityLock.lock()
|
||||
lastUserNavigationTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
activityLock.unlock()
|
||||
}
|
||||
|
||||
/// 距离最近一次用户翻页/跳转已经过去的时间。
|
||||
func secondsSinceLastUserNavigation() -> CFAbsoluteTime {
|
||||
activityLock.lock()
|
||||
let timestamp = lastUserNavigationTimestamp
|
||||
@@ -349,7 +303,6 @@ final class RDEPUBReaderContext {
|
||||
return CFAbsoluteTimeGetCurrent() - timestamp
|
||||
}
|
||||
|
||||
/// 根据规范化 href 获取文本章节数据。
|
||||
func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? {
|
||||
guard let textBook, let publication else { return nil }
|
||||
let normalizedHref = publication.resourceResolver.normalizedHref(href) ?? href
|
||||
@@ -358,42 +311,34 @@ 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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user