feat: complete large-book pagination runtime and UI coverage
This commit is contained in:
@@ -6,6 +6,15 @@ import UIKit
|
||||
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)
|
||||
@@ -42,9 +51,11 @@ final class RDEPUBReaderRuntime {
|
||||
context.clearActiveSnapshot()
|
||||
context.readingSession = nil
|
||||
context.textBook = nil
|
||||
context.bookPageMap = nil
|
||||
context.activeBookmarks = []
|
||||
context.activeHighlights = []
|
||||
context.searchState = nil
|
||||
clearOnDemandPageModeState()
|
||||
viewportMonitor.resetForReload()
|
||||
annotationCoordinator.updateCurrentSelection(nil)
|
||||
readerView.reloadData()
|
||||
@@ -84,6 +95,17 @@ final class RDEPUBReaderRuntime {
|
||||
return true
|
||||
}
|
||||
|
||||
if context.bookPageMap != nil {
|
||||
guard prepareOnDemandChapter(forAbsolutePageNumber: pageNumber) else {
|
||||
return false
|
||||
}
|
||||
readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
|
||||
if let location = locationCoordinator.currentVisibleLocation() {
|
||||
context.persist(location: location)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
guard context.activePages.indices.contains(pageNumber - 1) else {
|
||||
return false
|
||||
}
|
||||
@@ -282,6 +304,40 @@ final class RDEPUBReaderRuntime {
|
||||
paginationCoordinator.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
func applyBookPageMap(_ bookPageMap: RDEPUBBookPageMap, restoreLocation: RDEPUBLocation?) {
|
||||
context.textBook = nil
|
||||
context.bookPageMap = bookPageMap
|
||||
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
|
||||
paginationCoordinator.finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
func refreshBookPageMapInPlace(_ bookPageMap: RDEPUBBookPageMap) {
|
||||
guard let readerView = context.readerView,
|
||||
let controller = context.controller else { return }
|
||||
|
||||
let currentPage = max(readerView.currentPage, 0)
|
||||
let currentLocation = locationCoordinator.currentVisibleLocation()
|
||||
context.textBook = nil
|
||||
context.bookPageMap = bookPageMap
|
||||
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
|
||||
|
||||
// 仅刷新总页数,不重建页面内容(避免后台元数据解析期间刷新掉用户选区)
|
||||
readerView.reloadPageCountOnly()
|
||||
|
||||
// 用户正在选区时跳过页面跳转,避免打断选区
|
||||
if context.currentSelection == nil, bookPageMap.totalPages > 0 {
|
||||
readerView.transitionToPage(
|
||||
pageNum: min(currentPage, max(bookPageMap.totalPages - 1, 0)),
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
if let currentLocation {
|
||||
locationCoordinator.persist(location: currentLocation)
|
||||
} else if let resolvedLocation = controller.resolvedTextLocation(forPageNumber: currentPage + 1) {
|
||||
locationCoordinator.persist(location: resolvedLocation)
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成分页流程并恢复阅读位置
|
||||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.finishPagination(restoreLocation: restoreLocation)
|
||||
@@ -325,4 +381,174 @@ final class RDEPUBReaderRuntime {
|
||||
) {
|
||||
viewportMonitor.handleViewportChangeIfNeeded(reason: reason, viewportSignature: viewportSignature)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func prepareOnDemandChapter(forAbsolutePageNumber pageNumber: Int) -> Bool {
|
||||
guard let bookPageMap = context.bookPageMap,
|
||||
let publication = context.publication else {
|
||||
return false
|
||||
}
|
||||
let absolutePageIndex = pageNumber - 1
|
||||
guard let spineIndex = bookPageMap.spineIndex(forAbsolutePage: absolutePageIndex) else {
|
||||
return false
|
||||
}
|
||||
|
||||
chapterRuntimeStore.setCurrentChapter(
|
||||
spineIndex: spineIndex,
|
||||
totalSpineCount: publication.spine.count
|
||||
)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"prepareOnDemandChapter page=\(pageNumber) absoluteIndex=\(absolutePageIndex) spine=\(spineIndex)"
|
||||
)
|
||||
|
||||
if chapterRuntimeStore.chapterData(for: spineIndex) == nil {
|
||||
do {
|
||||
_ = try chapterLoader.loadChapterSynchronouslyForMigration(
|
||||
spineIndex: spineIndex,
|
||||
store: chapterRuntimeStore
|
||||
)
|
||||
} catch {
|
||||
RDEPUBBackgroundTrace.log("Runtime", "prepareOnDemandChapter FAILED: spine=\(spineIndex) error=\(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for evictable in chapterRuntimeStore.evictableSpineIndices() {
|
||||
chapterRuntimeStore.evict(spineIndex: evictable)
|
||||
}
|
||||
|
||||
let adjacent = [spineIndex - 1, spineIndex + 1].filter { publication.spine.indices.contains($0) }
|
||||
for adjacentSpineIndex in adjacent where chapterRuntimeStore.chapterData(for: adjacentSpineIndex) == nil {
|
||||
chapterRuntimeStore.addPrefetchTarget(adjacentSpineIndex)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"schedule prefetch currentSpine=\(spineIndex) adjacentSpine=\(adjacentSpineIndex)"
|
||||
)
|
||||
chapterLoader.loadChapter(
|
||||
spineIndex: adjacentSpineIndex,
|
||||
store: chapterRuntimeStore,
|
||||
priority: .prefetch
|
||||
) { _ in }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func extendPartialBookPageMapIfNeeded(currentPageNumber: Int, minimumTrailingPages: Int = 2, batchChapterCount: Int = 3) {
|
||||
guard let publication = context.publication,
|
||||
let currentMap = context.bookPageMap,
|
||||
let readerView = context.readerView else {
|
||||
return
|
||||
}
|
||||
|
||||
let buildableSpineIndices = publication.spine.indices.filter {
|
||||
let item = publication.spine[$0]
|
||||
return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml"))
|
||||
}
|
||||
guard currentMap.totalChapters < buildableSpineIndices.count else {
|
||||
return
|
||||
}
|
||||
guard currentMap.totalPages - currentPageNumber <= minimumTrailingPages else {
|
||||
return
|
||||
}
|
||||
|
||||
let lastKnownSpineIndex = currentMap.entries.last?.spineIndex ?? -1
|
||||
let nextSpineIndices = buildableSpineIndices.filter { $0 > lastKnownSpineIndex }.prefix(batchChapterCount)
|
||||
guard !nextSpineIndices.isEmpty else {
|
||||
return
|
||||
}
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"extendPartialBookPageMap currentPage=\(currentPageNumber) totalPages=\(currentMap.totalPages) appendSpines=\(Array(nextSpineIndices))"
|
||||
)
|
||||
|
||||
var appendedEntries: [RDEPUBBookPageMapEntry] = []
|
||||
for spineIndex in nextSpineIndices {
|
||||
do {
|
||||
let chapter = try chapterLoader.loadChapterSynchronouslyForMigration(
|
||||
spineIndex: spineIndex,
|
||||
store: chapterRuntimeStore
|
||||
)
|
||||
appendedEntries.append(
|
||||
RDEPUBBookPageMapEntry(
|
||||
spineIndex: chapter.spineIndex,
|
||||
href: chapter.href,
|
||||
title: chapter.title,
|
||||
pageCount: chapter.pages.count,
|
||||
absolutePageStart: 0,
|
||||
fragmentOffsets: chapter.chapterOffsetMap.fragmentOffsets
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
RDEPUBBackgroundTrace.log("Runtime", "extendPartialBookPageMap skip spine=\(spineIndex) error=\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
guard !appendedEntries.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let combinedEntries = (currentMap.entries.map {
|
||||
RDEPUBBookPageMapEntry(
|
||||
spineIndex: $0.spineIndex,
|
||||
href: $0.href,
|
||||
title: $0.title,
|
||||
pageCount: $0.pageCount,
|
||||
absolutePageStart: 0,
|
||||
fragmentOffsets: $0.fragmentOffsets
|
||||
)
|
||||
} + appendedEntries).sorted { $0.spineIndex < $1.spineIndex }
|
||||
|
||||
var absolutePageStart = 0
|
||||
let normalizedEntries = combinedEntries.map { entry -> RDEPUBBookPageMapEntry in
|
||||
let normalized = RDEPUBBookPageMapEntry(
|
||||
spineIndex: entry.spineIndex,
|
||||
href: entry.href,
|
||||
title: entry.title,
|
||||
pageCount: entry.pageCount,
|
||||
absolutePageStart: absolutePageStart,
|
||||
fragmentOffsets: entry.fragmentOffsets
|
||||
)
|
||||
absolutePageStart += entry.pageCount
|
||||
return normalized
|
||||
}
|
||||
|
||||
let newMap = RDEPUBBookPageMap(entries: normalizedEntries)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"extendPartialBookPageMap applied chapters=\(newMap.totalChapters) pages=\(newMap.totalPages)"
|
||||
)
|
||||
context.bookPageMap = newMap
|
||||
context.replaceActiveSnapshot(makeSnapshot(from: newMap))
|
||||
readerView.reloadData()
|
||||
readerView.transitionToPage(pageNum: max(currentPageNumber - 1, 0), animated: false)
|
||||
}
|
||||
|
||||
func clearOnDemandPageModeState() {
|
||||
chapterRuntimeStore.invalidateAllForSettingsChange()
|
||||
context.bookPageMap = nil
|
||||
}
|
||||
|
||||
private func makeSnapshot(from bookPageMap: RDEPUBBookPageMap) -> RDEPUBReadingSession.PaginationSnapshot {
|
||||
let pages = bookPageMap.entries.flatMap { entry in
|
||||
(0..<entry.pageCount).map { localPageIndex in
|
||||
EPUBPage(
|
||||
spineIndex: entry.spineIndex,
|
||||
chapterIndex: bookPageMap.chapterIndex(forSpineIndex: entry.spineIndex) ?? 0,
|
||||
pageIndexInChapter: localPageIndex,
|
||||
totalPagesInChapter: entry.pageCount,
|
||||
chapterTitle: entry.title,
|
||||
fixedSpread: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
let chapters = bookPageMap.entries.map { entry in
|
||||
EPUBChapterInfo(
|
||||
spineIndex: entry.spineIndex,
|
||||
title: entry.title,
|
||||
pageCount: entry.pageCount
|
||||
)
|
||||
}
|
||||
return (pages, chapters)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user