(() => { 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 contentRoot = document.querySelector( "#content .user_content, " + ".show-content .user_content, " + ".wiki-page-content .user_content, " + ".user_content" ) || document.querySelector( "[role='main'] .page-content, " + "[role='main'] .content" ) || document.querySelector("#content") || document.body; const pageTitleElement = contentRoot.querySelector("h1") || document.querySelector( "#content h1, .page-title, .wiki-page-title, h1.page-title" ); let pageTitle = pageTitleElement?.textContent?.trim() || ""; if (!pageTitle) { pageTitle = document.title.replace(/\s*:\s*[^:]+$/, "").trim(); } if (!pageTitle) { pageTitle = "Lesson"; } 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(); } } if (!slideDefinitions.length) { alert("No presentable page content was found."); return; } 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 = `
Audience Hidden
`; 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 splitParagraph(paragraph) { const text = paragraph.textContent.trim(); const sentences = text.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [text]; if (sentences.length < 2) return [paragraph.cloneNode(true)]; const midpoint = Math.ceil(sentences.length / 2); return [sentences.slice(0, midpoint), sentences.slice(midpoint)] .filter((part) => part.length) .map((part) => { const copy = paragraph.cloneNode(false); copy.textContent = part.join(" ").trim(); return copy; }); } 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); if (block.matches("p") && block.textContent.trim().length > 220) { return splitParagraph(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 = ` `; 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 `${clone.outerHTML}