Initial commit: RSS feed viewer with Docker setup
This commit is contained in:
155
static/app.js
Normal file
155
static/app.js
Normal file
@@ -0,0 +1,155 @@
|
||||
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.';
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user