ReadViewSDK/Sources/RDEpubReaderView/EPUBCore/RDEPUBWebView.swift

279 lines
9.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 isReaderPageVisible = true
var hasReaderPageBeenVisible = false
var needsReaderPagePlaybackRestart = false
var appliedNativeMediaPlaybackSuspended = false
var isNativeMediaPlaybackOperationInFlight = false
var nativeMediaPlaybackOperationGeneration: UInt = 0
var nativeMediaPlaybackRequests: [(suspended: Bool, completion: () -> Void)] = []
var isWaitingForReaderPageMediaNavigation = false
var pendingReaderPageMediaNavigation: WKNavigation?
var isReaderPageMediaActivationBlocked = 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
// configureWebViewIfNeededwebView 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
isReaderPageVisible = true
hasReaderPageBeenVisible = false
needsReaderPagePlaybackRestart = false
isWaitingForReaderPageMediaNavigation = false
pendingReaderPageMediaNavigation = nil
isReaderPageMediaActivationBlocked = 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)
if request.isFixedLayout {
// A fixed-layout load replaces the whole Hype document. Keep WebKit
// suspended until navigation finishes so the outgoing page cannot
// overlap the new page's zero-second narration.
hasReaderPageBeenVisible = false
needsReaderPagePlaybackRestart = false
isWaitingForReaderPageMediaNavigation = true
setNativeReaderPageMediaPlaybackSuspended(true)
}
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?()
}
}
}