// Client-side PowerPoint renderer for the Slides tab. // Renders an uploaded .pptx straight to HTML in the browser using PPTXjs — so // the slides show on ANY device with no PowerPoint installed and without // depending on Microsoft's Office Online viewer. If the client-side render // can't run (old browser, odd file), it falls back to the Office viewer, and // finally to a download link — so something always works. (function(){ // ---- load PPTXjs + deps once, lazily ---- let depsPromise = null; function loadScript(src){ return new Promise((res, rej)=>{ const s = document.createElement('script'); s.src = src; s.async = false; s.onload = res; s.onerror = ()=>rej(new Error('load '+src)); document.head.appendChild(s); }); } function loadCss(href){ return new Promise((res)=>{ const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href; l.onload = res; l.onerror = res; document.head.appendChild(l); }); } function loadPptxDeps(){ if(depsPromise) return depsPromise; const V = 'https://cdn.jsdelivr.net/gh/meshesha/PPTXjs@v1.21.1'; depsPromise = (async ()=>{ if(!window.jQuery) await loadScript('https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js'); if(!window.JSZip) await loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js'); if(!window.JSZipUtils) await loadScript('https://cdn.jsdelivr.net/npm/jszip-utils@0.1.0/dist/jszip-utils.min.js'); await loadScript(V + '/js/filereader.js'); await loadCss(V + '/css/pptxjs.css'); await loadScript(V + '/js/pptxjs.js'); await loadScript(V + '/js/divs2slides.js'); if(!(window.jQuery && window.jQuery.fn && window.jQuery.fn.pptxToHtml)) throw new Error('PPTXjs unavailable'); return window.jQuery; })().catch(err=>{ depsPromise = null; throw err; }); return depsPromise; } const { useState, useRef, useEffect, useLayoutEffect } = React; function PptxDeck({ url, index, onCount, deckName }){ const hostRef = useRef(null); // PPTXjs renders here (offscreen-ish) const wrapRef = useRef(null); // visible scaling wrapper const slidesRef = useRef([]); const [phase, setPhase] = useState('loading'); // loading | ready | office | error const [forceOffice, setForceOffice] = useState(false); const [n, setN] = useState(0); // render the deck once per URL useEffect(()=>{ let cancelled = false; setPhase('loading'); slidesRef.current = []; setN(0); (async ()=>{ let $; try { $ = await loadPptxDeps(); } catch(e){ if(!cancelled) setPhase('office'); return; } // deps blocked → Office viewer try { const host = hostRef.current; if(!host) return; host.innerHTML = ''; const id = 'pptx_' + Math.random().toString(36).slice(2); host.id = id; // PPTXjs (v1.21.1) fetches the URL itself; the file is same-origin on // the church's own domain, so no PowerPoint or cloud service is needed. $('#'+id).pptxToHtml({ pptxFileUrl: url, slideMode:false, keyBoardShortCut:false, mediaProcess:true }); // PPTXjs is async with no callback — poll for the rendered slides const started = Date.now(); const poll = setInterval(()=>{ if(cancelled){ clearInterval(poll); return; } const sl = host.querySelectorAll('.slide'); if(sl.length){ clearInterval(poll); slidesRef.current = Array.prototype.slice.call(sl); setN(sl.length); onCount && onCount(sl.length); setPhase('ready'); } else if(Date.now()-started > 8000){ clearInterval(poll); setPhase('office'); // took too long → Office viewer } }, 150); } catch(e){ if(!cancelled) setPhase('office'); } })(); return ()=>{ cancelled = true; }; }, [url]); // show only the active slide and scale it to fit the stage const fit = ()=>{ const wrap = wrapRef.current, slides = slidesRef.current; if(!wrap || !slides.length) return; const cur = slides[Math.min(index, slides.length-1)]; slides.forEach((el,k)=>{ el.style.display = (k===Math.min(index,slides.length-1))?'block':'none'; }); if(!cur) return; const sw = cur.offsetWidth || 960, sh = cur.offsetHeight || 540; const scale = Math.min(wrap.clientWidth / sw, wrap.clientHeight / sh); const host = hostRef.current; host.style.transformOrigin = 'top left'; host.style.transform = 'scale(' + scale + ')'; host.style.width = sw + 'px'; host.style.height = sh + 'px'; host.style.left = ((wrap.clientWidth - sw*scale)/2) + 'px'; host.style.top = ((wrap.clientHeight - sh*scale)/2) + 'px'; }; useLayoutEffect(()=>{ if(phase==='ready') fit(); }, [index, phase, n]); useEffect(()=>{ if(phase!=='ready') return; window.addEventListener('resize', fit); let ro = null; if(window.ResizeObserver && wrapRef.current){ ro = new ResizeObserver(fit); ro.observe(wrapRef.current); } return ()=>{ window.removeEventListener('resize', fit); if(ro) ro.disconnect(); }; }, [phase, n]); if(phase === 'office' || forceOffice){ return (
); } return (