mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
Self-contained examples for the PiP overlay API (POST /api/pip), each with a CSP-safe query-param overlay (external JS), config.example.json, zero runtime deps, an offline test, and a README: - PIP-Announce-Broadcast manual one-shot message to a screen/group - PIP-Weather-Widget Open-Meteo current conditions (keyless) - PIP-Air-Quality Open-Meteo US AQI widget (keyless) - PIP-Crypto-Ticker CoinGecko price strip (keyless) - PIP-News-Ticker scrolling RSS/Atom headlines - PIP-Room-Status-Calendar ICS-driven Available/Busy room sign - PIP-Event-Countdown client-side countdown, auto-clears at zero - PIP-Welcome-Board rotating welcome/birthday cards from CSV - PIP-Fundraiser-Thermometer goal-progress bar from local/URL JSON - PIP-QR-Rotator rotating QR codes, encoded client-side - PIP-Incident-Webhook event-driven: red on firing, clear on resolved Also includes the CAP-AU (NSW RFS) and US NWS/NOAA emergency-alert monitors that push expiry-aware PiP overlays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
2.2 KiB
JavaScript
61 lines
2.2 KiB
JavaScript
// External overlay script — same-origin so the server CSP (scriptSrc 'self') permits it.
|
|
// Reads the headline string from the query and scrolls it right-to-left, seamlessly.
|
|
(function () {
|
|
var q = new URLSearchParams(location.search);
|
|
var text = (q.get('text') || '').trim();
|
|
var label = (q.get('label') || 'NEWS').trim();
|
|
var sep = q.get('sep') || ' • ';
|
|
|
|
document.getElementById('label').textContent = label;
|
|
|
|
var track = document.getElementById('track');
|
|
var viewport = track.parentNode;
|
|
|
|
// Build one "run" of the content (separator-joined headlines). Splitting on the
|
|
// separator lets us colour the dividers without trusting feed markup (textContent only).
|
|
function buildRun(container) {
|
|
var parts = text.length ? text.split(sep) : ['(no headlines)'];
|
|
parts.forEach(function (p, i) {
|
|
if (i > 0) {
|
|
var s = document.createElement('span');
|
|
s.className = 'sep';
|
|
s.textContent = sep;
|
|
container.appendChild(s);
|
|
}
|
|
var span = document.createElement('span');
|
|
span.textContent = p;
|
|
container.appendChild(span);
|
|
});
|
|
}
|
|
|
|
// Two identical runs back-to-back → when the first scrolls fully off, reset by one
|
|
// run width for a seamless loop.
|
|
buildRun(track);
|
|
var gap = document.createElement('span');
|
|
gap.textContent = sep;
|
|
gap.className = 'sep';
|
|
track.appendChild(gap);
|
|
var runWidth = 0;
|
|
|
|
function measureAndStart() {
|
|
runWidth = track.scrollWidth; // width of a single run (+ trailing sep)
|
|
buildRun(track); // append the second copy for the wrap
|
|
var x = viewport.clientWidth; // start just off the right edge
|
|
var speed = 90; // px/sec
|
|
var last = null;
|
|
function frame(ts) {
|
|
if (last == null) last = ts;
|
|
var dt = (ts - last) / 1000; last = ts;
|
|
x -= speed * dt;
|
|
if (x <= -runWidth) x += runWidth; // wrap by exactly one run
|
|
track.style.transform = 'translateX(' + x + 'px)';
|
|
requestAnimationFrame(frame);
|
|
}
|
|
requestAnimationFrame(frame);
|
|
}
|
|
|
|
// Wait a tick so fonts/layout settle before measuring.
|
|
if (document.readyState === 'complete') measureAndStart();
|
|
else window.addEventListener('load', measureAndStart);
|
|
})();
|