1371 lines
43 KiB
JavaScript
1371 lines
43 KiB
JavaScript
(() => {
|
||
if (window.__qlearnPresenterBootstrapInstalled) return;
|
||
window.__qlearnPresenterBootstrapInstalled = true;
|
||
|
||
function startPresenter(config = {}) {
|
||
if (window.__qlearnPresenter) {
|
||
window.__qlearnPresenter.exit(false);
|
||
}
|
||
|
||
const presenterMode = config.mode || "single";
|
||
const sessionId = config.sessionId || "standalone";
|
||
|
||
const detectedPage = window.__hudraPageDetector?.resolve();
|
||
|
||
if (!detectedPage?.supported) {
|
||
window.__hudraPageDetector?.showNotice(
|
||
"Hudra couldn’t start because this page does not contain a supported lesson."
|
||
);
|
||
return;
|
||
}
|
||
|
||
const { contentRoot, pageTitleElement } = detectedPage;
|
||
|
||
let pageTitle = pageTitleElement?.textContent?.trim() || "";
|
||
|
||
function isHidden(element) {
|
||
if (!element || element.hidden) return true;
|
||
|
||
const computedStyle = window.getComputedStyle(element);
|
||
return (
|
||
computedStyle.display === "none" ||
|
||
computedStyle.visibility === "hidden"
|
||
);
|
||
}
|
||
|
||
const visibleSlideHeadings = [
|
||
...contentRoot.querySelectorAll("h2, h3")
|
||
].filter((heading) => {
|
||
const hasText = heading.textContent.trim().length > 0;
|
||
const hasMedia = Boolean(
|
||
heading.querySelector("img, video, iframe, svg, canvas, picture")
|
||
);
|
||
|
||
return (hasText || hasMedia) && !isHidden(heading);
|
||
});
|
||
|
||
const slideDefinitions = [];
|
||
|
||
if (visibleSlideHeadings.length) {
|
||
visibleSlideHeadings.forEach((heading, index) => {
|
||
const nextHeading = visibleSlideHeadings[index + 1] || null;
|
||
const range = document.createRange();
|
||
|
||
range.setStartBefore(heading);
|
||
|
||
if (nextHeading) {
|
||
range.setEndBefore(nextHeading);
|
||
} else {
|
||
range.setEnd(contentRoot, contentRoot.childNodes.length);
|
||
}
|
||
|
||
slideDefinitions.push({
|
||
index,
|
||
headingLevel: heading.tagName,
|
||
fragment: range.cloneContents()
|
||
});
|
||
|
||
if (typeof range.detach === "function") {
|
||
range.detach();
|
||
}
|
||
});
|
||
} else {
|
||
const range = document.createRange();
|
||
range.selectNodeContents(contentRoot);
|
||
|
||
slideDefinitions.push({
|
||
index: 0,
|
||
headingLevel: null,
|
||
fragment: range.cloneContents()
|
||
});
|
||
|
||
if (typeof range.detach === "function") {
|
||
range.detach();
|
||
}
|
||
}
|
||
|
||
const presentationLayer = document.createElement("div");
|
||
presentationLayer.id = "qlearn-presenter-layer";
|
||
document.body.appendChild(presentationLayer);
|
||
|
||
const pageHeader = document.createElement("div");
|
||
pageHeader.id = "qlearn-presenter-page-title";
|
||
pageHeader.textContent = pageTitle;
|
||
presentationLayer.appendChild(pageHeader);
|
||
|
||
const presenterCredit = document.createElement("div");
|
||
presenterCredit.id = "qlearn-presenter-credit";
|
||
presenterCredit.textContent = "Hudra";
|
||
presentationLayer.appendChild(presenterCredit);
|
||
|
||
const gameCredit = document.createElement("div");
|
||
gameCredit.id = "qlearn-presenter-game";
|
||
gameCredit.textContent = "The Game";
|
||
presentationLayer.appendChild(gameCredit);
|
||
|
||
const slidesContainer = document.createElement("div");
|
||
slidesContainer.id = "qlearn-presenter-slides";
|
||
presentationLayer.appendChild(slidesContainer);
|
||
|
||
const toast = document.createElement("div");
|
||
toast.id = "qlearn-presenter-toast";
|
||
presentationLayer.appendChild(toast);
|
||
|
||
const audienceHiddenOverlay = document.createElement("div");
|
||
audienceHiddenOverlay.id = "qlearn-presenter-audience-hidden";
|
||
audienceHiddenOverlay.innerHTML = `<div>Audience Hidden</div>`;
|
||
presentationLayer.appendChild(audienceHiddenOverlay);
|
||
|
||
let slideWrappers = slideDefinitions.map((slide, index) => {
|
||
const wrapper = document.createElement("div");
|
||
wrapper.className = "qlearn-presenter-slide";
|
||
wrapper.dataset.slideIndex = String(index);
|
||
|
||
if (slide.headingLevel) {
|
||
wrapper.dataset.headingLevel = slide.headingLevel.toLowerCase();
|
||
}
|
||
|
||
wrapper.appendChild(slide.fragment.cloneNode(true));
|
||
slidesContainer.appendChild(wrapper);
|
||
return wrapper;
|
||
});
|
||
|
||
slideWrappers.forEach((wrapper) => {
|
||
wrapper
|
||
.querySelectorAll(
|
||
"script, style, noscript, [hidden], [aria-hidden='true']"
|
||
)
|
||
.forEach((element) => element.remove());
|
||
|
||
wrapper.querySelectorAll("h1").forEach((element) => element.remove());
|
||
});
|
||
|
||
function isImageOnlySlide(wrapper) {
|
||
const images = [...wrapper.querySelectorAll("img")].filter((image) => {
|
||
const style = window.getComputedStyle(image);
|
||
return style.display !== "none" && style.visibility !== "hidden";
|
||
});
|
||
|
||
if (images.length !== 1) return false;
|
||
|
||
if (wrapper.querySelector("video, iframe, canvas, svg, table, pre, code, ul, ol")) {
|
||
return false;
|
||
}
|
||
|
||
const walker = document.createTreeWalker(
|
||
wrapper,
|
||
NodeFilter.SHOW_TEXT,
|
||
{
|
||
acceptNode(node) {
|
||
return node.textContent.trim()
|
||
? NodeFilter.FILTER_ACCEPT
|
||
: NodeFilter.FILTER_REJECT;
|
||
}
|
||
}
|
||
);
|
||
|
||
return !walker.nextNode();
|
||
}
|
||
|
||
slideWrappers.forEach((wrapper) => {
|
||
wrapper.classList.toggle("qlp-image-only", isImageOnlySlide(wrapper));
|
||
});
|
||
|
||
const style = document.createElement("style");
|
||
style.id = "qlearn-presenter-style";
|
||
style.textContent = `
|
||
body.qlearn-presenting {
|
||
overflow: hidden !important;
|
||
background: #111 !important;
|
||
}
|
||
|
||
body.qlearn-presenting > *:not(#qlearn-presenter-layer):not(#qlearn-presenter-controls) {
|
||
visibility: hidden !important;
|
||
}
|
||
|
||
#qlearn-presenter-layer {
|
||
visibility: visible !important;
|
||
position: fixed !important;
|
||
inset: 0 !important;
|
||
z-index: 2147483646 !important;
|
||
overflow: hidden !important;
|
||
background: white !important;
|
||
--qlp-slide-pad-top: 11vh;
|
||
--qlp-slide-pad-side: 7vw;
|
||
--qlp-slide-pad-bottom: 13vh;
|
||
--qlp-body-size: clamp(21px, 2.1vw, 33px);
|
||
--qlp-h2-size: clamp(44px, 5.8vw, 78px);
|
||
--qlp-h3-size: clamp(38px, 4.8vw, 66px);
|
||
--qlp-media-height: 58vh;
|
||
}
|
||
|
||
#qlearn-presenter-layer.qlp-aspect-16-10 {
|
||
--qlp-slide-pad-top: 10vh;
|
||
--qlp-slide-pad-side: 6.5vw;
|
||
--qlp-slide-pad-bottom: 11vh;
|
||
--qlp-body-size: clamp(20px, 1.95vw, 31px);
|
||
--qlp-h2-size: clamp(42px, 5.2vw, 72px);
|
||
--qlp-h3-size: clamp(36px, 4.3vw, 60px);
|
||
--qlp-media-height: 62vh;
|
||
}
|
||
|
||
#qlearn-presenter-layer,
|
||
#qlearn-presenter-layer * {
|
||
visibility: visible !important;
|
||
}
|
||
|
||
#qlearn-presenter-page-title {
|
||
position: absolute !important;
|
||
top: 0 !important;
|
||
left: 0 !important;
|
||
right: 0 !important;
|
||
z-index: 10 !important;
|
||
box-sizing: border-box !important;
|
||
min-height: 48px !important;
|
||
padding: 12px 7vw 10px !important;
|
||
overflow: hidden !important;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important;
|
||
background: rgba(255, 255, 255, 0.97) !important;
|
||
color: #3a3a3a !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: clamp(16px, 1.4vw, 22px) !important;
|
||
font-weight: 600 !important;
|
||
line-height: 1.2 !important;
|
||
text-overflow: ellipsis !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
|
||
#qlearn-presenter-credit {
|
||
position: absolute !important;
|
||
right: 18px !important;
|
||
bottom: 12px !important;
|
||
z-index: 20 !important;
|
||
color: rgba(0, 0, 0, 0.52) !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: 11px !important;
|
||
font-weight: 400 !important;
|
||
line-height: 1 !important;
|
||
pointer-events: none !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
|
||
#qlearn-presenter-game {
|
||
position: absolute !important;
|
||
left: 18px !important;
|
||
bottom: 12px !important;
|
||
z-index: 95 !important;
|
||
color: rgba(255, 255, 255, 0.72) !important;
|
||
mix-blend-mode: difference !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: 11px !important;
|
||
font-weight: 400 !important;
|
||
line-height: 1 !important;
|
||
pointer-events: none !important;
|
||
white-space: nowrap !important;
|
||
opacity: 1 !important;
|
||
transition: opacity 700ms ease !important;
|
||
}
|
||
|
||
#qlearn-presenter-game.qlearn-presenter-game-hidden {
|
||
opacity: 0 !important;
|
||
}
|
||
|
||
#qlearn-presenter-slides {
|
||
position: absolute !important;
|
||
inset: 0 !important;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide {
|
||
display: block !important;
|
||
position: absolute !important;
|
||
inset: 0 !important;
|
||
z-index: 0 !important;
|
||
box-sizing: border-box !important;
|
||
overflow-x: hidden !important;
|
||
overflow-y: auto !important;
|
||
padding: var(--qlp-slide-pad-top) var(--qlp-slide-pad-side) var(--qlp-slide-pad-bottom) !important;
|
||
background: white !important;
|
||
color: #222 !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: var(--qlp-body-size) !important;
|
||
line-height: 1.4 !important;
|
||
opacity: 0 !important;
|
||
visibility: hidden !important;
|
||
transform: translateY(8px) !important;
|
||
pointer-events: none !important;
|
||
transition:
|
||
opacity 220ms ease,
|
||
transform 220ms ease,
|
||
visibility 0s linear 220ms !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide.qlearn-slide-active {
|
||
z-index: 1 !important;
|
||
opacity: 1 !important;
|
||
visibility: visible !important;
|
||
transform: translateY(0) !important;
|
||
pointer-events: auto !important;
|
||
transition:
|
||
opacity 220ms ease,
|
||
transform 220ms ease,
|
||
visibility 0s linear 0s !important;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.qlearn-presenter-slide {
|
||
transform: none !important;
|
||
transition: none !important;
|
||
}
|
||
}
|
||
|
||
.qlearn-presenter-slide > div,
|
||
.qlearn-presenter-slide section,
|
||
.qlearn-presenter-slide article {
|
||
max-width: none !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide h2 {
|
||
margin: 0 0 0.7em !important;
|
||
padding: 0 !important;
|
||
color: #222 !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: var(--qlp-h2-size) !important;
|
||
font-weight: 700 !important;
|
||
line-height: 1.06 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide h3 {
|
||
margin: 0 0 0.7em !important;
|
||
padding: 0 !important;
|
||
color: #222 !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: var(--qlp-h3-size) !important;
|
||
font-weight: 700 !important;
|
||
line-height: 1.1 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide h4 {
|
||
margin: 1em 0 0.45em !important;
|
||
color: #222 !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: clamp(27px, 3vw, 42px) !important;
|
||
font-weight: 700 !important;
|
||
line-height: 1.15 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide h5,
|
||
.qlearn-presenter-slide h6 {
|
||
margin: 0.9em 0 0.4em !important;
|
||
color: #222 !important;
|
||
font-family: Arial, sans-serif !important;
|
||
font-size: 1.05em !important;
|
||
font-weight: 700 !important;
|
||
line-height: 1.2 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide h2:has(img),
|
||
.qlearn-presenter-slide h3:has(img) {
|
||
margin: 0 0 0.7em !important;
|
||
padding: 0 !important;
|
||
line-height: 1 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide h2 img,
|
||
.qlearn-presenter-slide h3 img {
|
||
display: block !important;
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
height: auto !important;
|
||
max-height: 28vh !important;
|
||
object-fit: contain !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide p {
|
||
margin-top: 0 !important;
|
||
margin-bottom: 0.8em !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide ul,
|
||
.qlearn-presenter-slide ol {
|
||
margin-top: 0.4em !important;
|
||
margin-bottom: 0.8em !important;
|
||
padding-left: 1.5em !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide li {
|
||
margin-bottom: 0.3em !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide a {
|
||
color: #0068a8 !important;
|
||
text-decoration: underline !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide img {
|
||
max-width: 100% !important;
|
||
max-height: calc(var(--qlp-media-height) * var(--qlp-media-scale, 1)) !important;
|
||
width: auto !important;
|
||
height: auto !important;
|
||
object-fit: contain !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide video {
|
||
display: block !important;
|
||
max-width: 100% !important;
|
||
max-height: calc(var(--qlp-media-height) * var(--qlp-media-scale, 1)) !important;
|
||
width: auto !important;
|
||
height: auto !important;
|
||
object-fit: contain !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide iframe {
|
||
display: block !important;
|
||
width: min(90vw, 1200px) !important;
|
||
height: min(65vh, 720px) !important;
|
||
max-width: 100% !important;
|
||
border: 0 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide table {
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
height: auto !important;
|
||
border-collapse: collapse !important;
|
||
table-layout: auto !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide tbody,
|
||
.qlearn-presenter-slide tr,
|
||
.qlearn-presenter-slide td,
|
||
.qlearn-presenter-slide th {
|
||
height: auto !important;
|
||
max-width: 100% !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide td,
|
||
.qlearn-presenter-slide th {
|
||
box-sizing: border-box !important;
|
||
padding: 0.35em !important;
|
||
vertical-align: top !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide [style*="width"] {
|
||
max-width: 100% !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide-content {
|
||
width: 100% !important;
|
||
box-sizing: border-box !important;
|
||
zoom: var(--qlp-content-scale, 1);
|
||
}
|
||
|
||
.qlearn-presenter-slide.qlp-tight {
|
||
padding-top: 8vh !important;
|
||
padding-bottom: 9vh !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide.qlp-very-tight {
|
||
padding-top: 7vh !important;
|
||
padding-right: 5vw !important;
|
||
padding-bottom: 8vh !important;
|
||
padding-left: 5vw !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide .qlp-continuation-title::after {
|
||
content: " · continued";
|
||
font-size: 0.42em;
|
||
font-weight: 500;
|
||
opacity: 0.62;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.qlearn-presenter-slide .qlp-compact-block {
|
||
font-size: 0.82em !important;
|
||
}
|
||
|
||
/* A slide containing exactly one image uses the full audience viewport. */
|
||
.qlearn-presenter-slide.qlp-image-only {
|
||
padding: 0 !important;
|
||
overflow: hidden !important;
|
||
background: #fff !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide.qlp-image-only .qlearn-presenter-slide-content {
|
||
width: 100vw !important;
|
||
height: 100vh !important;
|
||
zoom: 1 !important;
|
||
}
|
||
|
||
.qlearn-presenter-slide.qlp-image-only img {
|
||
position: absolute !important;
|
||
inset: 0 !important;
|
||
display: block !important;
|
||
width: 100vw !important;
|
||
height: 100vh !important;
|
||
max-width: none !important;
|
||
max-height: none !important;
|
||
margin: auto !important;
|
||
object-fit: contain !important;
|
||
}
|
||
|
||
#qlearn-presenter-layer.qlp-showing-image-only #qlearn-presenter-page-title {
|
||
display: none !important;
|
||
}
|
||
|
||
#qlearn-presenter-audience-hidden {
|
||
position: absolute !important;
|
||
inset: 0 !important;
|
||
z-index: 80 !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
background: #000 !important;
|
||
color: rgba(255, 255, 255, 0.92) !important;
|
||
font: 600 clamp(24px, 3vw, 44px)/1.2 Arial, sans-serif !important;
|
||
letter-spacing: 0.02em !important;
|
||
opacity: 0 !important;
|
||
visibility: hidden !important;
|
||
pointer-events: none !important;
|
||
transition: opacity 180ms ease, visibility 0s linear 180ms !important;
|
||
}
|
||
|
||
#qlearn-presenter-audience-hidden.qlearn-presenter-audience-hidden-visible {
|
||
opacity: 1 !important;
|
||
visibility: visible !important;
|
||
transition: opacity 180ms ease, visibility 0s linear 0s !important;
|
||
}
|
||
|
||
#qlearn-presenter-toast {
|
||
position: absolute !important;
|
||
left: 50% !important;
|
||
bottom: 78px !important;
|
||
transform: translateX(-50%) translateY(10px) !important;
|
||
z-index: 30 !important;
|
||
padding: 9px 14px !important;
|
||
border-radius: 999px !important;
|
||
background: rgba(0, 0, 0, 0.78) !important;
|
||
color: white !important;
|
||
font: 14px Arial, sans-serif !important;
|
||
opacity: 0 !important;
|
||
transition: opacity 140ms ease, transform 140ms ease !important;
|
||
pointer-events: none !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
|
||
#qlearn-presenter-toast.qlearn-presenter-toast-visible {
|
||
opacity: 1 !important;
|
||
transform: translateX(-50%) translateY(0) !important;
|
||
}
|
||
|
||
body.qlearn-presenter-audience #qlearn-presenter-controls {
|
||
display: none !important;
|
||
}
|
||
|
||
#qlearn-presenter-controls {
|
||
visibility: visible !important;
|
||
position: fixed !important;
|
||
left: 50% !important;
|
||
bottom: 20px !important;
|
||
transform: translateX(-50%) !important;
|
||
z-index: 2147483647 !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
gap: 12px !important;
|
||
padding: 10px 16px !important;
|
||
border-radius: 999px !important;
|
||
background: rgba(0, 0, 0, 0.8) !important;
|
||
color: white !important;
|
||
font: 16px Arial, sans-serif !important;
|
||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.3) !important;
|
||
}
|
||
|
||
#qlearn-presenter-controls,
|
||
#qlearn-presenter-controls * {
|
||
visibility: visible !important;
|
||
}
|
||
|
||
#qlearn-presenter-controls button {
|
||
border: 0 !important;
|
||
border-radius: 7px !important;
|
||
padding: 8px 13px !important;
|
||
background: white !important;
|
||
color: #222 !important;
|
||
cursor: pointer !important;
|
||
font: inherit !important;
|
||
}
|
||
|
||
#qlearn-presenter-controls button:hover:not(:disabled) {
|
||
background: #e8e8e8 !important;
|
||
}
|
||
|
||
#qlearn-presenter-controls button:disabled {
|
||
cursor: default !important;
|
||
opacity: 0.4 !important;
|
||
}
|
||
|
||
#qlearn-presenter-controls [data-role="counter"] {
|
||
min-width: 110px !important;
|
||
text-align: center !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
`;
|
||
|
||
document.head.appendChild(style);
|
||
|
||
function applyAspectMode() {
|
||
const ratio = window.innerWidth / Math.max(window.innerHeight, 1);
|
||
// Only two supported layouts: 16:9 (1.778) and 16:10 (1.6).
|
||
const isSixteenTen = Math.abs(ratio - 1.6) < Math.abs(ratio - 16 / 9);
|
||
presentationLayer.classList.toggle("qlp-aspect-16-10", isSixteenTen);
|
||
presentationLayer.classList.toggle("qlp-aspect-16-9", !isSixteenTen);
|
||
presentationLayer.dataset.aspect = isSixteenTen ? "16:10" : "16:9";
|
||
}
|
||
|
||
function wrapSlideContent(wrapper) {
|
||
let content = wrapper.querySelector(":scope > .qlearn-presenter-slide-content");
|
||
if (content) return content;
|
||
content = document.createElement("div");
|
||
content.className = "qlearn-presenter-slide-content";
|
||
while (wrapper.firstChild) content.appendChild(wrapper.firstChild);
|
||
wrapper.appendChild(content);
|
||
return content;
|
||
}
|
||
|
||
function slideOverflows(wrapper) {
|
||
const content = wrapSlideContent(wrapper);
|
||
return (
|
||
content.getBoundingClientRect().bottom >
|
||
wrapper.getBoundingClientRect().bottom - 4 ||
|
||
wrapper.scrollHeight > wrapper.clientHeight + 4
|
||
);
|
||
}
|
||
|
||
function resetFit(wrapper) {
|
||
wrapper.classList.remove("qlp-tight", "qlp-very-tight");
|
||
wrapper.style.setProperty("--qlp-content-scale", "1");
|
||
wrapper.style.setProperty("--qlp-media-scale", "1");
|
||
}
|
||
|
||
function fitSlide(wrapper, allowSmall = true) {
|
||
resetFit(wrapper);
|
||
if (wrapper.classList.contains("qlp-image-only")) return true;
|
||
if (!slideOverflows(wrapper)) return true;
|
||
|
||
wrapper.style.setProperty("--qlp-media-scale", "0.86");
|
||
if (!slideOverflows(wrapper)) return true;
|
||
|
||
wrapper.classList.add("qlp-tight");
|
||
if (!slideOverflows(wrapper)) return true;
|
||
|
||
wrapper.classList.add("qlp-very-tight");
|
||
if (!slideOverflows(wrapper)) return true;
|
||
|
||
const scales = allowSmall
|
||
? [0.96, 0.92, 0.88, 0.84, 0.80]
|
||
: [0.96, 0.92, 0.88];
|
||
for (const scale of scales) {
|
||
wrapper.style.setProperty("--qlp-content-scale", String(scale));
|
||
if (!slideOverflows(wrapper)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function makeContinuationHeading(sourceHeading) {
|
||
const heading = sourceHeading.cloneNode(true);
|
||
heading.classList.add("qlp-continuation-title");
|
||
return heading;
|
||
}
|
||
|
||
function splitList(list) {
|
||
const items = [...list.children].filter((item) => item.tagName === "LI");
|
||
if (items.length < 2) return [list.cloneNode(true)];
|
||
const midpoint = Math.ceil(items.length / 2);
|
||
return [items.slice(0, midpoint), items.slice(midpoint)].map((group) => {
|
||
const copy = list.cloneNode(false);
|
||
group.forEach((item) => copy.appendChild(item.cloneNode(true)));
|
||
return copy;
|
||
});
|
||
}
|
||
|
||
function divideOversizedBlock(block) {
|
||
if (block.matches("ul, ol")) return splitList(block);
|
||
return [block.cloneNode(true)];
|
||
}
|
||
|
||
function createGeneratedSlide(template, heading, continued) {
|
||
const wrapper = document.createElement("div");
|
||
wrapper.className = "qlearn-presenter-slide";
|
||
wrapper.dataset.headingLevel = template.dataset.headingLevel || "h2";
|
||
const content = document.createElement("div");
|
||
content.className = "qlearn-presenter-slide-content";
|
||
if (heading) {
|
||
content.appendChild(continued ? makeContinuationHeading(heading) : heading.cloneNode(true));
|
||
}
|
||
wrapper.appendChild(content);
|
||
return wrapper;
|
||
}
|
||
|
||
function paginateWrapper(original) {
|
||
const originalContent = wrapSlideContent(original);
|
||
const blocks = [...originalContent.children];
|
||
if (!blocks.length || fitSlide(original, false)) return [original];
|
||
|
||
const first = blocks[0]?.matches("h2, h3") ? blocks.shift() : null;
|
||
const generated = [];
|
||
let currentWrapper = createGeneratedSlide(original, first, false);
|
||
slidesContainer.appendChild(currentWrapper);
|
||
generated.push(currentWrapper);
|
||
|
||
function currentContent() {
|
||
return currentWrapper.querySelector(".qlearn-presenter-slide-content");
|
||
}
|
||
|
||
function newContinuation() {
|
||
currentWrapper = createGeneratedSlide(original, first, true);
|
||
slidesContainer.appendChild(currentWrapper);
|
||
generated.push(currentWrapper);
|
||
}
|
||
|
||
for (const sourceBlock of blocks) {
|
||
const pieces = divideOversizedBlock(sourceBlock);
|
||
for (const piece of pieces) {
|
||
currentContent().appendChild(piece);
|
||
resetFit(currentWrapper);
|
||
if (!slideOverflows(currentWrapper)) continue;
|
||
|
||
piece.remove();
|
||
const hasBody = [...currentContent().children].some((el) => !el.matches("h2, h3"));
|
||
if (hasBody) newContinuation();
|
||
currentContent().appendChild(piece);
|
||
|
||
if (!fitSlide(currentWrapper, false)) {
|
||
if (piece.matches("table, pre, iframe, video, img, div")) {
|
||
piece.classList.add("qlp-compact-block");
|
||
}
|
||
fitSlide(currentWrapper, true);
|
||
}
|
||
}
|
||
}
|
||
|
||
original.remove();
|
||
return generated;
|
||
}
|
||
|
||
function paginateAndFitSlides() {
|
||
applyAspectMode();
|
||
const originals = [...slideWrappers];
|
||
const rebuilt = [];
|
||
originals.forEach((wrapper) => rebuilt.push(...paginateWrapper(wrapper)));
|
||
slideWrappers = rebuilt;
|
||
slideWrappers.forEach((wrapper, index) => {
|
||
wrapper.dataset.slideIndex = String(index);
|
||
fitSlide(wrapper, true);
|
||
});
|
||
}
|
||
|
||
let resizeTimer = null;
|
||
function refitAfterResize() {
|
||
clearTimeout(resizeTimer);
|
||
resizeTimer = setTimeout(() => {
|
||
applyAspectMode();
|
||
slideWrappers.forEach((wrapper) => fitSlide(wrapper, true));
|
||
if (slideWrappers[current]) slideWrappers[current].scrollTop = 0;
|
||
}, 160);
|
||
}
|
||
|
||
applyAspectMode();
|
||
paginateAndFitSlides();
|
||
window.addEventListener("resize", refitAfterResize);
|
||
|
||
const controls = document.createElement("div");
|
||
controls.id = "qlearn-presenter-controls";
|
||
controls.innerHTML = `
|
||
<button type="button" data-action="prev">← Previous</button>
|
||
<span data-role="counter"></span>
|
||
<button type="button" data-action="next">Next →</button>
|
||
<button type="button" data-action="exit">Exit</button>
|
||
`;
|
||
|
||
document.body.appendChild(controls);
|
||
|
||
const previousButton = controls.querySelector('[data-action="prev"]');
|
||
const nextButton = controls.querySelector('[data-action="next"]');
|
||
const counter = controls.querySelector('[data-role="counter"]');
|
||
|
||
let current = 0;
|
||
let toastTimer = null;
|
||
let audienceHidden = false;
|
||
let audienceZoom = 100;
|
||
let audienceScrollY = 0;
|
||
|
||
function applyAudienceView() {
|
||
const slide = slideWrappers[current];
|
||
if (!slide) return;
|
||
slide.style.zoom = `${audienceZoom}%`;
|
||
slide.scrollTop = audienceScrollY;
|
||
requestAnimationFrame(() => { audienceScrollY = slide.scrollTop; });
|
||
}
|
||
|
||
function setAudienceZoom(value, announce = true) {
|
||
audienceZoom = Math.max(70, Math.min(150, Math.round(Number(value) / 5) * 5));
|
||
applyAudienceView();
|
||
if (announce) showToast(`Audience zoom ${audienceZoom}%`);
|
||
sendPresenterState();
|
||
}
|
||
|
||
function adjustAudienceZoom(delta) { setAudienceZoom(audienceZoom + Number(delta || 0)); }
|
||
|
||
function scrollAudience(delta) {
|
||
const slide = slideWrappers[current];
|
||
if (!slide) return;
|
||
slide.scrollBy({ top: Number(delta || 0), behavior: "smooth" });
|
||
setTimeout(() => { audienceScrollY = slide.scrollTop; sendPresenterState(); }, 180);
|
||
}
|
||
|
||
function resetAudienceView(announce = true) {
|
||
audienceZoom = 100;
|
||
audienceScrollY = 0;
|
||
applyAudienceView();
|
||
if (announce) showToast("Audience view reset");
|
||
sendPresenterState();
|
||
}
|
||
|
||
function setAudienceHidden(hidden, announce = true) {
|
||
audienceHidden = Boolean(hidden);
|
||
audienceHiddenOverlay.classList.toggle(
|
||
"qlearn-presenter-audience-hidden-visible",
|
||
audienceHidden
|
||
);
|
||
|
||
if (announce) {
|
||
showToast(audienceHidden ? "Audience hidden" : "Audience restored");
|
||
}
|
||
|
||
sendPresenterState();
|
||
}
|
||
|
||
function toggleAudienceHidden() {
|
||
if (videoModeActive) {
|
||
showToast("Pause the video before hiding the audience screen");
|
||
return;
|
||
}
|
||
setAudienceHidden(!audienceHidden);
|
||
}
|
||
|
||
function showToast(message) {
|
||
toast.textContent = message;
|
||
toast.classList.add("qlearn-presenter-toast-visible");
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => {
|
||
toast.classList.remove("qlearn-presenter-toast-visible");
|
||
}, 1200);
|
||
}
|
||
|
||
function isVisibleMedia(element) {
|
||
if (!element) return false;
|
||
const mediaStyle = window.getComputedStyle(element);
|
||
return mediaStyle.display !== "none" && mediaStyle.visibility !== "hidden";
|
||
}
|
||
|
||
function findPlayableVideo(slide) {
|
||
if (!slide) return null;
|
||
|
||
const directVideos = [...slide.querySelectorAll("video")];
|
||
const directVideo = directVideos.find(isVisibleMedia);
|
||
if (directVideo) {
|
||
return { video: directVideo, frame: null };
|
||
}
|
||
|
||
const videoFrames = [
|
||
...slide.querySelectorAll(
|
||
'iframe[data-media-type="video"], iframe[title*="video" i], iframe[src*="media_attachments_iframe"]'
|
||
)
|
||
].filter(isVisibleMedia);
|
||
|
||
for (const frame of videoFrames) {
|
||
try {
|
||
const frameDocument = frame.contentDocument || frame.contentWindow?.document;
|
||
const frameVideo = frameDocument?.querySelector("video");
|
||
if (frameVideo) return { video: frameVideo, frame };
|
||
} catch (error) {
|
||
// Cross-origin players cannot be controlled directly.
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
let activeVideo = null;
|
||
let activeVideoFrame = null;
|
||
let activeFullscreenDocument = null;
|
||
let videoModeActive = false;
|
||
let videoFullscreenActive = false;
|
||
let deliberatelyLeavingVideoFullscreen = false;
|
||
|
||
function getFullscreenElement(doc) {
|
||
return doc?.fullscreenElement || doc?.webkitFullscreenElement || null;
|
||
}
|
||
|
||
async function requestElementFullscreen(element) {
|
||
if (!element) return false;
|
||
try {
|
||
if (element.requestFullscreen) {
|
||
await element.requestFullscreen();
|
||
return true;
|
||
}
|
||
if (element.webkitRequestFullscreen) {
|
||
element.webkitRequestFullscreen();
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.warn("Hudra fullscreen request failed:", error);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function exitDocumentFullscreen(doc) {
|
||
if (!doc || !getFullscreenElement(doc)) return;
|
||
try {
|
||
if (doc.exitFullscreen) {
|
||
await doc.exitFullscreen();
|
||
} else if (doc.webkitExitFullscreen) {
|
||
doc.webkitExitFullscreen();
|
||
}
|
||
} catch (error) {
|
||
console.warn("Hudra could not exit video fullscreen:", error);
|
||
}
|
||
}
|
||
|
||
function removeVideoListeners() {
|
||
if (activeVideo) {
|
||
activeVideo.removeEventListener("ended", onActiveVideoEnded);
|
||
}
|
||
if (activeFullscreenDocument && activeFullscreenDocument !== document) {
|
||
activeFullscreenDocument.removeEventListener("fullscreenchange", onVideoFullscreenChange);
|
||
activeFullscreenDocument.removeEventListener("webkitfullscreenchange", onVideoFullscreenChange);
|
||
}
|
||
document.removeEventListener("fullscreenchange", onVideoFullscreenChange);
|
||
document.removeEventListener("webkitfullscreenchange", onVideoFullscreenChange);
|
||
}
|
||
|
||
function clearVideoState() {
|
||
removeVideoListeners();
|
||
activeVideo = null;
|
||
activeVideoFrame = null;
|
||
activeFullscreenDocument = null;
|
||
videoModeActive = false;
|
||
videoFullscreenActive = false;
|
||
deliberatelyLeavingVideoFullscreen = false;
|
||
}
|
||
|
||
async function stopActiveVideo({ pause = true, toastMessage = "⏸ Video paused" } = {}) {
|
||
if (!videoModeActive) return false;
|
||
|
||
const video = activeVideo;
|
||
const fullscreenDoc = activeFullscreenDocument;
|
||
|
||
deliberatelyLeavingVideoFullscreen = true;
|
||
if (pause && video && !video.paused) video.pause();
|
||
|
||
await exitDocumentFullscreen(fullscreenDoc);
|
||
if (fullscreenDoc !== document) {
|
||
await exitDocumentFullscreen(document);
|
||
}
|
||
|
||
clearVideoState();
|
||
if (toastMessage) showToast(toastMessage);
|
||
return true;
|
||
}
|
||
|
||
async function onActiveVideoEnded() {
|
||
await stopActiveVideo({
|
||
pause: false,
|
||
toastMessage: "↩ Video finished · returning to slide"
|
||
});
|
||
}
|
||
|
||
function onVideoFullscreenChange() {
|
||
if (!videoModeActive || deliberatelyLeavingVideoFullscreen) return;
|
||
|
||
const ownerFullscreen = getFullscreenElement(activeFullscreenDocument);
|
||
const parentFullscreen = getFullscreenElement(document);
|
||
|
||
if (videoFullscreenActive && !ownerFullscreen && !parentFullscreen) {
|
||
if (activeVideo && !activeVideo.paused) activeVideo.pause();
|
||
clearVideoState();
|
||
showToast("⏸ Video paused · returned to slide");
|
||
}
|
||
}
|
||
|
||
async function startCurrentSlideVideo() {
|
||
const slide = slideWrappers[current];
|
||
const playable = findPlayableVideo(slide);
|
||
|
||
if (!playable) {
|
||
showToast("No controllable video on this slide");
|
||
return false;
|
||
}
|
||
|
||
const { video, frame } = playable;
|
||
activeVideo = video;
|
||
activeVideoFrame = frame;
|
||
activeFullscreenDocument = video.ownerDocument || document;
|
||
videoModeActive = true;
|
||
deliberatelyLeavingVideoFullscreen = false;
|
||
|
||
video.addEventListener("ended", onActiveVideoEnded);
|
||
document.addEventListener("fullscreenchange", onVideoFullscreenChange);
|
||
document.addEventListener("webkitfullscreenchange", onVideoFullscreenChange);
|
||
if (activeFullscreenDocument !== document) {
|
||
activeFullscreenDocument.addEventListener("fullscreenchange", onVideoFullscreenChange);
|
||
activeFullscreenDocument.addEventListener("webkitfullscreenchange", onVideoFullscreenChange);
|
||
}
|
||
|
||
try {
|
||
if (video.ended) video.currentTime = 0;
|
||
|
||
// Prefer the actual video. If Canvas blocks that, fullscreen its iframe.
|
||
videoFullscreenActive = await requestElementFullscreen(video);
|
||
if (!videoFullscreenActive && frame) {
|
||
activeFullscreenDocument = document;
|
||
videoFullscreenActive = await requestElementFullscreen(frame);
|
||
}
|
||
|
||
await video.play();
|
||
showToast(
|
||
videoFullscreenActive
|
||
? "▶ Video playing fullscreen"
|
||
: "▶ Video playing · fullscreen unavailable"
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.warn("Hudra could not control this video:", error);
|
||
await stopActiveVideo({ pause: true, toastMessage: null });
|
||
showToast("Video player could not be controlled");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function toggleCurrentSlideVideo() {
|
||
if (videoModeActive) {
|
||
return stopActiveVideo({
|
||
pause: true,
|
||
toastMessage: "⏸ Video paused · returning to slide"
|
||
});
|
||
}
|
||
return startCurrentSlideVideo();
|
||
}
|
||
|
||
function makePreviewDocument(index) {
|
||
const slide = slideWrappers[index];
|
||
if (!slide) return "";
|
||
|
||
const clone = slide.cloneNode(true);
|
||
clone.classList.add("qlearn-slide-active");
|
||
clone.style.position = "relative";
|
||
clone.style.inset = "auto";
|
||
clone.style.width = "100%";
|
||
clone.style.height = "100%";
|
||
clone.style.opacity = "1";
|
||
clone.style.visibility = "visible";
|
||
clone.style.transform = "none";
|
||
clone.style.overflow = "hidden";
|
||
if (index === current) {
|
||
clone.style.zoom = `${audienceZoom}%`;
|
||
clone.dataset.hudraScrollY = String(audienceScrollY);
|
||
}
|
||
|
||
clone.querySelectorAll("video").forEach((video) => {
|
||
const replacement = document.createElement("div");
|
||
replacement.className = "hudra-preview-media";
|
||
replacement.textContent = "Video";
|
||
if (video.getAttribute("poster")) {
|
||
replacement.style.backgroundImage = `url('${video.getAttribute("poster").replace(/'/g, "%27")}')`;
|
||
}
|
||
video.replaceWith(replacement);
|
||
});
|
||
|
||
clone.querySelectorAll("iframe").forEach((frame) => {
|
||
const replacement = document.createElement("div");
|
||
replacement.className = "hudra-preview-media";
|
||
replacement.textContent = frame.title || "Embedded content";
|
||
frame.replaceWith(replacement);
|
||
});
|
||
|
||
return `<!doctype html><html><head><meta charset="utf-8"><base href="${location.href.replace(/"/g, """)}"><style>
|
||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#fff;}
|
||
body{font-family:Arial,sans-serif;}
|
||
.qlearn-presenter-slide{box-sizing:border-box;width:100%;height:100%;padding:7vh 6vw 9vh;background:white;color:#222;font-size:clamp(14px,2vw,30px);line-height:1.38;overflow:hidden;}
|
||
h2{margin:0 0 .65em;font-size:clamp(25px,5vw,66px);line-height:1.06;}
|
||
h3{margin:0 0 .65em;font-size:clamp(22px,4vw,52px);line-height:1.1;}
|
||
h4{font-size:clamp(18px,3vw,38px);}
|
||
p{margin-top:0;margin-bottom:.75em;} ul,ol{margin-top:.3em;margin-bottom:.7em;padding-left:1.4em;}
|
||
img,video{max-width:100%;max-height:56vh;width:auto;height:auto;object-fit:contain;}
|
||
.qlearn-presenter-slide.qlp-image-only{padding:0;position:relative;}
|
||
.qlearn-presenter-slide.qlp-image-only .qlearn-presenter-slide-content{width:100%;height:100%;}
|
||
.qlearn-presenter-slide.qlp-image-only img{position:absolute;inset:0;width:100%;height:100%;max-width:none;max-height:none;object-fit:contain;}
|
||
iframe{max-width:100%;height:54vh;border:0;} .hudra-preview-media{display:grid;place-items:center;min-height:34vh;border-radius:14px;background:#111722 center/cover no-repeat;color:#d7ddeb;font:700 clamp(16px,2vw,28px) system-ui;} table{width:100%;border-collapse:collapse;font-size:.78em;} td,th{padding:.3em;} pre{overflow:hidden;font-size:.7em;}
|
||
.qlp-continuation-title::after{content:" · continued";font-size:.42em;font-weight:500;opacity:.62;}
|
||
</style></head><body>${clone.outerHTML}<script>const s=document.querySelector('[data-hudra-scroll-y]');if(s)s.scrollTop=Number(s.dataset.hudraScrollY)||0;<\/script></body></html>`;
|
||
}
|
||
|
||
function sendPresenterState() {
|
||
chrome.runtime.sendMessage({
|
||
type: "QLEARN_PRESENTER_STATE",
|
||
sessionId,
|
||
current,
|
||
count: slideWrappers.length,
|
||
pageTitle,
|
||
audienceHidden,
|
||
audienceZoom,
|
||
audienceScrollY,
|
||
currentPreview: makePreviewDocument(current),
|
||
nextPreview: makePreviewDocument(current + 1)
|
||
}).catch(() => {});
|
||
}
|
||
|
||
function showSlide(index) {
|
||
if (videoModeActive) {
|
||
stopActiveVideo({ pause: true, toastMessage: null }).catch(() => {});
|
||
}
|
||
|
||
current = Math.max(0, Math.min(index, slideWrappers.length - 1));
|
||
|
||
slideWrappers.forEach((wrapper, wrapperIndex) => {
|
||
wrapper.classList.toggle(
|
||
"qlearn-slide-active",
|
||
wrapperIndex === current
|
||
);
|
||
});
|
||
|
||
presentationLayer.classList.toggle(
|
||
"qlp-showing-image-only",
|
||
slideWrappers[current]?.classList.contains("qlp-image-only")
|
||
);
|
||
|
||
audienceZoom = 100;
|
||
audienceScrollY = 0;
|
||
slideWrappers.forEach((wrapper) => { wrapper.style.zoom = "100%"; wrapper.scrollTop = 0; });
|
||
applyAudienceView();
|
||
counter.textContent = `Slide ${current + 1} of ${slideWrappers.length}`;
|
||
previousButton.disabled = current === 0;
|
||
nextButton.disabled = current === slideWrappers.length - 1;
|
||
|
||
sendPresenterState();
|
||
}
|
||
|
||
function exit(notifyBackground = true) {
|
||
document.removeEventListener("keydown", onKey);
|
||
window.removeEventListener("resize", refitAfterResize);
|
||
clearTimeout(resizeTimer);
|
||
clearTimeout(toastTimer);
|
||
clearTimeout(gameTimer);
|
||
if (activeVideo && !activeVideo.paused) activeVideo.pause();
|
||
if (videoModeActive) {
|
||
deliberatelyLeavingVideoFullscreen = true;
|
||
exitDocumentFullscreen(activeFullscreenDocument).catch(() => {});
|
||
clearVideoState();
|
||
}
|
||
presentationLayer.remove();
|
||
controls.remove();
|
||
style.remove();
|
||
document.body.classList.remove("qlearn-presenting", "qlearn-presenter-audience");
|
||
delete window.__qlearnPresenter;
|
||
|
||
if (notifyBackground) {
|
||
chrome.runtime.sendMessage({
|
||
type: "QLEARN_PRESENTER_ENDED",
|
||
sessionId
|
||
}).catch(() => {});
|
||
}
|
||
}
|
||
|
||
function onKey(event) {
|
||
const activeElement = document.activeElement;
|
||
const isTyping =
|
||
activeElement &&
|
||
["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName);
|
||
|
||
if (isTyping) return;
|
||
|
||
if (event.key.toLowerCase() === "b") {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (!event.repeat) toggleAudienceHidden();
|
||
return;
|
||
}
|
||
|
||
if (event.key === "Tab") {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (!event.repeat) toggleCurrentSlideVideo();
|
||
return;
|
||
}
|
||
|
||
if (["ArrowRight", "PageDown", " ", "Enter"].includes(event.key)) {
|
||
event.preventDefault();
|
||
if (videoModeActive) {
|
||
if (!event.repeat) showToast("Pause the video before changing slides");
|
||
return;
|
||
}
|
||
if (current < slideWrappers.length - 1) showSlide(current + 1);
|
||
return;
|
||
}
|
||
|
||
if (["ArrowLeft", "PageUp", "Backspace"].includes(event.key)) {
|
||
event.preventDefault();
|
||
if (videoModeActive) {
|
||
if (!event.repeat) showToast("Pause the video before changing slides");
|
||
return;
|
||
}
|
||
if (current > 0) showSlide(current - 1);
|
||
return;
|
||
}
|
||
|
||
if (event.key === "Home") {
|
||
event.preventDefault();
|
||
showSlide(0);
|
||
return;
|
||
}
|
||
|
||
if (event.key === "End") {
|
||
event.preventDefault();
|
||
showSlide(slideWrappers.length - 1);
|
||
return;
|
||
}
|
||
|
||
if (event.key === "Escape") {
|
||
event.preventDefault();
|
||
if (videoModeActive) {
|
||
stopActiveVideo({
|
||
pause: true,
|
||
toastMessage: "⏸ Video paused · returned to slide"
|
||
}).catch(() => {});
|
||
} else {
|
||
exit();
|
||
}
|
||
}
|
||
}
|
||
|
||
controls.addEventListener("click", (event) => {
|
||
const button = event.target.closest("button[data-action]");
|
||
if (!button) return;
|
||
|
||
const action = button.dataset.action;
|
||
|
||
if (action === "next" && current < slideWrappers.length - 1) {
|
||
if (videoModeActive) {
|
||
showToast("Pause the video before changing slides");
|
||
} else {
|
||
showSlide(current + 1);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (action === "prev" && current > 0) {
|
||
if (videoModeActive) {
|
||
showToast("Pause the video before changing slides");
|
||
} else {
|
||
showSlide(current - 1);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (action === "exit") exit();
|
||
});
|
||
|
||
document.body.classList.add("qlearn-presenting");
|
||
if (presenterMode === "audience") {
|
||
document.body.classList.add("qlearn-presenter-audience");
|
||
}
|
||
document.addEventListener("keydown", onKey);
|
||
|
||
const gameTimer = setTimeout(() => {
|
||
gameCredit.classList.add("qlearn-presenter-game-hidden");
|
||
setTimeout(() => gameCredit.remove(), 800);
|
||
}, 5000);
|
||
|
||
window.__qlearnPresenter = {
|
||
exit,
|
||
next() {
|
||
if (videoModeActive) {
|
||
showToast("Pause the video before changing slides");
|
||
} else if (current < slideWrappers.length - 1) {
|
||
showSlide(current + 1);
|
||
}
|
||
},
|
||
previous() {
|
||
if (videoModeActive) {
|
||
showToast("Pause the video before changing slides");
|
||
} else if (current > 0) {
|
||
showSlide(current - 1);
|
||
}
|
||
},
|
||
first() {
|
||
if (videoModeActive) showToast("Pause the video before changing slides");
|
||
else showSlide(0);
|
||
},
|
||
last() {
|
||
if (videoModeActive) showToast("Pause the video before changing slides");
|
||
else showSlide(slideWrappers.length - 1);
|
||
},
|
||
toggleVideo() {
|
||
return toggleCurrentSlideVideo();
|
||
},
|
||
toggleAudienceHidden() {
|
||
toggleAudienceHidden();
|
||
},
|
||
zoomIn() { adjustAudienceZoom(5); },
|
||
zoomOut() { adjustAudienceZoom(-5); },
|
||
setZoom(value) { setAudienceZoom(value); },
|
||
scrollUp() { scrollAudience(-140); },
|
||
scrollDown() { scrollAudience(140); },
|
||
scrollBy(value) { scrollAudience(value); },
|
||
resetView() { resetAudienceView(); },
|
||
goTo(index) {
|
||
const slideNumber = Number(index);
|
||
if (Number.isFinite(slideNumber)) showSlide(slideNumber);
|
||
},
|
||
getCurrentSlide() {
|
||
return current + 1;
|
||
},
|
||
getSlideCount() {
|
||
return slideWrappers.length;
|
||
},
|
||
reportState() {
|
||
sendPresenterState();
|
||
}
|
||
};
|
||
|
||
showSlide(0);
|
||
}
|
||
|
||
chrome.runtime.onMessage.addListener((message) => {
|
||
if (message?.type === "QLEARN_PRESENTER_START") {
|
||
startPresenter(message.config || {});
|
||
return;
|
||
}
|
||
|
||
if (message?.type === "QLEARN_PRESENTER_COMMAND" && window.__qlearnPresenter) {
|
||
const command = message.command;
|
||
if (command === "next") window.__qlearnPresenter.next();
|
||
if (command === "previous") window.__qlearnPresenter.previous();
|
||
if (command === "first") window.__qlearnPresenter.first();
|
||
if (command === "last") window.__qlearnPresenter.last();
|
||
if (command === "toggleVideo") window.__qlearnPresenter.toggleVideo();
|
||
if (command === "toggleAudienceHidden") window.__qlearnPresenter.toggleAudienceHidden();
|
||
if (command === "zoomIn") window.__qlearnPresenter.zoomIn();
|
||
if (command === "zoomOut") window.__qlearnPresenter.zoomOut();
|
||
if (command === "setZoom") window.__qlearnPresenter.setZoom(message.value);
|
||
if (command === "scrollUp") window.__qlearnPresenter.scrollUp();
|
||
if (command === "scrollDown") window.__qlearnPresenter.scrollDown();
|
||
if (command === "scrollBy") window.__qlearnPresenter.scrollBy(message.value);
|
||
if (command === "resetView") window.__qlearnPresenter.resetView();
|
||
if (command === "goTo") window.__qlearnPresenter.goTo(message.value);
|
||
if (command === "reportState") window.__qlearnPresenter.reportState();
|
||
if (command === "exit") window.__qlearnPresenter.exit(false);
|
||
}
|
||
});
|
||
})();
|