feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,65 +1,78 @@
|
||||
import UIKit
|
||||
|
||||
/// EPUB 阅读器运行时总协调器。
|
||||
/// 统一持有并分发给加载、分页、定位、搜索、工具栏、批注、视口监测等子协调器,
|
||||
/// 作为阅读器控制器的门面(Facade),简化外部调用。
|
||||
final class RDEPUBReaderRuntime {
|
||||
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
lazy var chapterRuntimeStore = RDEPUBChapterRuntimeStore()
|
||||
|
||||
lazy var summaryDiskCache = context.makeChapterSummaryDiskCache()
|
||||
|
||||
lazy var chapterLoader: RDEPUBChapterLoader = {
|
||||
let loader = RDEPUBChapterLoader(context: context)
|
||||
loader.setSummaryDiskCache(summaryDiskCache)
|
||||
return loader
|
||||
}()
|
||||
|
||||
lazy var pageResolver = RDEPUBPageResolver(context: context, store: chapterRuntimeStore)
|
||||
|
||||
lazy var loadCoordinator = RDEPUBReaderLoadCoordinator(context: context)
|
||||
|
||||
lazy var paginationCoordinator = RDEPUBReaderPaginationCoordinator(context: context)
|
||||
|
||||
lazy var locationCoordinator = RDEPUBReaderLocationCoordinator(context: context)
|
||||
|
||||
lazy var searchCoordinator = RDEPUBReaderSearchCoordinator(context: context)
|
||||
|
||||
lazy var chromeCoordinator = RDEPUBReaderChromeCoordinator(context: context)
|
||||
|
||||
lazy var annotationCoordinator = RDEPUBReaderAnnotationCoordinator(context: context)
|
||||
|
||||
lazy var viewportMonitor = RDEPUBReaderViewportMonitor(context: context)
|
||||
|
||||
lazy var jumpSessionManager = RDEPUBJumpSessionManager(context: context)
|
||||
|
||||
lazy var backgroundPriorityManager = RDEPUBBackgroundPriorityManager(context: context)
|
||||
|
||||
lazy var backgroundCoverageStore = RDEPUBBackgroundCoverageStore(context: context)
|
||||
|
||||
lazy var reconciliationCoordinator = RDEPUBPageMapReconciliationCoordinator(context: context)
|
||||
|
||||
/// 设置页面是否打开
|
||||
var isSettingsPanelOpen: Bool = false
|
||||
|
||||
/// 标记需要在设置页面关闭后触发完整补全
|
||||
var needsFullRepaginationAfterSettingsClose: Bool = false
|
||||
|
||||
/// 设置页内当前章预览的代次,用于丢弃过期结果
|
||||
private var settingsPreviewGeneration: Int = 0
|
||||
/// 设置页内 preview 防抖任务,只保留最后一次请求。
|
||||
|
||||
private var pendingSettingsPreviewWorkItem: DispatchWorkItem?
|
||||
/// 设置页内 preview 防抖延迟,合并连续点击。
|
||||
|
||||
private let settingsPreviewDebounceDelay: TimeInterval = 0.2
|
||||
|
||||
private struct SettingsPreviewAnchor {
|
||||
|
||||
let spineIndex: Int
|
||||
|
||||
let href: String
|
||||
|
||||
let offset: Int
|
||||
}
|
||||
|
||||
init(context: RDEPUBReaderContext) {
|
||||
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
|
||||
@@ -80,20 +93,10 @@ 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,
|
||||
@@ -134,7 +137,6 @@ final class RDEPUBReaderRuntime {
|
||||
return true
|
||||
}
|
||||
|
||||
/// 清除当前选区
|
||||
func clearSelection() {
|
||||
annotationCoordinator.updateCurrentSelection(nil)
|
||||
}
|
||||
@@ -230,30 +232,25 @@ 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()
|
||||
}
|
||||
|
||||
/// 跳转到指定搜索匹配项
|
||||
@discardableResult
|
||||
func selectSearchMatch(at index: Int) -> Bool {
|
||||
searchCoordinator.selectSearchMatch(at: index)
|
||||
}
|
||||
|
||||
/// 清除搜索状态
|
||||
func clearSearch() {
|
||||
searchCoordinator.clearSearch()
|
||||
}
|
||||
@@ -262,32 +259,26 @@ 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,
|
||||
@@ -306,17 +297,14 @@ 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?
|
||||
@@ -332,21 +320,26 @@ final class RDEPUBReaderRuntime {
|
||||
}
|
||||
|
||||
func refreshBookPageMapInPlace(_ bookPageMap: RDEPUBBookPageMap) {
|
||||
// 暂存完整 map,等用户下次导航时再应用,避免当前阅读位置跳转
|
||||
// 此时保留旧 map,用户看到的内容和页码完全不变
|
||||
if let pendingMap = context.pendingFullPageMap {
|
||||
let shouldKeepExisting =
|
||||
pendingMap.totalChapters > bookPageMap.totalChapters ||
|
||||
(pendingMap.totalChapters == bookPageMap.totalChapters &&
|
||||
pendingMap.totalPages >= bookPageMap.totalPages)
|
||||
if shouldKeepExisting {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
context.pendingFullPageMap = bookPageMap
|
||||
}
|
||||
|
||||
/// 用户导航时检查并应用待处理的完整 BookPageMap
|
||||
func applyPendingFullPageMapIfNeeded() {
|
||||
guard let pendingMap = context.pendingFullPageMap,
|
||||
let readerView = context.readerView,
|
||||
let controller = context.controller else { return }
|
||||
|
||||
// 如果正在重新分页,跳过本次检查,避免状态冲突
|
||||
guard !controller.isRepaginating else { return }
|
||||
|
||||
// 使用协调器判断是否允许接管
|
||||
let decision = reconciliationCoordinator.evaluateTakeover(
|
||||
candidatePageMap: pendingMap,
|
||||
candidateSegment: nil,
|
||||
@@ -364,13 +357,12 @@ final class RDEPUBReaderRuntime {
|
||||
applyFullPageMapReplacement(newPageMap, readerView: readerView, controller: controller)
|
||||
|
||||
case .expandWindow, .segmentReplace:
|
||||
// 这些情况在当前实现中不会发生,因为我们传入的是 candidatePageMap
|
||||
|
||||
RDEPUBBackgroundTrace.log("Reconciliation", "decision: unexpected segment decision")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用全量页图替换
|
||||
private func applyFullPageMapReplacement(
|
||||
_ newPageMap: RDEPUBBookPageMap,
|
||||
readerView: RDReaderView,
|
||||
@@ -380,12 +372,10 @@ final class RDEPUBReaderRuntime {
|
||||
|
||||
context.pendingFullPageMap = nil
|
||||
|
||||
// 替换 map 和快照
|
||||
context.textBook = nil
|
||||
context.bookPageMap = newPageMap
|
||||
context.replaceActiveSnapshot(makeSnapshot(from: newPageMap))
|
||||
|
||||
// 用位置在新 map 中重新解析正确的页码
|
||||
if let currentLocation {
|
||||
let newPageNumber = controller.pageNumber(for: currentLocation) ?? (readerView.currentPage + 1)
|
||||
let newPage = max(0, newPageNumber - 1)
|
||||
@@ -397,7 +387,6 @@ final class RDEPUBReaderRuntime {
|
||||
readerView.reloadPageCountOnly()
|
||||
}
|
||||
|
||||
// 检查是否应该结束 JumpSession(coverage-complete)
|
||||
if let currentLocation,
|
||||
let currentSpineIndex = context.normalizedSpineIndex(for: currentLocation),
|
||||
let activeSession = jumpSessionManager.activeSession {
|
||||
@@ -410,14 +399,12 @@ final class RDEPUBReaderRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成分页流程并恢复阅读位置
|
||||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
/// 重新分页并保持当前阅读位置不变
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
// 如果设置页面打开,只计算当前章节(热区)
|
||||
|
||||
if isSettingsPanelOpen {
|
||||
needsFullRepaginationAfterSettingsClose = true
|
||||
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||
@@ -427,7 +414,6 @@ final class RDEPUBReaderRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 防抖调度设置页内的当前章 preview,只保留最后一次请求。
|
||||
private func scheduleSettingsPreviewRepagination() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
settingsPreviewGeneration += 1
|
||||
@@ -440,8 +426,12 @@ final class RDEPUBReaderRuntime {
|
||||
return
|
||||
}
|
||||
self.pendingSettingsPreviewWorkItem = nil
|
||||
let previewAnchor = self.captureSettingsPreviewAnchor()
|
||||
self.chapterRuntimeStore.invalidateAllForSettingsChange()
|
||||
self.repaginateCurrentChapterOnly(previewGeneration: previewGeneration)
|
||||
self.repaginateCurrentChapterOnly(
|
||||
previewGeneration: previewGeneration,
|
||||
previewAnchor: previewAnchor
|
||||
)
|
||||
}
|
||||
pendingSettingsPreviewWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(
|
||||
@@ -450,8 +440,34 @@ final class RDEPUBReaderRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
/// 只重新计算当前章节(设置页面打开时使用)
|
||||
private func repaginateCurrentChapterOnly(previewGeneration: Int) {
|
||||
private func captureSettingsPreviewAnchor() -> SettingsPreviewAnchor? {
|
||||
guard let bookPageMap = context.bookPageMap,
|
||||
let readerView = context.readerView else { return nil }
|
||||
|
||||
let absolutePageIndex = readerView.currentPage
|
||||
guard absolutePageIndex >= 0,
|
||||
let spineIndex = bookPageMap.spineIndex(forAbsolutePage: absolutePageIndex),
|
||||
let localPageIndex = bookPageMap.localPageIndex(forAbsolutePage: absolutePageIndex),
|
||||
let chapter = chapterRuntimeStore.chapterData(for: spineIndex),
|
||||
chapter.pages.indices.contains(localPageIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let page = chapter.pages[localPageIndex]
|
||||
let offset = page.contentRange.length > 0
|
||||
? page.contentRange.location
|
||||
: page.pageStartOffset
|
||||
return SettingsPreviewAnchor(
|
||||
spineIndex: spineIndex,
|
||||
href: chapter.href,
|
||||
offset: offset
|
||||
)
|
||||
}
|
||||
|
||||
private func repaginateCurrentChapterOnly(
|
||||
previewGeneration: Int,
|
||||
previewAnchor: SettingsPreviewAnchor?
|
||||
) {
|
||||
guard let bookPageMap = context.bookPageMap,
|
||||
let readerView = context.readerView else { return }
|
||||
|
||||
@@ -485,11 +501,18 @@ final class RDEPUBReaderRuntime {
|
||||
self.context.replaceActiveSnapshot(self.makeSnapshot(from: partialMap))
|
||||
readerView.reloadData()
|
||||
|
||||
if let targetPage = self.settingsPreviewTargetPage(
|
||||
in: chapter,
|
||||
for: previewAnchor
|
||||
) {
|
||||
readerView.transitionToPage(pageNum: targetPage, animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
if let previewLocation,
|
||||
self.locationCoordinator.restoreReadingLocation(previewLocation, animated: false) {
|
||||
return
|
||||
}
|
||||
|
||||
readerView.transitionToPage(pageNum: 0, animated: false)
|
||||
|
||||
case .failure(let error):
|
||||
@@ -501,7 +524,37 @@ final class RDEPUBReaderRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页面即将打开
|
||||
private func settingsPreviewTargetPage(
|
||||
in chapter: RDEPUBRuntimeChapter,
|
||||
for anchor: SettingsPreviewAnchor?
|
||||
) -> Int? {
|
||||
guard let anchor,
|
||||
anchor.spineIndex == chapter.spineIndex,
|
||||
anchor.href == chapter.href,
|
||||
!chapter.pages.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let exactPage = chapter.pages.first(where: { page in
|
||||
let lowerBound = page.contentRange.location
|
||||
let upperBound = page.contentRange.location + page.contentRange.length
|
||||
if page.contentRange.length == 0 {
|
||||
return anchor.offset == lowerBound
|
||||
}
|
||||
return anchor.offset >= lowerBound && anchor.offset < upperBound
|
||||
}) {
|
||||
return exactPage.pageIndexInChapter
|
||||
}
|
||||
|
||||
if let nextPage = chapter.pages.first(where: { page in
|
||||
page.contentRange.location > anchor.offset
|
||||
}) {
|
||||
return nextPage.pageIndexInChapter
|
||||
}
|
||||
|
||||
return max(chapter.pages.count - 1, 0)
|
||||
}
|
||||
|
||||
func settingsPanelWillAppear() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
pendingSettingsPreviewWorkItem = nil
|
||||
@@ -510,13 +563,12 @@ final class RDEPUBReaderRuntime {
|
||||
settingsPreviewGeneration += 1
|
||||
}
|
||||
|
||||
/// 设置页面已关闭
|
||||
func settingsPanelDidDisappear() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
pendingSettingsPreviewWorkItem = nil
|
||||
isSettingsPanelOpen = false
|
||||
settingsPreviewGeneration += 1
|
||||
// 如果在设置页面期间有配置变化,触发完整补全
|
||||
|
||||
if needsFullRepaginationAfterSettingsClose {
|
||||
needsFullRepaginationAfterSettingsClose = false
|
||||
RDEPUBBackgroundTrace.log("Runtime", "settingsPanelDidDisappear: triggering full repagination")
|
||||
@@ -524,17 +576,14 @@ final class RDEPUBReaderRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新当前可见内容,保持阅读位置不变
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
paginationCoordinator.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
/// 重建外部 TextBook 数据
|
||||
func rebuildExternalTextBook() {
|
||||
paginationCoordinator.rebuildExternalTextBook()
|
||||
}
|
||||
|
||||
/// 恢复到指定阅读位置
|
||||
@discardableResult
|
||||
func restoreReadingLocation(
|
||||
_ location: RDEPUBLocation,
|
||||
@@ -548,17 +597,14 @@ final class RDEPUBReaderRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取当前可见页面的阅读位置
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
locationCoordinator.currentVisibleLocation()
|
||||
}
|
||||
|
||||
/// 获取当前视口签名快照
|
||||
func currentViewportSignature() -> RDEPUBViewportSignature? {
|
||||
viewportMonitor.currentViewportSignature()
|
||||
}
|
||||
|
||||
/// 视口变化时检查是否需要重新分页
|
||||
func handleViewportChangeIfNeeded(
|
||||
reason: RDEPUBViewportChangeReason,
|
||||
viewportSignature: RDEPUBViewportSignature? = nil
|
||||
@@ -574,7 +620,6 @@ final class RDEPUBReaderRuntime {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否是远距跳转
|
||||
let currentSpineIndex = locationCoordinator.currentVisibleLocation()
|
||||
.flatMap { context.normalizedSpineIndex(for: $0) }
|
||||
let isDistantJump = if let current = currentSpineIndex {
|
||||
@@ -584,7 +629,7 @@ final class RDEPUBReaderRuntime {
|
||||
}
|
||||
|
||||
if context.bookPageMap?.entry(forSpineIndex: targetSpineIndex) != nil {
|
||||
// 如果是远距跳转且目标已在当前窗口中,创建 JumpSession
|
||||
|
||||
if isDistantJump {
|
||||
jumpSessionManager.createSession(
|
||||
anchorSpineIndex: targetSpineIndex,
|
||||
@@ -641,14 +686,13 @@ final class RDEPUBReaderRuntime {
|
||||
context.replaceActiveSnapshot(makeSnapshot(from: partialMap))
|
||||
context.readerView?.reloadData()
|
||||
|
||||
// 远距跳转成功后创建 JumpSession
|
||||
if isDistantJump {
|
||||
jumpSessionManager.createSession(
|
||||
anchorSpineIndex: targetSpineIndex,
|
||||
reason: .tableOfContentsJump,
|
||||
totalSpineCount: publication.spine.count
|
||||
)
|
||||
// 添加温区锚点
|
||||
|
||||
backgroundPriorityManager.addWarmAnchor(spineIndex: targetSpineIndex)
|
||||
}
|
||||
|
||||
@@ -723,25 +767,23 @@ final class RDEPUBReaderRuntime {
|
||||
return
|
||||
}
|
||||
|
||||
// 确定当前阅读方向,优先向当前方向扩展
|
||||
let currentSpineIndex = locationCoordinator.currentVisibleLocation()
|
||||
.flatMap { context.normalizedSpineIndex(for: $0) }
|
||||
let isNearEnd = currentMap.totalPages - currentPageNumber <= minimumTrailingPages
|
||||
let isNearStart = currentPageNumber <= minimumTrailingPages
|
||||
|
||||
// 根据阅读方向决定扩展策略
|
||||
var spineIndicesToAppend: [Int] = []
|
||||
if isNearEnd {
|
||||
// 向后扩展
|
||||
|
||||
let lastKnownSpineIndex = currentMap.entries.last?.spineIndex ?? -1
|
||||
spineIndicesToAppend = Array(buildableSpineIndices.filter { $0 > lastKnownSpineIndex }.prefix(batchChapterCount))
|
||||
} else if isNearStart {
|
||||
// 向前扩展
|
||||
|
||||
let firstKnownSpineIndex = currentMap.entries.first?.spineIndex ?? Int.max
|
||||
let prependCandidates = buildableSpineIndices.filter { $0 < firstKnownSpineIndex }
|
||||
spineIndicesToAppend = Array(prependCandidates.suffix(batchChapterCount))
|
||||
} else {
|
||||
// 不在边界,不扩展
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -816,6 +858,40 @@ final class RDEPUBReaderRuntime {
|
||||
readerView.transitionToPage(pageNum: max(currentPageNumber - 1, 0), animated: false)
|
||||
}
|
||||
|
||||
func prefetchForwardChaptersAfterInitialOpen(anchorSpineIndex: Int, totalSpineCount: Int) {
|
||||
guard context.publication != nil else { return }
|
||||
|
||||
chapterRuntimeStore.setCurrentChapter(
|
||||
spineIndex: anchorSpineIndex,
|
||||
totalSpineCount: totalSpineCount,
|
||||
windowRadius: context.configuration.chapterWindowRadius
|
||||
)
|
||||
|
||||
let forwardTargets = chapterRuntimeStore.windowSpineIndices.filter { $0 > anchorSpineIndex }
|
||||
guard !forwardTargets.isEmpty else { return }
|
||||
|
||||
for spineIndex in forwardTargets {
|
||||
if chapterRuntimeStore.chapterData(for: spineIndex) != nil {
|
||||
appendLoadedForwardChaptersToCurrentPageMapIfPossible()
|
||||
continue
|
||||
}
|
||||
|
||||
chapterRuntimeStore.addPrefetchTarget(spineIndex)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"initial open prefetch forward spine=\(spineIndex)"
|
||||
)
|
||||
chapterLoader.loadChapter(
|
||||
spineIndex: spineIndex,
|
||||
store: chapterRuntimeStore,
|
||||
priority: .prefetch
|
||||
) { [weak self] result in
|
||||
guard let self, case .success = result else { return }
|
||||
self.appendLoadedForwardChaptersToCurrentPageMapIfPossible()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clearOnDemandPageModeState() {
|
||||
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||
chapterRuntimeStore.invalidateAllForSettingsChange()
|
||||
@@ -826,7 +902,6 @@ final class RDEPUBReaderRuntime {
|
||||
backgroundCoverageStore.clearAll()
|
||||
}
|
||||
|
||||
/// 处理内存警告
|
||||
func handleMemoryWarning() {
|
||||
let currentSpineIndex = locationCoordinator.currentVisibleLocation()
|
||||
.flatMap { context.normalizedSpineIndex(for: $0) }
|
||||
@@ -893,6 +968,76 @@ final class RDEPUBReaderRuntime {
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private func appendLoadedForwardChaptersToCurrentPageMapIfPossible() {
|
||||
guard let publication = context.publication,
|
||||
let currentMap = context.bookPageMap,
|
||||
let readerView = context.readerView,
|
||||
let lastKnownSpineIndex = currentMap.entries.last?.spineIndex else {
|
||||
return
|
||||
}
|
||||
|
||||
let buildableSpineIndices = publication.spine.indices.filter {
|
||||
let item = publication.spine[$0]
|
||||
return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml"))
|
||||
}
|
||||
|
||||
var appendedEntries: [RDEPUBBookPageMapEntry] = []
|
||||
for spineIndex in buildableSpineIndices where spineIndex > lastKnownSpineIndex {
|
||||
guard let chapter = chapterRuntimeStore.chapterData(for: spineIndex) else {
|
||||
break
|
||||
}
|
||||
appendedEntries.append(
|
||||
RDEPUBBookPageMapEntry(
|
||||
spineIndex: chapter.spineIndex,
|
||||
href: chapter.href,
|
||||
title: chapter.title,
|
||||
pageCount: chapter.pages.count,
|
||||
absolutePageStart: 0,
|
||||
fragmentOffsets: chapter.chapterOffsetMap.fragmentOffsets
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
guard !appendedEntries.isEmpty else { return }
|
||||
|
||||
let existingEntries = currentMap.entries.map {
|
||||
RDEPUBBookPageMapEntry(
|
||||
spineIndex: $0.spineIndex,
|
||||
href: $0.href,
|
||||
title: $0.title,
|
||||
pageCount: $0.pageCount,
|
||||
absolutePageStart: 0,
|
||||
fragmentOffsets: $0.fragmentOffsets
|
||||
)
|
||||
}
|
||||
|
||||
var absolutePageStart = 0
|
||||
let newEntries = (existingEntries + appendedEntries).map { entry -> RDEPUBBookPageMapEntry in
|
||||
let normalizedEntry = RDEPUBBookPageMapEntry(
|
||||
spineIndex: entry.spineIndex,
|
||||
href: entry.href,
|
||||
title: entry.title,
|
||||
pageCount: entry.pageCount,
|
||||
absolutePageStart: absolutePageStart,
|
||||
fragmentOffsets: entry.fragmentOffsets
|
||||
)
|
||||
absolutePageStart += entry.pageCount
|
||||
return normalizedEntry
|
||||
}
|
||||
|
||||
let newMap = RDEPUBBookPageMap(entries: newEntries)
|
||||
guard newMap.totalPages > currentMap.totalPages else { return }
|
||||
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"appendLoadedForwardChapters chapters=\(newMap.totalChapters) pages=\(newMap.totalPages)"
|
||||
)
|
||||
|
||||
context.bookPageMap = newMap
|
||||
context.replaceActiveSnapshot(makeSnapshot(from: newMap))
|
||||
readerView.reloadPageCountOnly()
|
||||
}
|
||||
|
||||
private func makeSnapshot(from bookPageMap: RDEPUBBookPageMap) -> RDEPUBReadingSession.PaginationSnapshot {
|
||||
let pages = bookPageMap.entries.flatMap { entry in
|
||||
(0..<entry.pageCount).map { localPageIndex in
|
||||
|
||||
Reference in New Issue
Block a user