// === Sunbeam Display Engine === (function () { 'use strict'; var slug = document.body.dataset.slug; var transition = document.body.dataset.transition || 'fade'; var designW = parseInt(document.body.dataset.width) || 1920; var designH = parseInt(document.body.dataset.height) || 1080; var slides = window.playlistData || []; var currentIndex = -1, slideTimer = null, layerA = document.getElementById('slide-a'), layerB = document.getElementById('slide-b'), activeLayer = 'a', pollInterval = 30000, icsCache = {}; // === Aspect-Ratio Scaling === function scaleDisplay() { var wrapper = document.getElementById('scale-wrapper'); if (!wrapper) return; var winW = window.innerWidth, winH = window.innerHeight; var scaleX = winW / designW, scaleY = winH / designH; var scale = Math.min(scaleX, scaleY); wrapper.style.transform = 'translate(-50%, -50%) scale(' + scale + ')'; } scaleDisplay(); window.addEventListener('resize', scaleDisplay); // === Clock === function updateClock() { var now = new Date(); var timeEl = document.getElementById('clock-time'), dateEl = document.getElementById('clock-date'); if (timeEl) timeEl.textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (dateEl) dateEl.textContent = now.toLocaleDateString([], { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } updateClock(); setInterval(updateClock, 1000); if (!slides || slides.length === 0) { layerA.innerHTML = '

\u{1F431}

No meows in this PURR.
Configure slides in the Scratching Post admin.

'; layerA.classList.add('active'); return; } function buildBgStyle(slide) { var bg = slide.backgroundColor || '#ffffff'; var bgSize = slide.backgroundSize || 'cover'; var style = 'background-color:' + bg + ';'; if (slide.backgroundImage) { style += 'background-image:url(' + slide.backgroundImage + ');'; style += 'background-size:' + bgSize + ';'; style += 'background-position:center;'; style += 'background-repeat:no-repeat;'; } if (slide.customCss) style += slide.customCss; return style; } function renderSlide(slide, layer) { var style = buildBgStyle(slide); if (slide.type === 'content') { layer.innerHTML = '
' + (slide.content || '') + '
'; } else if (slide.type === 'embed') { layer.innerHTML = '
'; } else if (slide.type === 'icscalendar') { layer.innerHTML = '

Upcoming Events

Loading calendar
'; loadIcsEvents(slide.icsSource, layer); } else { layer.innerHTML = '
' + (slide.content || '') + '
'; } } function loadIcsEvents(source, layer) { if (!source) { var g = layer.querySelector('.events-grid'); if (g) g.innerHTML = '

No calendar source.

'; return; } var cached = icsCache[source]; if (cached && (Date.now() - cached.time) < 300000) { renderIcsEvents(cached.events, layer); return; } fetch('/api/parseics?url=' + encodeURIComponent(source)).then(function (r) { return r.json(); }).then(function (data) { var events = data.events || []; icsCache[source] = { events: events, time: Date.now() }; renderIcsEvents(events, layer); }).catch(function () { var g = layer.querySelector('.events-grid'); if (g) g.innerHTML = '

Could not load calendar.

'; }); } function renderIcsEvents(events, layer) { var grid = layer.querySelector('.events-grid'); if (!grid) return; if (events.length === 0) { grid.innerHTML = '

No upcoming events.

'; return; } var html = ''; events.forEach(function (e) { html += '
' + escapeHtml(e.summary || 'Event') + '
'; if (e.start) { var d = new Date(e.start); var timeStr = e.allDay ? 'All Day' : formatEventTime(e.start, e.end); html += '
' + d.toLocaleDateString([], { weekday: 'short', day: 'numeric', month: 'short' }) + ' \u00B7 ' + timeStr + '
'; } if (e.location) html += '
\u{1F4CD} ' + escapeHtml(e.location) + '
'; html += '
'; }); grid.innerHTML = html; } function formatEventTime(start, end) { var s = new Date(start), result = s.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (end) { var e = new Date(end); result += ' \u2013 ' + e.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } return result; } function showNextSlide() { currentIndex = (currentIndex + 1) % slides.length; var slide = slides[currentIndex], incoming, outgoing; if (activeLayer === 'a') { incoming = layerB; outgoing = layerA; activeLayer = 'b'; } else { incoming = layerA; outgoing = layerB; activeLayer = 'a'; } renderSlide(slide, incoming); if (transition === 'slide') outgoing.classList.add('slide-out'); incoming.classList.add('active'); outgoing.classList.remove('active'); setTimeout(function () { outgoing.classList.remove('slide-out'); outgoing.innerHTML = ''; }, 1200); startProgress(slide.duration); clearTimeout(slideTimer); slideTimer = setTimeout(showNextSlide, slide.duration * 1000); } function startProgress(duration) { var fill = document.getElementById('progress-fill'); if (!fill) return; fill.style.transition = 'none'; fill.style.width = '0%'; fill.offsetHeight; fill.style.transition = 'width ' + duration + 's linear'; fill.style.width = '100%'; } function pollForUpdates() { fetch('/api/playlist/' + slug).then(function (r) { if (!r.ok) throw new Error('Not found'); return r.json(); }).then(function (data) { document.getElementById('status-dot').classList.remove('error'); var newSlides = data.slides, changed = newSlides.length !== slides.length; if (!changed) { for (var i = 0; i < slides.length; i++) { if (slides[i].id !== newSlides[i].id || slides[i].duration !== newSlides[i].duration || slides[i].updatedAt !== newSlides[i].updatedAt) { changed = true; break; } } } if (changed) { console.log('[Sunbeam] Playlist updated'); slides = newSlides; icsCache = {}; if (slides.length === 0) { clearTimeout(slideTimer); layerA.innerHTML = '

\u{1F431}

No meows in this PURR.

'; layerA.classList.add('active'); layerB.classList.remove('active'); return; } if (currentIndex >= slides.length) currentIndex = -1; } }).catch(function (err) { document.getElementById('status-dot').classList.add('error'); console.warn('[Sunbeam] Poll failed:', err); }); } function escapeHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; } document.addEventListener('keydown', function (e) { if (e.key === 'ArrowRight' || e.key === ' ') { clearTimeout(slideTimer); showNextSlide(); } if (e.key === 'ArrowLeft') { currentIndex = Math.max(-1, currentIndex - 2); clearTimeout(slideTimer); showNextSlide(); } if (e.key === 'f' || e.key === 'F11') { e.preventDefault(); if (!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); } if (e.key === 'c') { document.body.style.cursor = document.body.style.cursor === 'default' ? 'none' : 'default'; } }); console.log('[Sunbeam] Display engine starting for "' + slug + '" with ' + slides.length + ' slide(s) at ' + designW + 'x' + designH); showNextSlide(); setInterval(pollForUpdates, pollInterval); })();