diff --git a/wwwroot/js/display.js b/wwwroot/js/display.js new file mode 100644 index 0000000..f1ae937 --- /dev/null +++ b/wwwroot/js/display.js @@ -0,0 +1,104 @@ +// === Sunbeam Display Engine === +(function () { + 'use strict'; + var slug = document.body.dataset.slug; + var transition = document.body.dataset.transition || 'fade'; + var slides = window.playlistData || []; + var currentIndex = -1, slideTimer = null, layerA = document.getElementById('slide-a'), layerB = document.getElementById('slide-b'), activeLayer = 'a', pollInterval = 30000, icsCache = {}; + + 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 renderSlide(slide, layer) { + var bg = slide.backgroundColor || '#0a0a14'; + var bgImg = slide.backgroundImage ? 'url(' + slide.backgroundImage + ')' : 'none'; + var css = slide.customCss || ''; + 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)'); + showNextSlide(); setInterval(pollForUpdates, pollInterval); +})();