feat: complete large-book pagination runtime and UI coverage

This commit is contained in:
shenlei
2026-06-03 17:47:16 +08:00
parent 584976ac5f
commit feb05eaf87
53 changed files with 6307 additions and 5126 deletions
@@ -51,6 +51,8 @@ public final class RDURLReaderController: UIViewController {
private var isRetryingPendingDemoPage = false
private let maxPendingDemoPageAttempts = 24
private let pendingDemoPageRetryDelay: TimeInterval = 0.25
private var demoStateTimer: Timer?
private var lastEmittedDemoState = ""
///
/// - Parameters:
@@ -81,7 +83,17 @@ public final class RDURLReaderController: UIViewController {
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
emitDemoState()
startDemoStateTimerIfNeeded()
refreshDemoState()
}
public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopDemoStateTimer()
}
deinit {
stopDemoStateTimer()
}
/// Demo
@@ -155,8 +167,8 @@ public final class RDURLReaderController: UIViewController {
edgeInsets: epubConfiguration.reflowableContentInsets,
numberOfColumns: 1,
columnGap: 20,
avoidOrphans: true,
avoidWidows: true,
avoidOrphans: false,
avoidWidows: false,
avoidPageBreakInsideEnabled: true,
hyphenation: true,
imageMaxHeightRatio: 0.85
@@ -205,15 +217,17 @@ public final class RDURLReaderController: UIViewController {
guard let readerController else { return false }
guard pageNumber > 0 else { return false }
let totalPages = readerController.textBook?.pages.count ?? readerController.activePages.count
let knownPages = readerController.readerContext.bookPageMap?.totalPages
?? readerController.textBook?.pages.count
?? readerController.activePages.count
let numPages = readerController.readerView.numberOfPages()
print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): textBook.pages=\(totalPages), readerView.numberOfPages=\(numPages), currentPage=\(readerController.readerView.currentPage)")
guard pageNumber <= totalPages else { return false }
readerController.readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): after transition, currentPage=\(readerController.readerView.currentPage)")
emitDemoState(prefix: "page=\(pageNumber)")
return true
print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): knownPages=\(knownPages), readerView.numberOfPages=\(numPages), currentPage=\(readerController.readerView.currentPage)")
let moved = readerController.go(toPageNumber: pageNumber, animated: animated)
print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): moved=\(moved), after currentPage=\(readerController.readerView.currentPage)")
if moved {
emitDemoState(prefix: "page=\(pageNumber)")
}
return moved
}
private func queuePendingDemoPageNavigation(_ pageNumber: Int, animated: Bool) {
@@ -288,19 +302,87 @@ public final class RDURLReaderController: UIViewController {
])
}
private func startDemoStateTimerIfNeeded() {
guard demoStateTimer == nil else { return }
demoStateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.refreshDemoState(logIfChanged: false)
}
if let demoStateTimer {
RunLoop.main.add(demoStateTimer, forMode: .common)
}
}
private func stopDemoStateTimer() {
demoStateTimer?.invalidate()
demoStateTimer = nil
}
private func emitDemoState(prefix: String? = nil) {
refreshDemoState(logPrefix: prefix, logIfChanged: true)
}
private func refreshDemoState(logPrefix: String? = nil, logIfChanged: Bool = true) {
let page = readerController?.currentPageNumber.map(String.init) ?? "nil"
let display = readerController?.configuration.displayType.demoArgumentValue ?? epubConfiguration.displayType.demoArgumentValue
let toolbar = readerController?.readerView.isShowToolView == true ? "visible" : "hidden"
let highlights = readerController?.highlights.count ?? 0
let selection = readerController?.currentSelection == nil ? 0 : 1
let state = "reader=opened page=\(page) display=\(display) toolbar=\(toolbar) highlights=\(highlights) selection=\(selection)"
let location = readerController?.currentLocation
let href = encodedDemoLocationHref(location?.href)
let progression = location.map { String(format: "%.4f", $0.navigationProgression) } ?? "nil"
let mapSnapshot = demoPaginationSnapshot()
let layoutConfig = readerController?.readerContext.currentTextLayoutConfig(pageSize: currentTextPageSize())
let state = [
"reader=opened",
"page=\(page)",
"display=\(display)",
"toolbar=\(toolbar)",
"highlights=\(highlights)",
"selection=\(selection)",
"href=\(href)",
"progression=\(progression)",
"mode=\(mapSnapshot.mode)",
"pagination=\(mapSnapshot.phase)",
"knownPages=\(mapSnapshot.knownPages)",
"knownChapters=\(mapSnapshot.knownChapters)",
"buildableChapters=\(mapSnapshot.buildableChapters)",
"avoidWidows=\(layoutConfig?.avoidWidows == true ? 1 : 0)",
"avoidOrphans=\(layoutConfig?.avoidOrphans == true ? 1 : 0)"
].joined(separator: " ")
demoStateLabel.text = state
if let prefix {
logDemoState(prefix: prefix)
} else {
if let logPrefix {
logDemoState(prefix: logPrefix)
} else if logIfChanged, state != lastEmittedDemoState {
print("[ReadViewDemo] automation \(state)")
}
lastEmittedDemoState = state
}
private func encodedDemoLocationHref(_ href: String?) -> String {
guard let href, !href.isEmpty else { return "nil" }
return href.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? href.replacingOccurrences(of: " ", with: "%20")
}
private func demoPaginationSnapshot() -> (mode: String, phase: String, knownPages: Int, knownChapters: Int, buildableChapters: Int) {
guard let readerController else {
return ("unavailable", "none", 0, 0, 0)
}
if let bookPageMap = readerController.readerContext.bookPageMap {
let buildableChapters = readerController.publication?.spine.filter {
$0.linear && ($0.mediaType.contains("html") || $0.mediaType.contains("xhtml"))
}.count ?? 0
let phase = buildableChapters > 0 && bookPageMap.totalChapters >= buildableChapters ? "full" : "partial"
return ("bookPageMap", phase, bookPageMap.totalPages, bookPageMap.totalChapters, buildableChapters)
}
if let textBook = readerController.textBook {
return ("textBook", "full", textBook.pages.count, textBook.chapters.count, textBook.chapters.count)
}
let snapshotChapters = readerController.activeChapters.count
let snapshotPages = readerController.activePages.count
return ("snapshot", snapshotPages > 0 ? "full" : "none", snapshotPages, snapshotChapters, snapshotChapters)
}
///