Files

132 lines
8.3 KiB
JavaScript

// === 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 = '<div class="no-slides"><h1>\u{1F431}</h1><p>No meows in this PURR.<br>Configure slides in the Scratching Post admin.</p></div>';
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 = '<div class="slide-inner" style="' + style + '"><div class="content-wrap">' + (slide.content || '') + '</div></div>';
} else if (slide.type === 'embed') {
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><iframe class="embed-frame" src="' + escapeHtml(slide.embedUrl || '') + '" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>';
} else if (slide.type === 'icscalendar') {
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><div class="ics-display"><h2>Upcoming Events</h2><div class="events-grid"><div class="loading-state">Loading calendar</div></div></div></div>';
loadIcsEvents(slide.icsSource, layer);
} else {
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><div class="content-wrap">' + (slide.content || '') + '</div></div>';
}
}
function loadIcsEvents(source, layer) {
if (!source) { var g = layer.querySelector('.events-grid'); if (g) g.innerHTML = '<p style="opacity:0.5;">No calendar source.</p>'; 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 = '<p style="opacity:0.5;">Could not load calendar.</p>'; });
}
function renderIcsEvents(events, layer) {
var grid = layer.querySelector('.events-grid'); if (!grid) return;
if (events.length === 0) { grid.innerHTML = '<p style="opacity:0.5;font-size:1.2em;">No upcoming events.</p>'; return; }
var html = '';
events.forEach(function (e) {
html += '<div class="event-card"><div class="event-title">' + escapeHtml(e.summary || 'Event') + '</div>';
if (e.start) { var d = new Date(e.start); var timeStr = e.allDay ? 'All Day' : formatEventTime(e.start, e.end); html += '<div class="event-time">' + d.toLocaleDateString([], { weekday: 'short', day: 'numeric', month: 'short' }) + ' \u00B7 ' + timeStr + '</div>'; }
if (e.location) html += '<div class="event-location">\u{1F4CD} ' + escapeHtml(e.location) + '</div>';
html += '</div>';
});
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 = '<div class="no-slides"><h1>\u{1F431}</h1><p>No meows in this PURR.</p></div>'; 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);
})();