源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
172 lines
7.3 KiB
Swift
172 lines
7.3 KiB
Swift
import Foundation
|
||
|
||
/// EPUB 阅读器分页协调器:负责出版物的分页计算和页面数据更新。
|
||
///
|
||
/// 职责:
|
||
/// - 根据出版物类型(文本重排/Fixed Layout/Web 内容)选择分页策略
|
||
/// - 后台构建文本图书模型并应用分页快照
|
||
/// - 重新分页时保持当前阅读位置
|
||
/// - 刷新可见内容并保持位置
|
||
/// - 重建外部纯文本图书
|
||
final class RDEPUBReaderPaginationCoordinator {
|
||
private unowned let context: RDEPUBReaderContext
|
||
|
||
init(context: RDEPUBReaderContext) {
|
||
self.context = context
|
||
}
|
||
|
||
/// 对出版物执行分页:文本重排走 TextBookBuilder,Fixed Layout 直接生成快照,Web 内容走 Paginator。
|
||
func paginatePublication(restoreLocation: RDEPUBLocation?) {
|
||
guard let controller = context.controller,
|
||
let parser = context.parser,
|
||
let publication = context.publication,
|
||
let readingSession = context.readingSession else {
|
||
return
|
||
}
|
||
|
||
controller.isRepaginating = true
|
||
controller.errorLabel.isHidden = true
|
||
controller.showLoading()
|
||
let token = UUID()
|
||
context.paginationToken = token
|
||
|
||
if publication.readingProfile == .textReflowable {
|
||
let pageSize = controller.currentTextPageSize()
|
||
context.lastTextPaginationPageSize = pageSize
|
||
let layoutConfig = controller.currentTextLayoutConfig(pageSize: pageSize)
|
||
let builder = context.makeTextBookBuilder(layoutConfig: layoutConfig)
|
||
let renderStyle = controller.currentTextRenderStyle()
|
||
|
||
DispatchQueue.global(qos: .userInitiated).async { [weak controller] in
|
||
guard let controller else { return }
|
||
do {
|
||
let textBook = try builder.build(
|
||
parser: parser,
|
||
publication: publication,
|
||
pageSize: pageSize,
|
||
style: renderStyle
|
||
)
|
||
DispatchQueue.main.async {
|
||
guard self.context.paginationToken == token else { return }
|
||
self.context.runtime?.applyTextBook(textBook, restoreLocation: restoreLocation)
|
||
}
|
||
} catch {
|
||
DispatchQueue.main.async {
|
||
guard self.context.paginationToken == token else { return }
|
||
self.context.handle(error: error)
|
||
}
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
if publication.layout == .fixed {
|
||
let snapshot = readingSession.makePaginationSnapshot(
|
||
pageCounts: Array(repeating: 1, count: publication.spine.count),
|
||
preferences: controller.currentPreferences(),
|
||
layoutContext: controller.currentLayoutContext()
|
||
)
|
||
context.runtime?.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation)
|
||
return
|
||
}
|
||
|
||
let paginator = context.makePaginator()
|
||
context.paginator = paginator
|
||
paginator.calculate(
|
||
parser: parser,
|
||
hostingView: controller.ensurePaginationHostView(),
|
||
presentation: controller.currentPreferences().presentationStyle(viewportSize: controller.currentLayoutContext().viewportSize)
|
||
) { [weak controller] pageCounts in
|
||
guard let controller, self.context.paginationToken == token else { return }
|
||
let snapshot = readingSession.makePaginationSnapshot(
|
||
pageCounts: pageCounts,
|
||
preferences: controller.currentPreferences(),
|
||
layoutContext: controller.currentLayoutContext()
|
||
)
|
||
self.context.paginator = nil
|
||
self.context.runtime?.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation)
|
||
}
|
||
}
|
||
|
||
/// 应用文本图书模型:生成分页快照并完成分页流程。
|
||
func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) {
|
||
guard let controller = context.controller else { return }
|
||
context.textBook = textBook
|
||
let snapshot = controller.nativeTextSnapshot(from: textBook)
|
||
context.replaceActiveSnapshot(snapshot)
|
||
|
||
guard !textBook.pages.isEmpty else {
|
||
context.handle(error: RDEPUBParserError.emptySpine)
|
||
return
|
||
}
|
||
|
||
finishPagination(restoreLocation: restoreLocation)
|
||
}
|
||
|
||
/// 应用分页快照(Fixed Layout 或 Web 内容),并完成分页流程。
|
||
func applyPaginationSnapshot(
|
||
_ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]),
|
||
restoreLocation: RDEPUBLocation?
|
||
) {
|
||
guard let controller = context.controller else { return }
|
||
context.textBook = nil
|
||
context.replaceActiveSnapshot(snapshot)
|
||
|
||
guard !snapshot.pages.isEmpty else {
|
||
context.handle(error: RDEPUBParserError.emptySpine)
|
||
return
|
||
}
|
||
|
||
finishPagination(restoreLocation: restoreLocation)
|
||
}
|
||
|
||
/// 分页完成后的收尾:刷新视图、恢复阅读位置、处理待定视口变更。
|
||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||
guard let controller = context.controller,
|
||
let readerView = context.readerView else { return }
|
||
controller.isRepaginating = false
|
||
controller.hideLoading()
|
||
readerView.reloadData()
|
||
if let targetLocation = restoreLocation {
|
||
controller.restoreReadingLocation(targetLocation)
|
||
} else {
|
||
readerView.transitionToPage(pageNum: 0)
|
||
context.readingSession?.transition(to: .idle)
|
||
}
|
||
|
||
context.runtime?.viewportMonitor.processPendingChangeAfterPagination()
|
||
}
|
||
|
||
/// 重新分页并保持当前阅读位置(优先使用待恢复位置,其次当前位置,最后持久化位置)。
|
||
func repaginatePreservingCurrentLocation() {
|
||
guard context.publication != nil else { return }
|
||
let restoreLocation = context.runtime?.viewportMonitor.consumePendingPresentationRestoreLocation()
|
||
?? context.currentVisibleLocation()
|
||
?? context.persistenceLocation()
|
||
paginatePublication(restoreLocation: restoreLocation)
|
||
}
|
||
|
||
/// 刷新可见内容并保持当前阅读位置不变。
|
||
func refreshVisibleContentPreservingLocation() {
|
||
guard let readerView = context.readerView else { return }
|
||
let restoreLocation = context.currentVisibleLocation() ?? context.persistenceLocation()
|
||
readerView.reloadData()
|
||
if let restoreLocation {
|
||
_ = context.restoreReadingLocation(restoreLocation)
|
||
}
|
||
}
|
||
|
||
/// 重建外部纯文本图书(布局变更后重新排版)。
|
||
func rebuildExternalTextBook() {
|
||
guard let controller = context.controller,
|
||
let textFileURL = controller.textFileURL else { return }
|
||
let restoreLocation = context.currentVisibleLocation() ?? context.persistenceLocation()
|
||
let pageSize = controller.currentTextPageSize()
|
||
let style = controller.currentTextRenderStyle()
|
||
let builder = context.makePlainTextBookBuilder(layoutConfig: controller.currentTextLayoutConfig(pageSize: pageSize))
|
||
if let newBook = try? builder.build(textFileURL: textFileURL, pageSize: pageSize, style: style) {
|
||
context.runtime?.applyTextBook(newBook, restoreLocation: restoreLocation)
|
||
}
|
||
}
|
||
}
|