优化 EPUB 翻页与后台分页刷新

This commit is contained in:
shen
2026-07-06 11:08:28 +09:00
committed by shenlei
parent 71f4feb12f
commit 9e91207011
7 changed files with 274 additions and 32 deletions
@@ -21,6 +21,8 @@ final class RDEPUBPresentationRuntime {
private unowned let reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator
private var pendingCommitRetryWorkItem: DispatchWorkItem?
init(
context: RDEPUBReaderContext,
locationCoordinator: RDEPUBReaderLocationCoordinator,
@@ -62,11 +64,22 @@ final class RDEPUBPresentationRuntime {
guard let readerView = context.readerView,
let controller = context.controller else { return }
guard !controller.isRepaginating else { return }
guard !readerView.isPageCurlTransitioning else {
guard !context.pendingPageMapUpdates.isEmpty else {
cancelPendingCommitRetry()
return
}
guard !controller.isRepaginating else {
schedulePendingCommitRetry()
return
}
guard !readerView.isPageCurlTransitioning else {
schedulePendingCommitRetry()
return
}
cancelPendingCommitRetry()
let rankedUpdates = rankedPendingPageMapUpdates()
for (index, update) in rankedUpdates {
if commitPendingPageMapUpdate(
@@ -75,9 +88,16 @@ final class RDEPUBPresentationRuntime {
readerView: readerView,
controller: controller
) {
if !context.pendingPageMapUpdates.isEmpty {
schedulePendingCommitRetry()
}
return
}
}
if !context.pendingPageMapUpdates.isEmpty {
schedulePendingCommitRetry()
}
}
func queueExtendedPartialPageMap(
@@ -139,19 +159,40 @@ final class RDEPUBPresentationRuntime {
controller: RDEPUBReaderController
) {
let currentLocation = locationCoordinator.currentVisibleLocation()
let livePageBeforeApply = readerView.currentPage + 1
context.textBook = nil
applyPageMapToLiveModel(newPageMap)
RDEPUBBackgroundTrace.log(
"Reconciliation",
"applyFullPageMapReplacement livePageBeforeApply=\(livePageBeforeApply) totalPages=\(newPageMap.totalPages) totalChapters=\(newPageMap.totalChapters)"
)
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
)
let resolvedTargetPage = controller.pageNumber(for: currentLocation)
let shouldTrustResolvedLocation = shouldTrustFullReplaceResolvedPage(
resolvedTargetPage,
livePageBeforeApply: livePageBeforeApply
)
RDEPUBBackgroundTrace.log(
"Reconciliation",
"applyFullPageMapReplacement decision livePage=\(livePageBeforeApply) resolvedTargetPage=\(resolvedTargetPage ?? -1) trustResolved=\(shouldTrustResolvedLocation) href=\(currentLocation.href)"
)
if shouldTrustResolvedLocation,
rebindVisibleLocation(currentLocation, readerView: readerView, controller: controller) {
return
}
let fallbackPage = max(livePageBeforeApply, 1)
RDEPUBBackgroundTrace.log(
"Reconciliation",
"applyFullPageMapReplacement preserveLivePage fallbackPage=\(fallbackPage) currentReaderPage=\(readerView.currentPage + 1)"
)
rebindVisiblePage(
to: fallbackPage - 1,
readerView: readerView
)
} else {
readerView.reloadPageCountOnly()
}
@@ -223,17 +264,23 @@ final class RDEPUBPresentationRuntime {
return false
}
case .extendPartial(_, let currentLocation):
case .extendPartial(let capturedPageNumber, let currentLocation):
removePendingPageMapUpdate(at: index)
applyPageMapToLiveModel(update.pageMap)
if let currentLocation,
let livePageNumber = max(readerView.currentPage, 0) + 1
let shouldTrustCapturedLocation = livePageNumber == max(capturedPageNumber, 1)
RDEPUBBackgroundTrace.log(
"Reconciliation",
"extendPartial commit capturedPage=\(capturedPageNumber) livePage=\(livePageNumber) trustCaptured=\(shouldTrustCapturedLocation) totalPages=\(update.pageMap.totalPages) totalChapters=\(update.pageMap.totalChapters)"
)
if shouldTrustCapturedLocation,
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).
// Prefer the live readerView page when the user has moved since the
// extension request was created; otherwise a stale captured location can
// snap pageCurl back to the previous page after the turn completes.
let livePageIndex = max(readerView.currentPage, 0)
rebindVisiblePage(
to: livePageIndex,
@@ -250,6 +297,19 @@ final class RDEPUBPresentationRuntime {
}
private func rebindVisiblePage(to pageIndex: Int, readerView: RDReaderView) {
if pageIndex == readerView.currentPage {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"rebindVisiblePage pageUnchanged currentPage=\(readerView.currentPage + 1) displayType=\(readerView.currentDisplayType)"
)
readerView.reloadPageCountOnly()
return
}
RDEPUBBackgroundTrace.log(
"Reconciliation",
"rebindVisiblePage targetPage=\(pageIndex + 1) currentPage=\(readerView.currentPage + 1) displayType=\(readerView.currentDisplayType) isPageCurlTransitioning=\(readerView.isPageCurlTransitioning)"
)
if readerView.currentDisplayType == .pageCurl {
if readerView.isPageCurlTransitioning {
// Defer the transition until the current page-curl animation completes,
@@ -257,6 +317,10 @@ final class RDEPUBPresentationRuntime {
DispatchQueue.main.async { [weak readerView] in
guard let readerView, !readerView.isPageCurlTransitioning else { return }
let livePageIndex = max(readerView.currentPage, 0)
RDEPUBBackgroundTrace.log(
"Reconciliation",
"rebindVisiblePage deferredTransition livePage=\(livePageIndex + 1)"
)
readerView.transitionToPage(pageNum: livePageIndex, animated: false)
}
} else {
@@ -276,6 +340,10 @@ final class RDEPUBPresentationRuntime {
controller: RDEPUBReaderController
) -> Bool {
guard let targetPageNumber = controller.pageNumber(for: location) else {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"rebindVisibleLocation failedToResolve locationHref=\(location.href) currentPage=\(readerView.currentPage + 1)"
)
return false
}
@@ -284,9 +352,18 @@ final class RDEPUBPresentationRuntime {
forAbsolutePageNumber: targetPageNumber,
allowSynchronousLoad: false
) == false {
RDEPUBBackgroundTrace.log(
"Reconciliation",
"rebindVisibleLocation prepareOnDemandBlocked targetPage=\(targetPageNumber) currentPage=\(readerView.currentPage + 1)"
)
return false
}
RDEPUBBackgroundTrace.log(
"Reconciliation",
"rebindVisibleLocation targetPage=\(targetPageNumber) currentPage=\(readerView.currentPage + 1) href=\(location.href)"
)
rebindVisiblePage(
to: max(targetPageNumber - 1, 0),
readerView: readerView
@@ -353,4 +430,29 @@ final class RDEPUBPresentationRuntime {
&& candidate.pageMap.totalPages >= existing.pageMap.totalPages
)
}
}
private func shouldTrustFullReplaceResolvedPage(
_ resolvedTargetPage: Int?,
livePageBeforeApply: Int
) -> Bool {
guard let resolvedTargetPage else { return false }
return abs(resolvedTargetPage - livePageBeforeApply) <= 1
}
private func schedulePendingCommitRetry() {
guard pendingCommitRetryWorkItem == nil else { return }
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingCommitRetryWorkItem = nil
self.commitPendingPageMapUpdateIfNeeded()
}
pendingCommitRetryWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: workItem)
}
private func cancelPendingCommitRetry() {
pendingCommitRetryWorkItem?.cancel()
pendingCommitRetryWorkItem = nil
}
}