fix epub reader playback and trial navigation
This commit is contained in:
@@ -56,4 +56,4 @@ public struct RDEPUBNavigatorLayoutContext: Equatable {
|
||||
insets.right = horizontalInsets
|
||||
return insets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ public enum RDEPUBSafeArea {
|
||||
/// paddings, not approximations of the safe area itself.
|
||||
public static let minimumVerticalTextMargin: CGFloat = 20
|
||||
|
||||
/// Compact top/bottom padding used by a landscape two-page spread. This
|
||||
/// applies only beyond the actual safe area, which is always preserved.
|
||||
public static let landscapeVerticalTextMargin: CGFloat = 12
|
||||
|
||||
public static let minimumHorizontalTextMargin: CGFloat = 16
|
||||
|
||||
public static func keyWindow() -> UIWindow? {
|
||||
|
||||
@@ -25,10 +25,22 @@ extension RDEPUBWebView {
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
)
|
||||
userContentController.addUserScript(
|
||||
WKUserScript(
|
||||
source: Self.readerPageMediaLifecycleUserScript,
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: false
|
||||
)
|
||||
)
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.websiteDataStore = .nonPersistent()
|
||||
configuration.userContentController = userContentController
|
||||
// Interactive fixed-layout EPUBs commonly start narration from their
|
||||
// document-load timeline. Allow those page-owned audio elements to play
|
||||
// without requiring an extra tap, and keep playback inside the reader.
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
let schemeHandler = RDEPUBResourceURLSchemeHandler(parser: parser)
|
||||
configuration.setURLSchemeHandler(schemeHandler, forURLScheme: RDEPUBResourceURLSchemeHandler.scheme)
|
||||
|
||||
@@ -130,3 +142,110 @@ private final class RDEPUBWeakScriptMessageHandler: NSObject, WKScriptMessageHan
|
||||
target?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
}
|
||||
|
||||
extension RDEPUBWebView {
|
||||
|
||||
/// Installed in every frame before EPUB scripts run. Hidden pages start in
|
||||
/// a suspended state, including audio created with `new Audio()` that is
|
||||
/// not attached to the DOM (as used by Tumult Hype publications).
|
||||
static let readerPageMediaLifecycleUserScript = #"""
|
||||
(() => {
|
||||
if (window.__rdReaderMediaLifecycleInstalled) return;
|
||||
window.__rdReaderMediaLifecycleInstalled = true;
|
||||
|
||||
let isActive = false;
|
||||
const trackedMedia = new Set();
|
||||
const command = "rd-reader-page-visibility";
|
||||
const mediaPrototype = window.HTMLMediaElement && window.HTMLMediaElement.prototype;
|
||||
const nativePlay = mediaPrototype && mediaPrototype.play;
|
||||
|
||||
const register = media => {
|
||||
if (media) trackedMedia.add(media);
|
||||
return media;
|
||||
};
|
||||
|
||||
const collectDocumentMedia = () => {
|
||||
if (!document.querySelectorAll) return;
|
||||
document.querySelectorAll("audio,video").forEach(register);
|
||||
};
|
||||
|
||||
const setActive = active => {
|
||||
isActive = !!active;
|
||||
collectDocumentMedia();
|
||||
trackedMedia.forEach(media => {
|
||||
if (isActive) {
|
||||
if (media.__rdReaderReplayWhenVisible) {
|
||||
media.__rdReaderReplayWhenVisible = false;
|
||||
try {
|
||||
media.currentTime = 0;
|
||||
const result = nativePlay && nativePlay.call(media);
|
||||
if (result && result.catch) result.catch(() => {});
|
||||
} catch (_) {}
|
||||
}
|
||||
} else {
|
||||
if (media.__rdReaderWasPlayed || media.autoplay || (!media.paused && !media.ended)) {
|
||||
media.__rdReaderReplayWhenVisible = true;
|
||||
}
|
||||
if (!media.paused) {
|
||||
try { media.pause(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (mediaPrototype && nativePlay) {
|
||||
mediaPrototype.play = function() {
|
||||
register(this);
|
||||
this.__rdReaderWasPlayed = true;
|
||||
if (!isActive) {
|
||||
this.__rdReaderReplayWhenVisible = true;
|
||||
try { this.pause(); } catch (_) {}
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.__rdReaderReplayWhenVisible = false;
|
||||
return nativePlay.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
const NativeAudio = window.Audio;
|
||||
if (typeof NativeAudio === "function") {
|
||||
const ReaderAudio = function(source) {
|
||||
const media = arguments.length > 0 ? new NativeAudio(source) : new NativeAudio();
|
||||
return register(media);
|
||||
};
|
||||
ReaderAudio.prototype = NativeAudio.prototype;
|
||||
try { Object.setPrototypeOf(ReaderAudio, NativeAudio); } catch (_) {}
|
||||
window.Audio = ReaderAudio;
|
||||
}
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
const message = event.data;
|
||||
if (!message || message.type !== command) return;
|
||||
setActive(message.active);
|
||||
for (let index = 0; index < window.frames.length; index += 1) {
|
||||
try { window.frames[index].postMessage(message, "*"); } catch (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => setActive(isActive));
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(() => setActive(isActive)).observe(document, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
})();
|
||||
"""#
|
||||
|
||||
func setReaderPageVisible(_ isVisible: Bool) {
|
||||
isReaderPageVisible = isVisible
|
||||
applyReaderPageMediaVisibility()
|
||||
}
|
||||
|
||||
func applyReaderPageMediaVisibility() {
|
||||
guard let webView else { return }
|
||||
let active = isReaderPageVisible ? "true" : "false"
|
||||
let script = "window.postMessage({type: 'rd-reader-page-visibility', active: \(active)}, '*');"
|
||||
webView.evaluateJavaScript(script, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ extension RDEPUBWebView: WKNavigationDelegate {
|
||||
|
||||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFinish", url: webView.url)
|
||||
applyReaderPageMediaVisibility()
|
||||
applyPresentation()
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,8 @@ public final class RDEPUBWebView: UIView {
|
||||
|
||||
var isFixedLayout = false
|
||||
|
||||
var isReaderPageVisible = true
|
||||
|
||||
var pendingProgressionRequest = false
|
||||
|
||||
var fixedLayoutReadyWorkItem: DispatchWorkItem?
|
||||
@@ -186,6 +188,7 @@ public final class RDEPUBWebView: UIView {
|
||||
pendingHighlights = []
|
||||
fixedSpread = nil
|
||||
isFixedLayout = false
|
||||
isReaderPageVisible = true
|
||||
pendingProgressionRequest = false
|
||||
didRenderCurrentRequest = false
|
||||
teardownWebView()
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
<script>
|
||||
(function() {
|
||||
var Fit = { AUTO: 'auto', PAGE: 'page', WIDTH: 'width' };
|
||||
var safeAreaInsets = { top: {{INSET_TOP}}, right: {{INSET_RIGHT}}, bottom: {{INSET_BOTTOM}}, left: {{INSET_LEFT}} };
|
||||
var viewportSize = { width: {{VIEWPORT_WIDTH}}, height: {{VIEWPORT_HEIGHT}} };
|
||||
var fit = '{{FIT_MODE}}';
|
||||
var pendingFrames = 0;
|
||||
@@ -206,10 +205,9 @@
|
||||
}
|
||||
|
||||
if (widthFit && scaledHeight > localViewport.height) {
|
||||
offsetY = safeAreaInsets.top;
|
||||
offsetY = 0;
|
||||
} else {
|
||||
offsetY = (localViewport.height - scaledHeight) / 2;
|
||||
offsetY += (safeAreaInsets.top - safeAreaInsets.bottom) / 2;
|
||||
}
|
||||
|
||||
iframe.style.left = offsetX + 'px';
|
||||
|
||||
Reference in New Issue
Block a user