169 lines
7.2 KiB
Swift
169 lines
7.2 KiB
Swift
// RDEPUBWebView+Reflowable.swift
|
||
// RDEPUBWebView 的重排模式渲染扩展
|
||
// 处理重排(Reflowable)模式的加载流程:构建渲染请求、
|
||
// 生成加载签名(去重)、通过 JS 桥接应用分页样式和高亮、
|
||
// 处理重复请求跳过和已有文档复用等优化逻辑。
|
||
|
||
import UIKit
|
||
import WebKit
|
||
|
||
extension RDEPUBWebView {
|
||
/// 便捷方法:加载重排模式的指定页面
|
||
public func loadPage(
|
||
parser: RDEPUBParser,
|
||
spineIndex: Int,
|
||
pageIndex: Int,
|
||
totalPagesInChapter: Int,
|
||
viewportSize: CGSize,
|
||
padding: UIEdgeInsets,
|
||
fontSize: CGFloat,
|
||
lineHeightMultiple: CGFloat,
|
||
themeBackgroundColor: String?,
|
||
themeTextColor: String?,
|
||
targetLocation: RDEPUBLocation?,
|
||
highlights: [RDEPUBHighlight]
|
||
) {
|
||
let href = parser.spine.indices.contains(spineIndex) ? parser.spine[spineIndex].href : ""
|
||
let request = RDEPUBRenderRequest.reflowable(
|
||
RDEPUBReflowableRenderRequest(
|
||
spineIndex: spineIndex,
|
||
href: href,
|
||
pageIndex: pageIndex,
|
||
totalPagesInChapter: totalPagesInChapter,
|
||
presentation: RDEPUBPresentationStyle(
|
||
viewportSize: viewportSize,
|
||
contentInsets: padding,
|
||
fontSize: fontSize,
|
||
lineHeightMultiple: lineHeightMultiple,
|
||
themeBackgroundColor: themeBackgroundColor,
|
||
themeTextColor: themeTextColor
|
||
),
|
||
targetLocation: targetLocation,
|
||
highlights: highlights,
|
||
searchPresentation: nil
|
||
)
|
||
)
|
||
load(publication: parser.makePublication(), request: request)
|
||
}
|
||
|
||
/// 处理重排模式加载:设置状态、去重检查、加载资源或直接应用样式
|
||
func handleReflowableLoad(
|
||
publication: RDEPUBPublication,
|
||
request: RDEPUBReflowableRenderRequest,
|
||
loadSignature: String,
|
||
in webView: WKWebView
|
||
) {
|
||
guard publication.spine.indices.contains(request.spineIndex),
|
||
let requestURL = publication.resourceResolver.resourceURL(forRelativePath: request.href) else {
|
||
return
|
||
}
|
||
|
||
currentPageIndex = request.pageIndex
|
||
currentTotalPagesInChapter = max(1, request.totalPagesInChapter)
|
||
viewportSize = request.presentation.viewportSize
|
||
currentPadding = request.presentation.contentInsets
|
||
currentFontSize = request.presentation.fontSize
|
||
currentLineHeightMultiple = request.presentation.lineHeightMultiple
|
||
currentThemeBackgroundColor = request.presentation.themeBackgroundColor
|
||
currentThemeTextColor = request.presentation.themeTextColor
|
||
targetLocation = request.targetLocation
|
||
pendingHighlights = request.highlights
|
||
fixedSpread = nil
|
||
isFixedLayout = false
|
||
pendingProgressionRequest = true
|
||
|
||
if currentLoadSignature == loadSignature {
|
||
if didRenderCurrentRequest || webView.isLoading {
|
||
let reason = didRenderCurrentRequest ? "already-rendered" : "still-loading"
|
||
RDEPUBWebViewDebug.log(debugScope, message: "skip duplicate request href=\(request.href) page=\(request.pageIndex) reason=\(reason)")
|
||
return
|
||
}
|
||
|
||
if webView.url == requestURL {
|
||
RDEPUBWebViewDebug.log(debugScope, message: "reuse existing document href=\(request.href) page=\(request.pageIndex)")
|
||
applyPresentation()
|
||
return
|
||
}
|
||
}
|
||
|
||
currentLoadSignature = loadSignature
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "load-request", url: requestURL)
|
||
webView.load(URLRequest(url: requestURL))
|
||
}
|
||
|
||
/// 生成重排模式加载签名(包含所有渲染参数的哈希,用于去重)
|
||
func reflowableLoadSignature(publicationKey: String, request: RDEPUBReflowableRenderRequest) -> String {
|
||
let targetSignature = [
|
||
request.targetLocation?.href ?? "",
|
||
String(request.targetLocation?.progression ?? 0),
|
||
String(request.targetLocation?.lastProgression ?? 0),
|
||
request.targetLocation?.fragment ?? ""
|
||
].joined(separator: "|")
|
||
let highlightSignature = request.highlights
|
||
.map { [$0.id, $0.rangeInfo ?? "", $0.color, $0.style.rawValue].joined(separator: "|") }
|
||
.joined(separator: ",")
|
||
let searchSignature = [
|
||
request.searchPresentation?.keyword ?? "",
|
||
request.searchPresentation?.resources.map {
|
||
[
|
||
$0.href,
|
||
String($0.matchCount),
|
||
String($0.activeLocalMatchIndex ?? -1)
|
||
].joined(separator: "|")
|
||
}.joined(separator: ",") ?? ""
|
||
].joined(separator: "#")
|
||
let presentation = request.presentation
|
||
return [
|
||
publicationKey,
|
||
"reflowable",
|
||
String(request.spineIndex),
|
||
request.href,
|
||
String(request.pageIndex),
|
||
String(request.totalPagesInChapter),
|
||
String(format: "%.2f", presentation.viewportSize.width),
|
||
String(format: "%.2f", presentation.viewportSize.height),
|
||
String(format: "%.2f", presentation.contentInsets.top),
|
||
String(format: "%.2f", presentation.contentInsets.left),
|
||
String(format: "%.2f", presentation.contentInsets.bottom),
|
||
String(format: "%.2f", presentation.contentInsets.right),
|
||
String(format: "%.2f", presentation.fontSize),
|
||
String(format: "%.2f", presentation.lineHeightMultiple),
|
||
String(presentation.numberOfColumns),
|
||
String(format: "%.2f", presentation.columnGap),
|
||
presentation.themeBackgroundColor ?? "",
|
||
presentation.themeTextColor ?? "",
|
||
targetSignature,
|
||
highlightSignature,
|
||
searchSignature
|
||
].joined(separator: "#")
|
||
}
|
||
|
||
/// 应用分页展示样式:通过 JS 桥接注入 CSS、设置分页参数和高亮,然后触发渲染完成
|
||
func applyPresentation() {
|
||
guard let webView, let currentRenderRequest else { return }
|
||
guard case .reflowable(let request) = currentRenderRequest else {
|
||
return
|
||
}
|
||
|
||
let script = RDEPUBJavaScriptBridge.applyPresentationScript(for: request)
|
||
RDEPUBWebViewDebug.logJavaScript(
|
||
debugScope,
|
||
webView: webView,
|
||
action: "applyPresentation",
|
||
details: "spine=\(request.spineIndex) page=\(request.pageIndex) total=\(request.totalPagesInChapter)"
|
||
)
|
||
|
||
webView.evaluateJavaScript(script) { [weak self] _, error in
|
||
guard let self else { return }
|
||
if let error {
|
||
RDEPUBWebViewDebug.logJavaScript(self.debugScope, webView: webView, action: "applyPresentationFailed", details: error.localizedDescription)
|
||
self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription)
|
||
return
|
||
}
|
||
self.applySearchDecorationsIfNeeded {
|
||
self.rendered()
|
||
}
|
||
}
|
||
}
|
||
}
|