feat: EPUB 阅读器搜索、选中注释、书签 chrome 状态及大量重构优化
- 新增 RDEPUBReaderSearchCoordinator 与 RDEPUBSelectionState 管理搜索和选中状态 - 新增 BookmarkChromeStateTests、NavigationBackwardTests、SelectionAnnotateTests 等 UI 测试 - 新增多个边界测试 epub 样本(损坏结构、空归档、缺失文件、流式外链验证) - 重构阅读器 chrome 状态管理,统一 tool bar 与 search bar 交互 - 优化大书分页缓存策略(RDEPUBChapterSummaryDiskCache、RDEPUBPageCountCache) - 移除废弃的 RDEPUBLocationConverter 和 RDEPUBPageBreakPolicy - 更新 epub-bridge.js 与 JS bridge 通信协议 - 全面更新现有 UI 测试以适配新的 helper 和状态管理
This commit is contained in:
@@ -54,6 +54,9 @@ public final class RDURLReaderController: UIViewController {
|
||||
private var demoStateTimer: Timer?
|
||||
private var lastEmittedDemoState = ""
|
||||
private var pendingSearchKeyword: String?
|
||||
private var externalLinkActivationCount = 0
|
||||
private var lastActivatedExternalURL: URL?
|
||||
private var lastReaderErrorDescription = "none"
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameters:
|
||||
@@ -78,6 +81,7 @@ public final class RDURLReaderController: UIViewController {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
title = bookURL.deletingPathExtension().lastPathComponent
|
||||
RDEPUBResourceURLSchemeHandler.resetDebugMetrics()
|
||||
embedReaderController()
|
||||
installDemoStateLabel()
|
||||
}
|
||||
@@ -370,6 +374,10 @@ public final class RDURLReaderController: UIViewController {
|
||||
let progression = location.map { String(format: "%.4f", $0.navigationProgression) } ?? "nil"
|
||||
let mapSnapshot = demoPaginationSnapshot()
|
||||
let layoutConfig = readerController?.readerContext.currentTextLayoutConfig(pageSize: currentTextPageSize())
|
||||
let resourceMetrics = RDEPUBResourceURLSchemeHandler.debugMetricsSnapshot()
|
||||
let cacheStats = readerController?.readerContext.makeChapterSummaryDiskCache().cacheStatistics
|
||||
?? (fileCount: 0, totalBytes: 0)
|
||||
let inspectable = readerController?.configuration.allowsInspectableWebViews ?? epubConfiguration.allowsInspectableWebViews
|
||||
let state = [
|
||||
"reader=opened",
|
||||
"page=\(page)",
|
||||
@@ -389,7 +397,17 @@ public final class RDURLReaderController: UIViewController {
|
||||
"avoidOrphans=\(layoutConfig?.avoidOrphans == true ? 1 : 0)",
|
||||
"windowSize=\(readerController?.configuration.onDemandChapterWindowSize ?? epubConfiguration.onDemandChapterWindowSize)",
|
||||
"parseMs=\(readerController?.readerContext.lastMetadataParseWallClockMs ?? 0)",
|
||||
"parseConcurrency=\(readerController?.readerContext.lastMetadataParseConcurrency ?? 0)"
|
||||
"parseConcurrency=\(readerController?.readerContext.lastMetadataParseConcurrency ?? 0)",
|
||||
"inspectable=\(inspectable ? 1 : 0)",
|
||||
"streamedResources=\(resourceMetrics.streamedResponses)",
|
||||
"inMemoryResources=\(resourceMetrics.inMemoryResponses)",
|
||||
"resourceFailures=\(resourceMetrics.failures)",
|
||||
"cacheFiles=\(cacheStats.fileCount)",
|
||||
"cacheBytes=\(cacheStats.totalBytes)",
|
||||
"externalLinks=\(externalLinkActivationCount)",
|
||||
"lastExternalURL=\(encodedDemoLocationHref(lastActivatedExternalURL?.absoluteString))",
|
||||
"lastError=\(encodedDemoField(lastReaderErrorDescription))",
|
||||
"searchMatchText=\(encodedDemoField(currentSearchMatchText()))"
|
||||
].joined(separator: " ")
|
||||
demoStateLabel.text = state
|
||||
if let logPrefix {
|
||||
@@ -405,6 +423,25 @@ public final class RDURLReaderController: UIViewController {
|
||||
return href.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? href.replacingOccurrences(of: " ", with: "%20")
|
||||
}
|
||||
|
||||
private func encodedDemoField(_ value: String?) -> String {
|
||||
guard let value, !value.isEmpty else { return "nil" }
|
||||
return value.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? value.replacingOccurrences(of: " ", with: "_")
|
||||
}
|
||||
|
||||
private func currentSearchMatchText() -> String {
|
||||
guard let match = readerController?.searchState?.currentMatch,
|
||||
let rangeLocation = match.rangeLocation,
|
||||
let chapterData = readerController?.textChapterData(forNormalizedHref: match.href) else {
|
||||
return "none"
|
||||
}
|
||||
let nsRange = NSRange(location: rangeLocation, length: match.rangeLength)
|
||||
guard nsRange.location >= 0,
|
||||
nsRange.location + nsRange.length <= chapterData.attributedContent.length else {
|
||||
return "none"
|
||||
}
|
||||
return chapterData.attributedContent.attributedSubstring(from: nsRange).string
|
||||
}
|
||||
|
||||
private func demoPaginationSnapshot() -> (mode: String, phase: String, knownPages: Int, knownChapters: Int, buildableChapters: Int) {
|
||||
guard let readerController else {
|
||||
return ("unavailable", "none", 0, 0, 0)
|
||||
@@ -486,6 +523,17 @@ extension RDURLReaderController: RDEPUBReaderDelegate {
|
||||
public func epubReader(_ reader: UIViewController, didUpdateBookmarks bookmarks: [RDEPUBBookmark]) {
|
||||
emitDemoState(prefix: "bookmarks=\(bookmarks.count)")
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didActivateExternalLink url: URL) {
|
||||
externalLinkActivationCount += 1
|
||||
lastActivatedExternalURL = url
|
||||
emitDemoState(prefix: "externalLinks=\(externalLinkActivationCount)")
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didFailWithError error: Error) {
|
||||
lastReaderErrorDescription = String(describing: error)
|
||||
emitDemoState(prefix: "lastError=\(lastReaderErrorDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// 翻页模式扩展:提供 Demo 命令行参数值
|
||||
|
||||
Reference in New Issue
Block a user