Files

156 lines
4.0 KiB
JavaScript

const rssUrl = window.__RSS_URL__;
const els = {
feedTitle: document.getElementById('feedTitle'),
headlineText: document.getElementById('headlineText'),
headlineLink: document.getElementById('headlineLink'),
clock: document.getElementById('clock'),
articleTime: document.getElementById('articleTime'),
progressFill: document.getElementById('progressFill'),
bg: document.getElementById('bg'),
};
let items = [];
let idx = 0;
let timer = null;
let progressTimer = null;
let paused = false;
const SLIDE_MS = 6000;
function fmtClock(d) {
const pad = (n) => String(n).padStart(2, '0');
return `${pad(d.getDate())}-${pad(d.getMonth() + 1)}-${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function tickClock() {
els.clock.textContent = fmtClock(new Date());
}
function resetProgress() {
if (progressTimer) {
cancelAnimationFrame(progressTimer);
progressTimer = null;
}
const start = performance.now();
els.progressFill.style.width = '0%';
const loop = (t) => {
if (paused) return;
const p = Math.min(1, (t - start) / SLIDE_MS);
els.progressFill.style.width = `${(p * 100).toFixed(2)}%`;
if (p < 1) progressTimer = requestAnimationFrame(loop);
};
progressTimer = requestAnimationFrame(loop);
}
function setBackground(item) {
if (!els.bg) return;
const url = item?.enclosure_url;
if (url) {
// Include the same gradient fallbacks as the CSS, but insert the image layer.
// NOTE: No dark overlay on top of the image (requested).
els.bg.style.backgroundImage = [
`url("${url}")`,
'radial-gradient(1200px 600px at 70% 20%, rgba(100, 150, 255, 0.35), transparent 55%)',
'radial-gradient(900px 500px at 20% 70%, rgba(20, 255, 190, 0.16), transparent 55%)',
'linear-gradient(130deg, var(--bg1), var(--bg2))',
].join(', ');
} else {
// Reset to stylesheet default.
els.bg.style.backgroundImage = '';
}
}
function setHeadline(i, animate = true) {
if (!items.length) return;
const item = items[i];
setBackground(item);
const node = els.headlineText;
if (animate) {
node.classList.remove('slide-in');
node.classList.add('slide-out');
setTimeout(() => {
node.textContent = item.title || '(untitled)';
els.headlineLink.href = item.link || '#';
if (els.articleTime) els.articleTime.textContent = item.published || '—';
node.classList.remove('slide-out');
node.classList.add('slide-in');
}, 280);
} else {
node.textContent = item.title || '(untitled)';
els.headlineLink.href = item.link || '#';
node.classList.add('slide-in');
}
resetProgress();
}
function next() {
if (!items.length) return;
idx = (idx + 1) % items.length;
setHeadline(idx);
}
function prev() {
if (!items.length) return;
idx = (idx - 1 + items.length) % items.length;
setHeadline(idx);
}
function startAuto() {
stopAuto();
timer = setInterval(() => {
if (!paused) next();
}, SLIDE_MS);
}
function stopAuto() {
if (timer) clearInterval(timer);
timer = null;
}
function togglePause() {
paused = !paused;
if (!paused) resetProgress();
}
async function loadFeed() {
els.feedTitle.textContent = 'Loading…';
els.headlineText.textContent = 'Loading headlines…';
const apiUrl = `/api/feed?url=${encodeURIComponent(rssUrl)}`;
const r = await fetch(apiUrl, { cache: 'no-store' });
const data = await r.json();
if (!r.ok) throw new Error(data?.error || 'Failed to load feed');
els.feedTitle.textContent = data.title || 'News';
items = (data.items || []).filter((x) => (x.title || '').trim().length > 0).slice(0, 5);
if (!items.length) {
els.headlineText.textContent = 'No items found.';
return;
}
idx = 0;
setHeadline(idx, false);
startAuto();
}
function wireControls() {
// Removed manual navigation controls. This is now a passive slideshow.
}
(async function init() {
tickClock();
setInterval(tickClock, 1000);
wireControls();
try {
await loadFeed();
} catch (e) {
els.feedTitle.textContent = 'Error';
els.headlineText.textContent = e?.message || 'Failed to load.';
}
})();