- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
244 lines
7.9 KiB
Swift
244 lines
7.9 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)?
|
||
|
||
// M-15: Backing store for UIEditMenuInteraction (iOS 16+).
|
||
// Stored as Any? to avoid @available on stored property restriction.
|
||
var _editMenuInteraction: Any?
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
// MARK: - UIEditMenuInteractionDelegate (iOS 16+)
|
||
|
||
@available(iOS 16.0, *)
|
||
extension RDEPUBAnnotationWebView: UIEditMenuInteractionDelegate {
|
||
func editMenuInteraction(
|
||
_ interaction: UIEditMenuInteraction,
|
||
menuFor configuration: UIEditMenuConfiguration
|
||
) -> UIMenu {
|
||
UIMenu(children: [
|
||
UICommand(title: "拷贝", action: #selector(rd_copy(_:))),
|
||
UICommand(title: "高亮", action: #selector(rd_highlight(_:))),
|
||
UICommand(title: "批注", action: #selector(rd_annotate(_:)))
|
||
])
|
||
}
|
||
|
||
func editMenuInteraction(
|
||
_ interaction: UIEditMenuInteraction,
|
||
targetRectFor configuration: UIEditMenuConfiguration
|
||
) -> CGRect {
|
||
bounds
|
||
}
|
||
}
|
||
|
||
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")
|
||
}
|
||
|
||
// M-08: script message handler 经 RDEPUBWeakScriptMessageHandler 弱代理注册
|
||
// (见 configureWebViewIfNeeded),webView → userContentController → self
|
||
// 的保留环不再成立,deinit 会随最后一个持有者释放而正常触发。
|
||
// reset() 仍可用于在丢弃路径上提前拆除 WKWebView(不必等 dealloc)。
|
||
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?()
|
||
}
|
||
}
|
||
}
|