'use strict'; let ssIdx = 0; let ssTimer = null; let ssPlaying = true; let ssQueue = SS_ENTRIES.slice(); let wakeLock = null; async function acquireWakeLock() { if (!('wakeLock' in navigator) || document.hidden) return; try { wakeLock = await navigator.wakeLock.request('screen'); } catch (_) {} } function releaseWakeLock() { wakeLock?.release(); wakeLock = null; } const img = document.getElementById('ss-img'); const vid = document.getElementById('ss-video'); const title = document.getElementById('ss-title'); const cap = document.getElementById('ss-caption'); const ctr = document.getElementById('ss-counter'); const btn = document.getElementById('ss-play-btn'); function ssInterval() { return (parseFloat(document.getElementById('ss-interval').value) || 4) * 1000; } function ssShow(i, instant, onShown) { const e = ssQueue[i]; if (!e) return; ssIdx = i; title.textContent = e.title !== e.name ? e.title : ''; cap.textContent = e.caption || ''; ctr.textContent = `${i + 1} / ${ssQueue.length}`; const done = () => { applyEntry(e); onShown?.(); }; if (instant) { applyEntry(e); return; } if (e.type === 'video') { crossFade(done); } else { const preload = new Image(); preload.onload = preload.onerror = () => crossFade(done); preload.src = e.src; } } function applyEntry(e) { if (e.type === 'video') { img.style.display = 'none'; vid.style.display = ''; vid.src = e.src; void vid.offsetWidth; vid.classList.remove('fading'); vid.play().catch(() => {}); } else { vid.pause && vid.pause(); vid.style.display = 'none'; img.style.display = ''; img.src = e.src; img.alt = e.title; void img.offsetWidth; img.classList.remove('fading'); } } function crossFade(cb) { img.classList.add('fading'); vid.classList.add('fading'); setTimeout(cb, 500); } function ssSchedule() { clearTimeout(ssTimer); if (ssPlaying && ssQueue.length > 1) { // Start the next timer only after the photo is actually visible (post-preload // + post-crossfade), so each photo gets a full interval on screen. ssTimer = setTimeout(() => { ssShow((ssIdx + 1) % ssQueue.length, false, ssSchedule); }, ssInterval()); } } function ssNext() { ssShow((ssIdx + 1) % ssQueue.length); if (ssPlaying) ssSchedule(); } function ssPrev() { ssShow((ssIdx - 1 + ssQueue.length) % ssQueue.length); if (ssPlaying) ssSchedule(); } function ssToggle() { ssPlaying = !ssPlaying; btn.textContent = ssPlaying ? '⏸ Pause' : '▶ Play'; if (ssPlaying) { ssSchedule(); acquireWakeLock(); } else { clearTimeout(ssTimer); releaseWakeLock(); } } // ── Shuffle ──────────────────────────────────────────────────────────────────── function shuffle(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } // ── Full screen ──────────────────────────────────────────────────────────────── function ssFsActive() { return !!(document.fullscreenElement || document.webkitFullscreenElement); } function ssFsEnter() { const el = document.documentElement; return (el.requestFullscreen || el.webkitRequestFullscreen).call(el); } function ssFsExit() { return (document.exitFullscreen || document.webkitExitFullscreen).call(document); } function ssFsToggle() { if (ssFsActive()) ssFsExit(); else ssFsEnter().catch(() => {}); } function onFsChange() { const inFs = ssFsActive(); document.body.classList.toggle('ss-fullscreen', inFs); if (inFs) { const hint = document.createElement('div'); hint.textContent = 'Press Esc to exit full screen'; hint.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.75);color:#fff;padding:8px 18px;border-radius:6px;font-size:.9rem;pointer-events:none;transition:opacity 1.2s'; document.body.appendChild(hint); setTimeout(() => { hint.style.opacity = '0'; }, 1800); setTimeout(() => { hint.remove(); }, 3100); } else { if (ssPlaying) ssToggle(); // pause so current photo stays visible } } document.addEventListener('fullscreenchange', onFsChange); document.addEventListener('webkitfullscreenchange', onFsChange); // Pause timer while tab is hidden; reschedule fresh on return so no burst catch-up. // Wake lock is auto-released by the browser on hide; re-acquire on return. document.addEventListener('visibilitychange', () => { if (document.hidden) { clearTimeout(ssTimer); } else if (ssPlaying) { ssSchedule(); acquireWakeLock(); } }); // Click/tap anywhere in the stage → open photo in its album lightbox. // Skip when the video element is the target so its native controls still work. // Read the live src attribute rather than ssIdx, which updates ahead of the // cross-fade and would point to the next photo while the old one is still visible. document.getElementById('ss-stage').addEventListener('click', ev => { if (ev.target === vid || vid.contains(ev.target)) return; const activeSrc = vid.style.display !== 'none' ? vid.getAttribute('src') : img.getAttribute('src'); const e = ssQueue.find(entry => entry.src === activeSrc) || ssQueue[ssIdx]; if (!e) return; const lastSlash = e.file_rel.lastIndexOf('/'); const albumRel = lastSlash >= 0 ? e.file_rel.slice(0, lastSlash) : ''; window.location.href = '/browse/' + (albumRel ? albumRel + '/' : '') + '?photo=' + encodeURIComponent(e.name); }); // ── Keyboard & touch ─────────────────────────────────────────────────────────── document.addEventListener('keydown', e => { if (e.key === 'ArrowRight') ssNext(); if (e.key === 'ArrowLeft') ssPrev(); if (e.key === 'f') ssFsToggle(); if (e.key === ' ') { e.preventDefault(); ssToggle(); } }); let swipeX = null; document.addEventListener('touchstart', e => { swipeX = e.changedTouches[0].clientX; }, { passive: true }); document.addEventListener('touchend', e => { if (swipeX === null) return; const dx = e.changedTouches[0].clientX - swipeX; if (Math.abs(dx) > 50) (dx < 0 ? ssNext : ssPrev)(); swipeX = null; }, { passive: true }); // ── Init ─────────────────────────────────────────────────────────────────────── const ssParams = new URLSearchParams(location.search); if (ssParams.get('shuffle') === '1') { ssQueue = shuffle(SS_ENTRIES); } else { ssQueue = SS_ENTRIES.slice().reverse(); } const ivParam = parseFloat(ssParams.get('interval')); if (ivParam >= 1 && ivParam <= 60) { document.getElementById('ss-interval').value = ivParam; } if (ssQueue.length > 0) { ssShow(0, true); ssSchedule(); acquireWakeLock(); } // Browsers require a live user gesture for requestFullscreen — entering it // automatically on page load is always blocked. We wait for the first tap. if (ssParams.get('fullscreen') === '1') { const overlay = document.createElement('div'); overlay.textContent = 'Click or tap anywhere to enter full screen.'; overlay.style.cssText = [ 'position:fixed;inset:0;z-index:200', 'display:flex;align-items:center;justify-content:center', 'color:rgba(255,255,255,.75);font-size:1.1rem;cursor:pointer', 'background:rgba(0,0,0,.45)', ].join(';'); document.body.appendChild(overlay); overlay.addEventListener('click', () => { overlay.remove(); ssFsEnter().catch(() => {}); }, { once: true }); }