优化 EPUB 翻页与后台分页刷新
This commit is contained in:
parent
71f4feb12f
commit
9e91207011
@ -137,7 +137,7 @@ public final class RDEPUBTextBookCache {
|
||||
let fileURL = cacheDirectory.appendingPathComponent(key)
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||
#if DEBUG
|
||||
print("[Cache] load MISS key=\(key)")
|
||||
// print("[Cache] load MISS key=\(key)")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
@ -148,7 +148,7 @@ public final class RDEPUBTextBookCache {
|
||||
from: data
|
||||
) else {
|
||||
#if DEBUG
|
||||
print("[Cache] load MISS key=\(key) (unarchive returned nil)")
|
||||
// print("[Cache] load MISS key=\(key) (unarchive returned nil)")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
@ -157,12 +157,12 @@ public final class RDEPUBTextBookCache {
|
||||
result[chapter.href] = chapter.toCache()
|
||||
}
|
||||
#if DEBUG
|
||||
print("[Cache] load HIT key=\(key) chapters=\(result.count)")
|
||||
// print("[Cache] load HIT key=\(key) chapters=\(result.count)")
|
||||
#endif
|
||||
return result
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("[Cache] load MISS key=\(key) error=\(error)")
|
||||
// print("[Cache] load MISS key=\(key) error=\(error)")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -56,6 +56,8 @@ final class RDEPUBMetadataParseWorker {
|
||||
|
||||
private let catalog: [(key: RDEPUBChapterCacheKey, spineIndex: Int, href: String, title: String)]
|
||||
|
||||
private let progressLogStride = 4
|
||||
|
||||
init(
|
||||
context: RDEPUBReaderContext,
|
||||
cancellationController: RDEPUBMetadataParseCancellationController,
|
||||
@ -116,19 +118,38 @@ final class RDEPUBMetadataParseWorker {
|
||||
func start(token: UUID, restoreLocation: RDEPUBLocation?) {
|
||||
let cancellationController = self.cancellationController
|
||||
|
||||
DispatchQueue.global(qos: .utility).async { [weak self] in
|
||||
guard let self else { return }
|
||||
DispatchQueue.global(qos: .utility).async { [self] in
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"workerDispatched token=\(token.uuidString)"
|
||||
)
|
||||
let context = self.context
|
||||
guard let context,
|
||||
context.controller != nil,
|
||||
!cancellationController.isCancelled,
|
||||
context.paginationToken == token else { return }
|
||||
context.paginationToken == token else {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"workerAbortedBeforeStart reason=contextUnavailableOrTokenMismatch"
|
||||
)
|
||||
return
|
||||
}
|
||||
defer { context.runtime?.paginationCoordinator.finishMetadataParseCancellationController(cancellationController) }
|
||||
guard context.controller != nil,
|
||||
!cancellationController.isCancelled,
|
||||
context.paginationToken == token else { return }
|
||||
context.paginationToken == token else {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"workerAbortedAfterStart reason=contextUnavailableOrTokenMismatch"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if let restoredPageMap = self.restoreBookPageMapIfPossible() {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"restoreBookPageMapIfPossible hit totalChapters=\(restoredPageMap.totalChapters) totalPages=\(restoredPageMap.totalPages)"
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
guard context.paginationToken == token,
|
||||
context.controller != nil,
|
||||
@ -175,13 +196,26 @@ final class RDEPUBMetadataParseWorker {
|
||||
|
||||
let uncachedSpineIndices = prioritizedSpineIndices
|
||||
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"waitingForInteractionCooldown elapsedSinceNavigation=\(String(format: "%.2f", context.secondsSinceLastUserNavigation())) uncached=\(uncachedSpineIndices.count)"
|
||||
)
|
||||
self.waitForReadingInteractionToSettle(cancellationController: cancellationController)
|
||||
guard !cancellationController.isCancelled,
|
||||
context.controller != nil,
|
||||
context.paginationToken == token else {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"workerAbortedDuringCooldown"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"start totalBuildable=\(self.allBuildableIndices.count) cached=\(cachedSpineIndices.count) uncached=\(uncachedSpineIndices.count) concurrency=\(self.workerCount) refreshInterval=\(self.pageMapRefreshInterval)"
|
||||
)
|
||||
|
||||
let wallClockStart = CFAbsoluteTimeGetCurrent()
|
||||
var totalRenderMs: Double = 0
|
||||
var totalWriteMs: Double = 0
|
||||
@ -283,6 +317,10 @@ final class RDEPUBMetadataParseWorker {
|
||||
resultLock.lock()
|
||||
parseState.summariesBySpineIndex[spineIndex] = renderResult
|
||||
parseState.totalResolvedCount += 1
|
||||
let resolvedCount = parseState.totalResolvedCount
|
||||
let shouldLogProgress = resolvedCount == self.allBuildableIndices.count
|
||||
|| resolvedCount == cachedSpineIndices.count + 1
|
||||
|| resolvedCount % self.progressLogStride == 0
|
||||
if parseState.totalResolvedCount - parseState.lastAppliedCount >= refreshInterval
|
||||
|| parseState.totalResolvedCount == self.allBuildableIndices.count {
|
||||
parseState.lastAppliedCount = parseState.totalResolvedCount
|
||||
@ -290,6 +328,17 @@ final class RDEPUBMetadataParseWorker {
|
||||
}
|
||||
resultLock.unlock()
|
||||
|
||||
if shouldLogProgress {
|
||||
let progressPercent = Self.progressPercent(
|
||||
resolved: resolvedCount,
|
||||
total: self.allBuildableIndices.count
|
||||
)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"chapterReady spine=\(spineIndex) resolved=\(resolvedCount)/\(self.allBuildableIndices.count) progress=\(progressPercent)% pageCount=\(renderResult.pageCount)"
|
||||
)
|
||||
}
|
||||
|
||||
if let snapshot {
|
||||
let mergeStart = CFAbsoluteTimeGetCurrent()
|
||||
let partialMap = self.buildPageMap(summaries: snapshot)
|
||||
@ -297,6 +346,10 @@ final class RDEPUBMetadataParseWorker {
|
||||
timingLock.lock()
|
||||
totalMergeMs += mergeElapsed
|
||||
timingLock.unlock()
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"partialMap resolvedChapters=\(snapshot.count) totalPages=\(partialMap.totalPages) mergeMs=\(Int(mergeElapsed))"
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
guard context.paginationToken == token,
|
||||
context.controller != nil,
|
||||
@ -314,6 +367,10 @@ final class RDEPUBMetadataParseWorker {
|
||||
timingLock.lock()
|
||||
failedChapters += 1
|
||||
timingLock.unlock()
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"chapterFailed spine=\(spineIndex) retryScheduled=true error=\(String(describing: error))"
|
||||
)
|
||||
|
||||
self.scheduleRetry(
|
||||
spineIndex: spineIndex,
|
||||
@ -354,6 +411,10 @@ final class RDEPUBMetadataParseWorker {
|
||||
let finalMergeStart = CFAbsoluteTimeGetCurrent()
|
||||
let pageMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
|
||||
let finalMergeMs = Int((CFAbsoluteTimeGetCurrent() - finalMergeStart) * 1000)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"finish resolved=\(parseState.summariesBySpineIndex.count)/\(self.allBuildableIndices.count) totalPages=\(pageMap.totalPages) elapsedMs=\(wallClockMs) renderMs=\(renderTotal) writeMs=\(writeTotal) mergeMs=\(mergeTotal + finalMergeMs) failed=\(failed)"
|
||||
)
|
||||
|
||||
if let coverageStore = context.runtime?.backgroundCoverageStore {
|
||||
let resolvedSpineIndices = Set(parseState.summariesBySpineIndex.keys)
|
||||
@ -407,10 +468,18 @@ final class RDEPUBMetadataParseWorker {
|
||||
cancellationController: RDEPUBMetadataParseCancellationController
|
||||
) {
|
||||
guard retryCount < Self.maxRetryCount else {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"retryAborted spine=\(spineIndex) retryCount=\(retryCount)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let delay = Self.retryDelays[min(retryCount, Self.retryDelays.count - 1)]
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"retryScheduled spine=\(spineIndex) retryCount=\(retryCount + 1) delayMs=\(Int(delay * 1000))"
|
||||
)
|
||||
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self, let context = self.context else { return }
|
||||
@ -474,6 +543,10 @@ final class RDEPUBMetadataParseWorker {
|
||||
|
||||
if shouldRefresh {
|
||||
let partialMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Metadata",
|
||||
"retryPartialMap resolvedChapters=\(parseState.summariesBySpineIndex.count) totalPages=\(partialMap.totalPages)"
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
guard context.paginationToken == self.token,
|
||||
context.controller != nil,
|
||||
@ -495,6 +568,11 @@ final class RDEPUBMetadataParseWorker {
|
||||
}
|
||||
}
|
||||
|
||||
private static func progressPercent(resolved: Int, total: Int) -> Int {
|
||||
guard total > 0 else { return 0 }
|
||||
return Int((Double(resolved) / Double(total) * 100.0).rounded())
|
||||
}
|
||||
|
||||
private func restoreBookPageMapIfPossible() -> RDEPUBBookPageMap? {
|
||||
guard let summaryDiskCache else { return nil }
|
||||
guard summaryDiskCache.isCacheComplete(keys: catalog.map(\.key)) else {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,27 @@
|
||||
import UIKit
|
||||
|
||||
enum RDEPUBTextPageLayoutMetrics {
|
||||
|
||||
static let pageNumberTrailingPadding: CGFloat = 4
|
||||
|
||||
static let pageNumberFooterPadding: CGFloat = 8
|
||||
|
||||
static let pageNumberReservedHeight: CGFloat = ceil(UIFont.systemFont(ofSize: 13).lineHeight) + pageNumberFooterPadding
|
||||
|
||||
static func contentInsets(
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
safeAreaInsets: UIEdgeInsets
|
||||
) -> UIEdgeInsets {
|
||||
let configInsets = configuration.reflowableContentInsets
|
||||
return UIEdgeInsets(
|
||||
top: max(configInsets.top, safeAreaInsets.top),
|
||||
left: max(configInsets.left, safeAreaInsets.left),
|
||||
bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight,
|
||||
right: max(configInsets.right, safeAreaInsets.right)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class RDEPUBReaderEnvironment {
|
||||
|
||||
weak var controller: RDEPUBReaderController?
|
||||
@ -46,11 +68,14 @@ final class RDEPUBReaderEnvironment {
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
pageSize: CGSize
|
||||
) -> RDEPUBTextLayoutConfig {
|
||||
let layoutContext = currentLayoutContext(configuration: configuration)
|
||||
let safeAreaInsets = controller?.view.safeAreaInsets ?? .zero
|
||||
return RDEPUBTextLayoutConfig(
|
||||
frameWidth: max(pageSize.width, 1),
|
||||
frameHeight: max(pageSize.height, 1),
|
||||
edgeInsets: layoutContext.safeReflowableContentInsets,
|
||||
edgeInsets: RDEPUBTextPageLayoutMetrics.contentInsets(
|
||||
configuration: configuration,
|
||||
safeAreaInsets: safeAreaInsets
|
||||
),
|
||||
numberOfColumns: configuration.numberOfColumns,
|
||||
columnGap: configuration.columnGap,
|
||||
avoidOrphans: false,
|
||||
|
||||
@ -191,12 +191,21 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
runtime: runtime,
|
||||
layoutSnapshot: layoutSnapshot
|
||||
)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Pagination",
|
||||
"anchorChapterReady spine=\(runtimeChapter.spineIndex) pages=\(runtimeChapter.pages.count)"
|
||||
)
|
||||
let initialChapters = self.loadInitialInteractiveRuntimeChapters(
|
||||
anchorChapter: runtimeChapter,
|
||||
publication: publication,
|
||||
runtime: runtime,
|
||||
layoutSnapshot: layoutSnapshot
|
||||
)
|
||||
let initialPageCount = initialChapters.reduce(0) { $0 + $1.pages.count }
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Pagination",
|
||||
"initialInteractiveChapters ready count=\(initialChapters.count) pages=\(initialPageCount) spines=\(initialChapters.map(\.spineIndex))"
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard context.paginationToken == token,
|
||||
@ -220,9 +229,17 @@ final class RDEPUBReaderPaginationCoordinator {
|
||||
parser: parser,
|
||||
publication: publication
|
||||
)
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Pagination",
|
||||
"startingMetadataWorker token=\(token.uuidString) anchorSpine=\(runtimeChapter.spineIndex) partialPages=\(partialMap.totalPages) partialChapters=\(partialMap.totalChapters)"
|
||||
)
|
||||
worker.start(token: token, restoreLocation: restoreLocation)
|
||||
}
|
||||
} catch {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Pagination",
|
||||
"initialPaginationFailed error=\(String(describing: error)) prioritizedCandidates=\(prioritizedCandidates)"
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
guard context.paginationToken == token,
|
||||
context.controller != nil else { return }
|
||||
|
||||
@ -78,8 +78,12 @@ final class RDReaderPreloadController {
|
||||
environment: Environment,
|
||||
contentViewProvider: (Int, UIView?) -> UIView?
|
||||
) -> UIView {
|
||||
let reusableView = detachedReusablePageView(for: pageNum)
|
||||
let view = contentViewProvider(pageNum, reusableView) ?? reusableView ?? UIView()
|
||||
let view: UIView
|
||||
if let reusableView = detachedReusablePageView(for: pageNum) {
|
||||
view = reusableView
|
||||
} else {
|
||||
view = contentViewProvider(pageNum, nil) ?? UIView()
|
||||
}
|
||||
if shouldCache(view: view, for: pageNum, environment: environment) {
|
||||
pageCurlCachedViews[pageNum] = view
|
||||
} else {
|
||||
@ -111,8 +115,14 @@ final class RDReaderPreloadController {
|
||||
preloadHostView.frame = parentView.bounds
|
||||
|
||||
for targetPage in targets {
|
||||
let existing = detachedReusablePageView(for: targetPage)
|
||||
let contentView = contentViewProvider(targetPage, existing) ?? existing ?? UIView()
|
||||
let contentView: UIView
|
||||
if let existing = preloadedPageViews[targetPage], existing.superview === preloadHostView {
|
||||
contentView = existing
|
||||
} else if let cached = pageCurlCachedViews.removeValue(forKey: targetPage), cached.superview == nil {
|
||||
contentView = cached
|
||||
} else {
|
||||
contentView = contentViewProvider(targetPage, nil) ?? UIView()
|
||||
}
|
||||
let shouldCacheContentView = shouldCache(view: contentView, for: targetPage, environment: environment)
|
||||
if shouldCacheContentView {
|
||||
preloadedPageViews[targetPage] = contentView
|
||||
|
||||
@ -763,6 +763,14 @@ public class RDReaderView: UIView {
|
||||
guard let safePageNum = clampedPageNumber(pageNum) else { return }
|
||||
switch currentDisplayType {
|
||||
case .pageCurl:
|
||||
if !animated, safePageNum == currentPage, !detectPageViewControllerFault(pageViewController) {
|
||||
RDReaderTapDebug.log(
|
||||
"ReaderView.transitionToPage",
|
||||
"skip same page page=\(safePageNum) animated=\(animated)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let request = PageTransitionRequest(pageNum: safePageNum, animated: animated)
|
||||
if shouldQueuePageTransition(request) {
|
||||
return
|
||||
@ -823,6 +831,7 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
|
||||
public func reloadData() {
|
||||
invalidatePageCaches()
|
||||
switchReaderDisplayType(currentDisplayType)
|
||||
topToolView = resolvedTopChromeView()
|
||||
bottomToolView = resolvedBottomChromeView()
|
||||
@ -830,8 +839,9 @@ public class RDReaderView: UIView {
|
||||
|
||||
public func reloadPageCountOnly() {
|
||||
if currentDisplayType == .pageCurl {
|
||||
if !isPageCurlTransitioning, currentPage >= 0, numberOfPages() > 0 {
|
||||
transitionToPage(pageNum: currentPage, animated: false)
|
||||
let totalPages = numberOfPages()
|
||||
if !isPageCurlTransitioning, currentPage >= totalPages, totalPages > 0 {
|
||||
transitionToPage(pageNum: totalPages - 1, animated: false)
|
||||
}
|
||||
} else {
|
||||
collectionView.reloadData()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user