- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
118 lines
6.1 KiB
Swift
118 lines
6.1 KiB
Swift
// RDEPUBWebView+JavaScriptBridge.swift
|
||
// RDEPUBWebView 的 JS 桥接和导航委托扩展
|
||
// 实现 WKNavigationDelegate 处理页面加载生命周期,
|
||
// 实现 WKScriptMessageHandler 接收 JS 消息(进度/选择/链接/错误/固定版式就绪),
|
||
// 并将内部链接和外部链接路由到委托方法。
|
||
|
||
import Foundation
|
||
import WebKit
|
||
|
||
extension RDEPUBWebView {
|
||
/// 从 URL 解析内部位置(ss-reader:// 协议的链接)
|
||
func internalLocation(from url: URL) -> RDEPUBLocation? {
|
||
let href = url.path.removingPercentEncoding?.trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
|
||
let fragment = url.fragment
|
||
guard !href.isEmpty || fragment != nil else { return nil }
|
||
return RDEPUBLocation(href: href, progression: 0, fragment: fragment)
|
||
}
|
||
|
||
/// 从 JS 消息体中构建当前位置对象
|
||
func currentLocation(from body: [String: Any]) -> RDEPUBLocation {
|
||
let progression = (body["progression"] as? NSNumber)?.doubleValue ?? 0
|
||
let lastProgression = (body["lastProgression"] as? NSNumber)?.doubleValue
|
||
let fragment = body["fragment"] as? String
|
||
return RDEPUBLocation(href: currentHref, progression: progression, lastProgression: lastProgression, fragment: fragment)
|
||
}
|
||
}
|
||
|
||
extension RDEPUBWebView: WKNavigationDelegate {
|
||
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didStart", url: webView.url)
|
||
}
|
||
|
||
public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didCommit", url: webView.url)
|
||
}
|
||
|
||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFinish", url: webView.url)
|
||
applyPresentation()
|
||
}
|
||
|
||
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "processTerminated", url: webView.url)
|
||
}
|
||
|
||
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFail", url: webView.url, error: error)
|
||
}
|
||
|
||
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFailProvisional", url: webView.url, error: error)
|
||
}
|
||
|
||
/// 导航策略决策:区分内部链接(ss-reader://)、外部链接(http/https/mailto/tel)和其他导航
|
||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "decidePolicy", url: navigationAction.request.url)
|
||
guard navigationAction.navigationType == .linkActivated,
|
||
let url = navigationAction.request.url else {
|
||
decisionHandler(.allow)
|
||
return
|
||
}
|
||
|
||
if url.scheme == RDEPUBResourceURLSchemeHandler.scheme,
|
||
let location = internalLocation(from: url) {
|
||
delegate?.epubWebView(self, didActivateInternalLink: location, fromSpineIndex: currentSpineIndex)
|
||
decisionHandler(.cancel)
|
||
return
|
||
}
|
||
|
||
if let scheme = url.scheme?.lowercased(), ["http", "https", "mailto", "tel"].contains(scheme) {
|
||
delegate?.epubWebView(self, didActivateExternalLink: url)
|
||
decisionHandler(.cancel)
|
||
return
|
||
}
|
||
|
||
decisionHandler(.allow)
|
||
}
|
||
}
|
||
|
||
extension RDEPUBWebView: WKScriptMessageHandler {
|
||
/// 接收 JS 桥接消息,根据消息类型路由到对应的委托方法
|
||
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||
RDEPUBWebViewDebug.logMessage(debugScope, webView: message.webView, name: message.name, body: message.body)
|
||
switch message.name {
|
||
case RDEPUBJavaScriptBridgeMessage.progressionChanged.rawValue:
|
||
guard let body = message.body as? [String: Any] else { return }
|
||
delegate?.epubWebView(self, didUpdateLocation: currentLocation(from: body), spineIndex: currentSpineIndex)
|
||
case RDEPUBJavaScriptBridgeMessage.selectionChanged.rawValue:
|
||
if let body = message.body as? [String: Any],
|
||
let text = body["text"] as? String,
|
||
let rangeInfo = body["rangeInfo"] as? String {
|
||
let location = currentLocation(from: body)
|
||
let selection = RDEPUBSelection(location: location, text: text, rangeInfo: rangeInfo)
|
||
delegate?.epubWebView(self, didChangeSelection: selection, spineIndex: currentSpineIndex)
|
||
} else {
|
||
delegate?.epubWebView(self, didChangeSelection: nil, spineIndex: currentSpineIndex)
|
||
}
|
||
case RDEPUBJavaScriptBridgeMessage.internalLink.rawValue:
|
||
guard let body = message.body as? [String: Any],
|
||
let href = body["href"] as? String else { return }
|
||
let location = RDEPUBLocation(href: href, progression: 0)
|
||
delegate?.epubWebView(self, didActivateInternalLink: location, fromSpineIndex: currentSpineIndex)
|
||
case RDEPUBJavaScriptBridgeMessage.externalLink.rawValue:
|
||
guard let body = message.body as? [String: Any],
|
||
let urlString = body["url"] as? String,
|
||
let url = URL(string: urlString) else { return }
|
||
delegate?.epubWebView(self, didActivateExternalLink: url)
|
||
case RDEPUBJavaScriptBridgeMessage.javaScriptError.rawValue:
|
||
delegate?.epubWebView(self, didLogJavaScriptError: "\(message.body)")
|
||
case RDEPUBJavaScriptBridgeMessage.fixedLayoutReady.rawValue:
|
||
applySearchDecorationsIfNeeded {
|
||
self.rendered()
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
} |