ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBPresentationRuntime.swift
shenlei 15b15d0e11 Remove all RDEPUBBackgroundTrace.log and bare print diagnostic calls
Keep only the evaluateFullPageMapTakeover: fullReplace diagnostic log
in RDEPUBPageMapReconciliationCoordinator.swift for future debugging.

Simplified RDEPUBBackgroundTrace to just the log method (removed measure
and debug gating). Removed all [EPUB][...], [ReadViewDemo], and
[EPUB][Pagination] print statements across the project.

Also includes earlier bug fixes:
- Fix stale currentPageNumber in extendPartial commit
- Fix pageCurl rebindVisiblePage during transition
- Fix keepCurrentWindow not removing pending update from queue
- Fix right-aligned text (巫鸿 bug) via avoidPageBreakInside,
  tail merger, and continuation paragraph normalization
- Add text-indent reset for aligned blocks in CSS compatibility layer

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 18:25:22 +08:00

380 lines
14 KiB
Swift

import UIKit
enum RDEPUBPendingPageMapUpdateKind {
case reconcileFullMap
case extendPartial(currentPageNumber: Int, currentLocation: RDEPUBLocation?)
case appendForward
}
struct RDEPUBPendingPageMapUpdate {
let pageMap: RDEPUBBookPageMap
let source: RDEPUBPaginationStateSource
let kind: RDEPUBPendingPageMapUpdateKind
}
final class RDEPUBPresentationRuntime {
private unowned let context: RDEPUBReaderContext
private unowned let locationCoordinator: RDEPUBReaderLocationCoordinator
private unowned let jumpSessionManager: RDEPUBJumpSessionManager
private unowned let reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator
let navigationStateMachine = RDEPUBNavigationStateMachine()
private(set) var paginationState = RDEPUBPaginationState()
init(
context: RDEPUBReaderContext,
locationCoordinator: RDEPUBReaderLocationCoordinator,
jumpSessionManager: RDEPUBJumpSessionManager,
reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator
) {
self.context = context
self.locationCoordinator = locationCoordinator
self.jumpSessionManager = jumpSessionManager
self.reconciliationCoordinator = reconciliationCoordinator
}
func applyBookPageMap(
_ bookPageMap: RDEPUBBookPageMap,
restoreLocation: RDEPUBLocation?,
finishPagination: (RDEPUBLocation?) -> Void
) {
navigationStateMachine.transition(to: .presentingWindow)
context.textBook = nil
context.bookPageMap = bookPageMap
context.pendingPageMapUpdates.removeAll()
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
paginationState.activePageMap = bookPageMap
paginationState.pendingPageMapUpdates.removeAll()
paginationState.source = .initialPartial
finishPagination(restoreLocation)
}
func refreshBookPageMapInPlace(_ bookPageMap: RDEPUBBookPageMap) {
navigationStateMachine.transition(to: .reconcilingFullMap)
enqueuePendingPageMapUpdate(
RDEPUBPendingPageMapUpdate(
pageMap: bookPageMap,
source: .pendingFullMap,
kind: .reconcileFullMap
)
)
}
func commitPendingPageMapUpdateIfNeeded() {
guard let readerView = context.readerView,
let controller = context.controller else { return }
guard !controller.isRepaginating else { return }
guard !readerView.isPageCurlTransitioning else {
return
}
let rankedUpdates = rankedPendingPageMapUpdates()
for (index, update) in rankedUpdates {
if commitPendingPageMapUpdate(
update,
at: index,
readerView: readerView,
controller: controller
) {
return
}
}
}
func queueExtendedPartialPageMap(
_ bookPageMap: RDEPUBBookPageMap,
currentPageNumber: Int,
currentLocation: RDEPUBLocation?
) {
enqueuePendingPageMapUpdate(
RDEPUBPendingPageMapUpdate(
pageMap: bookPageMap,
source: .asyncExtension,
kind: .extendPartial(
currentPageNumber: currentPageNumber,
currentLocation: currentLocation
)
)
)
}
func applySettingsPreviewPageMap(_ bookPageMap: RDEPUBBookPageMap) {
navigationStateMachine.transition(to: .presentingWindow)
context.bookPageMap = bookPageMap
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
paginationState.activePageMap = bookPageMap
paginationState.source = .settingsPreview
}
func queueForwardAppendedPageMap(_ bookPageMap: RDEPUBBookPageMap) {
enqueuePendingPageMapUpdate(
RDEPUBPendingPageMapUpdate(
pageMap: bookPageMap,
source: .asyncExtension,
kind: .appendForward
)
)
}
func clear() {
paginationState = RDEPUBPaginationState()
navigationStateMachine.transition(to: .idle)
}
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)
}
private func applyFullPageMapReplacement(
_ newPageMap: RDEPUBBookPageMap,
readerView: RDReaderView,
controller: RDEPUBReaderController
) {
let currentLocation = locationCoordinator.currentVisibleLocation()
context.textBook = nil
applyPageMapToLiveModel(newPageMap, source: .fullReplacement)
if let currentLocation {
if rebindVisibleLocation(currentLocation, readerView: readerView, controller: controller) == false {
let newPageNumber = controller.pageNumber(for: currentLocation) ?? (readerView.currentPage + 1)
let newPage = max(0, newPageNumber - 1)
rebindVisiblePage(
to: newPage,
readerView: readerView
)
}
} else {
readerView.reloadPageCountOnly()
}
if let currentLocation,
context.normalizedSpineIndex(for: currentLocation) != nil,
let activeSession = jumpSessionManager.activeSession {
let candidateIndices = Set(newPageMap.entries.map { $0.spineIndex })
let protectedIndices = activeSession.protectedSpineIndices
if protectedIndices.isSubset(of: candidateIndices) {
jumpSessionManager.endSession(.coverageComplete)
}
}
}
private func enqueuePendingPageMapUpdate(_ update: RDEPUBPendingPageMapUpdate) {
var updates = context.pendingPageMapUpdates
if let existingIndex = updates.firstIndex(where: {
pendingPageMapUpdateKindMatches($0.kind, update.kind)
}) {
let existing = updates[existingIndex]
if shouldReplacePendingPageMapUpdate(existing, with: update) {
updates[existingIndex] = update
}
} else {
updates.append(update)
}
context.pendingPageMapUpdates = updates
paginationState.pendingPageMapUpdates = updates
commitPendingPageMapUpdateIfNeeded()
}
private func rankedPendingPageMapUpdates() -> [(Int, RDEPUBPendingPageMapUpdate)] {
context.pendingPageMapUpdates.enumerated().sorted { lhs, rhs in
pendingPriority(for: lhs.element.kind) > pendingPriority(for: rhs.element.kind)
}
}
private func commitPendingPageMapUpdate(
_ update: RDEPUBPendingPageMapUpdate,
at index: Int,
readerView: RDReaderView,
controller: RDEPUBReaderController
) -> Bool {
switch update.kind {
case .reconcileFullMap:
navigationStateMachine.transition(to: .reconcilingFullMap)
let decision = reconciliationCoordinator.evaluateTakeover(
candidatePageMap: update.pageMap,
candidateSegment: nil,
currentWindow: context.bookPageMap,
jumpSession: jumpSessionManager.activeSession
)
switch decision {
case .keepCurrentWindow:
removePendingPageMapUpdate(at: index)
return false
case .fullReplace(let newPageMap):
removePendingPageMapUpdate(at: index)
applyFullPageMapReplacement(newPageMap, readerView: readerView, controller: controller)
return true
case .expandWindow, .segmentReplace:
removePendingPageMapUpdate(at: index)
return false
}
case .extendPartial(let currentPageNumber, let currentLocation):
removePendingPageMapUpdate(at: index)
applyPageMapToLiveModel(update.pageMap, source: update.source)
if let currentLocation,
rebindVisibleLocation(currentLocation, readerView: readerView, controller: controller) {
return true
}
// Fallback: prefer the live readerView.currentPage over the stale captured
// currentPageNumber, which may be outdated by the time this commit runs
// (especially in pageCurl mode where the user may have turned several pages
// since the extension was initiated).
let livePageIndex = max(readerView.currentPage, 0)
rebindVisiblePage(
to: livePageIndex,
readerView: readerView
)
return true
case .appendForward:
removePendingPageMapUpdate(at: index)
applyPageMapToLiveModel(update.pageMap, source: update.source)
readerView.reloadPageCountOnly()
return true
}
}
private func rebindVisiblePage(to pageIndex: Int, readerView: RDReaderView) {
if readerView.currentDisplayType == .pageCurl {
if readerView.isPageCurlTransitioning {
// Defer the transition until the current page-curl animation completes,
// and re-read the live page at that time to avoid jumping to a stale position.
DispatchQueue.main.async { [weak readerView] in
guard let readerView, !readerView.isPageCurlTransitioning else { return }
let livePageIndex = max(readerView.currentPage, 0)
readerView.transitionToPage(pageNum: livePageIndex, animated: false)
}
} else {
readerView.transitionToPage(pageNum: pageIndex, animated: false)
}
} else {
readerView.reloadPageCountOnly()
if pageIndex != readerView.currentPage {
readerView.transitionToPage(pageNum: pageIndex, animated: false)
}
}
}
private func rebindVisibleLocation(
_ location: RDEPUBLocation,
readerView: RDReaderView,
controller: RDEPUBReaderController
) -> Bool {
guard let targetPageNumber = controller.pageNumber(for: location) else {
return false
}
if context.bookPageMap != nil,
context.runtime?.prepareOnDemandChapter(
forAbsolutePageNumber: targetPageNumber,
allowSynchronousLoad: true
) == false {
return false
}
rebindVisiblePage(
to: max(targetPageNumber - 1, 0),
readerView: readerView
)
return true
}
private func applyPageMapToLiveModel(
_ pageMap: RDEPUBBookPageMap,
source: RDEPUBPaginationStateSource
) {
navigationStateMachine.transition(to: .presentingWindow)
context.bookPageMap = pageMap
context.replaceActiveSnapshot(makeSnapshot(from: pageMap))
discardSupersededPendingPageMapUpdates(afterApplying: pageMap)
paginationState.activePageMap = pageMap
paginationState.source = source
}
private func removePendingPageMapUpdate(at index: Int) {
var updates = context.pendingPageMapUpdates
guard updates.indices.contains(index) else { return }
updates.remove(at: index)
context.pendingPageMapUpdates = updates
paginationState.pendingPageMapUpdates = updates
}
private func discardSupersededPendingPageMapUpdates(afterApplying liveMap: RDEPUBBookPageMap) {
let updates = context.pendingPageMapUpdates.filter { update in
update.pageMap.totalChapters > liveMap.totalChapters
|| (
update.pageMap.totalChapters == liveMap.totalChapters
&& update.pageMap.totalPages > liveMap.totalPages
)
}
context.pendingPageMapUpdates = updates
paginationState.pendingPageMapUpdates = updates
}
private func pendingPriority(for kind: RDEPUBPendingPageMapUpdateKind) -> Int {
switch kind {
case .extendPartial:
return 3
case .appendForward:
return 2
case .reconcileFullMap:
return 1
}
}
private func pendingPageMapUpdateKindMatches(
_ lhs: RDEPUBPendingPageMapUpdateKind,
_ rhs: RDEPUBPendingPageMapUpdateKind
) -> Bool {
switch (lhs, rhs) {
case (.reconcileFullMap, .reconcileFullMap),
(.appendForward, .appendForward),
(.extendPartial, .extendPartial):
return true
default:
return false
}
}
private func shouldReplacePendingPageMapUpdate(
_ existing: RDEPUBPendingPageMapUpdate,
with candidate: RDEPUBPendingPageMapUpdate
) -> Bool {
candidate.pageMap.totalChapters > existing.pageMap.totalChapters
|| (
candidate.pageMap.totalChapters == existing.pageMap.totalChapters
&& candidate.pageMap.totalPages >= existing.pageMap.totalPages
)
}
}