右对齐文本显示修复(如"巫鸿"只显示"鸿"的问题): - RDEPUBChapterTailNormalizer: 短尾页合并时检查 avoidPageBreakInside 语义, 避免将带有此标记的短文本(如 right-info 署名)错误合并回上一页 - RDEPUBTextContentView: 续段归一化时跳过右对齐/居中对齐的段落, 防止错误修改 firstLineHeadIndent 导致首字不可见 - RDEPUBCSSCompatibilityLayer: 为含 text-align:right/center 的 CSS 块 自动注入 text-indent:0 !important,确保右对齐/居中文本无首行缩进 - RDEPUBTextRendererSupport: 排版属性归一化时将右对齐/居中段落的 firstLineHeadIndent 重置为 0 图片查看器及脚注点击功能: - 新增 RDEPUBImageViewController 和 RDEPUBImageViewerCoordinator, 支持从 WebView 和 TextPage 两种模式查看图片 - epub-bridge.js: 检测图片和脚注图片的点击事件,脚注图片显示弹窗 - RDEPUBJavaScriptBridge: 新增 imageDidTap/footnoteDidTap 桥接消息 - RDEPUBWebView: 新增图片和脚注点击的 delegate 方法 - RDEPUBAttachmentNormalizer: 改进脚注检测,优先使用 alt 文本判断 - RDEPUBPaginationModels: 新增 .footnote 附件类型 - RDEPUBPageLayoutSnapshot: 运行时动态解析附件类型,脚注优先级高于图片 位置解析改进: - RDEPUBReaderController+LocationResolution: 利用 rangeInfo 提升页码定位精度 其他: - RDEPUBTextBookCache: schema 版本升级至 13 - RDEPUBReaderTheme: 主题更新 - Pod 项目文件更新 Co-Authored-By: Claude <noreply@anthropic.com>
213 lines
6.6 KiB
Swift
213 lines
6.6 KiB
Swift
|
|
import UIKit
|
|
import WebKit
|
|
|
|
public protocol RDEPUBWebViewDelegate: AnyObject {
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String)
|
|
|
|
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didTapImageWithSource src: String, sourceRect: CGRect?)
|
|
|
|
func epubWebView(_ webView: RDEPUBWebView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?)
|
|
}
|
|
|
|
public extension RDEPUBWebViewDelegate {
|
|
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {}
|
|
func epubWebView(_ webView: RDEPUBWebView, didTapImageWithSource src: String, sourceRect: CGRect?) {}
|
|
func epubWebView(_ webView: RDEPUBWebView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?) {}
|
|
}
|
|
|
|
final class RDEPUBAnnotationWebView: WKWebView {
|
|
|
|
var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)?
|
|
|
|
override var canBecomeFirstResponder: Bool {
|
|
true
|
|
}
|
|
|
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
|
switch action {
|
|
case #selector(rd_copy(_:)),
|
|
#selector(rd_highlight(_:)),
|
|
#selector(rd_annotate(_:)):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
@objc func rd_copy(_ sender: Any?) {
|
|
onSelectionAction?(.copy)
|
|
}
|
|
|
|
@objc func rd_highlight(_ sender: Any?) {
|
|
onSelectionAction?(.highlight)
|
|
}
|
|
|
|
@objc func rd_annotate(_ sender: Any?) {
|
|
onSelectionAction?(.annotate)
|
|
}
|
|
}
|
|
|
|
public final class RDEPUBWebView: UIView {
|
|
|
|
public weak var delegate: RDEPUBWebViewDelegate?
|
|
|
|
public var onRendered: (() -> Void)?
|
|
|
|
var onDecorationsResolved: (([RDEPUBTextOverlayDecoration]) -> Void)?
|
|
|
|
var publication: RDEPUBPublication?
|
|
|
|
var currentRenderRequest: RDEPUBRenderRequest?
|
|
|
|
var webView: WKWebView?
|
|
|
|
var schemeHandler: RDEPUBResourceURLSchemeHandler?
|
|
|
|
var configuredPublicationKey: String?
|
|
|
|
var currentLoadSignature: String?
|
|
|
|
var currentSpineIndex = 0
|
|
|
|
var currentHref = ""
|
|
|
|
var currentPageIndex = 0
|
|
|
|
var currentTotalPagesInChapter = 1
|
|
|
|
var viewportSize: CGSize = .zero
|
|
|
|
var currentPadding: UIEdgeInsets = .zero
|
|
|
|
var currentFontSize: CGFloat = 16
|
|
|
|
var currentLineHeightMultiple: CGFloat = 1.5
|
|
|
|
var currentThemeBackgroundColor: String?
|
|
|
|
var currentThemeTextColor: String?
|
|
|
|
var targetLocation: RDEPUBLocation?
|
|
|
|
var pendingHighlights: [RDEPUBHighlight] = []
|
|
|
|
var fixedSpread: EPUBFixedSpread?
|
|
|
|
var isFixedLayout = false
|
|
|
|
var pendingProgressionRequest = false
|
|
|
|
var fixedLayoutReadyWorkItem: DispatchWorkItem?
|
|
|
|
var didRenderCurrentRequest = false
|
|
|
|
let debugScope = "ReaderWebView"
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
teardownWebView()
|
|
}
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
webView?.frame = bounds
|
|
}
|
|
|
|
public func reset() {
|
|
delegate = nil
|
|
onRendered = nil
|
|
onDecorationsResolved = nil
|
|
publication = nil
|
|
currentRenderRequest = nil
|
|
currentSpineIndex = 0
|
|
currentHref = ""
|
|
currentPageIndex = 0
|
|
currentTotalPagesInChapter = 1
|
|
viewportSize = .zero
|
|
currentPadding = .zero
|
|
currentFontSize = 16
|
|
currentLineHeightMultiple = 1.5
|
|
currentThemeBackgroundColor = nil
|
|
currentThemeTextColor = nil
|
|
targetLocation = nil
|
|
pendingHighlights = []
|
|
fixedSpread = nil
|
|
isFixedLayout = false
|
|
pendingProgressionRequest = false
|
|
didRenderCurrentRequest = false
|
|
teardownWebView()
|
|
}
|
|
|
|
public func load(publication: RDEPUBPublication, request: RDEPUBRenderRequest) {
|
|
configureWebViewIfNeeded(publication: publication)
|
|
guard let webView else { return }
|
|
|
|
self.publication = publication
|
|
self.currentRenderRequest = request
|
|
self.currentSpineIndex = request.primarySpineIndex
|
|
self.currentHref = request.primaryHref
|
|
self.didRenderCurrentRequest = false
|
|
cancelFixedLayoutReadyFallback()
|
|
|
|
let publicationKey = publication.parser.opfURL?.path ?? publication.parser.extractionRootURL?.path ?? ""
|
|
let loadSignature = pageLoadSignature(publicationKey: publicationKey, request: request)
|
|
RDEPUBWebViewDebug.log(
|
|
debugScope,
|
|
message: "load request=\(request.isFixedLayout ? "fixed" : "reflowable") signature=\(loadSignature) webView=\(RDEPUBWebViewDebug.webViewID(webView))"
|
|
)
|
|
|
|
switch request {
|
|
case .reflowable(let reflowableRequest):
|
|
handleReflowableLoad(publication: publication, request: reflowableRequest, loadSignature: loadSignature, in: webView)
|
|
case .fixed(let fixedRequest):
|
|
handleFixedLayoutLoad(publication: publication, request: fixedRequest, loadSignature: loadSignature, in: webView)
|
|
}
|
|
}
|
|
|
|
func pageLoadSignature(
|
|
publicationKey: String,
|
|
request: RDEPUBRenderRequest
|
|
) -> String {
|
|
switch request {
|
|
case .reflowable(let request):
|
|
return reflowableLoadSignature(publicationKey: publicationKey, request: request)
|
|
case .fixed(let request):
|
|
return fixedLayoutLoadSignature(publicationKey: publicationKey, request: request)
|
|
}
|
|
}
|
|
|
|
func rendered() {
|
|
guard !didRenderCurrentRequest else { return }
|
|
didRenderCurrentRequest = true
|
|
cancelFixedLayoutReadyFallback()
|
|
RDEPUBWebViewDebug.log(debugScope, message: "rendered spine=\(currentSpineIndex) href=\(currentHref) fixed=\(isFixedLayout)")
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
guard let self else { return }
|
|
self.delegate?.epubWebViewDidFinishRendering(self)
|
|
self.onRendered?()
|
|
}
|
|
}
|
|
}
|