From b52c53fab05367ff62cdad60eae30dbb4cc0a174 Mon Sep 17 00:00:00 2001 From: Homelab Cursor Date: Fri, 20 Mar 2026 19:44:26 +0100 Subject: [PATCH] feat: Dashboard komplett neu - weisse Schrift, grosse Font, Nav-Links, Film-Info - Hintergrund: #1a1a2e (dunkles Blau statt fast-schwarz) - Text: #ffffff (weiss statt grau) - Muted: #b8b8d0 (hell genug zum Lesen) - Minimum Font: 14px, Body: 17px, Titel: 19px - Nav-Links: Downloads + Status im Header - Film-Info: Jahr/Land/Genre per Wikidata (lazy load) - Kein Monospace mehr, system-ui Sans-Serif - ACHTUNG: Extra-Routes Import-Block NICHT LOESCHEN --- homelab-ai-bot/savetv_web.py | 343 ++++++++++++++++++++--------------- 1 file changed, 196 insertions(+), 147 deletions(-) diff --git a/homelab-ai-bot/savetv_web.py b/homelab-ai-bot/savetv_web.py index 0aab0f70..bfb6b5c6 100644 --- a/homelab-ai-bot/savetv_web.py +++ b/homelab-ai-bot/savetv_web.py @@ -45,32 +45,29 @@ HTML = r""" Save.TV Archiv
- +
+ + +
Lade...
@@ -301,29 +316,29 @@ HTML = r"""
-
0 ausgewählt
- - - +
0 ausgewählt
+ + +
@@ -343,6 +358,38 @@ let allFilms = []; let selected = new Set(); let downloads = {}; let currentFilter = 'all'; +let filmInfoCache = {}; +let infoQueue = []; +let infoLoading = false; + +function queueInfoLoad(films) { + films.forEach(f => { + if (!filmInfoCache[f.tid] && !infoQueue.some(q => q.tid === f.tid)) { + infoQueue.push({tid: f.tid, title: f.title}); + } + }); + if (!infoLoading) processInfoQueue(); +} + +async function processInfoQueue() { + if (!infoQueue.length) { infoLoading = false; return; } + infoLoading = true; + const item = infoQueue.shift(); + try { + const r = await fetch('/api/filminfo?title=' + encodeURIComponent(item.title)); + const d = await r.json(); + let parts = []; + if (d.year) parts.push(d.year); + if (d.countries && d.countries.length) parts.push(d.countries.join('/')); + if (d.genres && d.genres.length) parts.push(d.genres.join(', ')); + if (parts.length) { + filmInfoCache[item.tid] = parts.join(' \u00b7 '); + const el = document.getElementById('info-' + item.tid); + if (el) { el.textContent = filmInfoCache[item.tid]; el.classList.add('loaded'); } + } + } catch(e) {} + setTimeout(processInfoQueue, 200); +} async function loadFilms() { const resp = await fetch('/api/films'); @@ -352,80 +399,85 @@ async function loadFilms() { document.getElementById('loading').style.display = 'none'; document.getElementById('content').style.display = 'block'; document.getElementById('header-stats').textContent = - `${data.total} Aufnahmen · ${data.kino} Kino · ${data.urgent} dringend`; + data.total + ' Aufnahmen \u00b7 ' + data.kino + ' Kino \u00b7 ' + data.urgent + ' dringend'; renderFilms(); } function setFilter(f) { currentFilter = f; - document.querySelectorAll('.filter-btn').forEach((b, i) => { + document.querySelectorAll('.filter-btn').forEach(function(b, i) { b.classList.toggle('active', ['all','urgent','kino','tv'][i] === f); }); renderFilms(); } function renderFilms() { - const q = document.getElementById('search').value.toLowerCase(); - const films = allFilms.filter(f => { - if (q && !f.title.toLowerCase().includes(q)) return false; + var q = document.getElementById('search').value.toLowerCase(); + var films = allFilms.filter(function(f) { + if (q && f.title.toLowerCase().indexOf(q) === -1) return false; if (currentFilter === 'urgent') return f.days_left <= 7; if (currentFilter === 'kino') return f.cinema && f.days_left > 7; if (currentFilter === 'tv') return !f.cinema; return true; }); - const urgent = films.filter(f => f.days_left <= 7); - const kino = films.filter(f => f.cinema && f.days_left > 7); - const tv = films.filter(f => !f.cinema && f.days_left > 7); + var urgent = films.filter(function(f) { return f.days_left <= 7; }); + var kino = films.filter(function(f) { return f.cinema && f.days_left > 7; }); + var tv = films.filter(function(f) { return !f.cinema && f.days_left > 7; }); document.getElementById('urgent-count').textContent = urgent.length; document.getElementById('kino-count').textContent = kino.length; document.getElementById('tv-count').textContent = tv.length; function renderGrid(arr, gridId, sectionId) { - const el = document.getElementById(sectionId); - const grid = document.getElementById(gridId); + var el = document.getElementById(sectionId); + var grid = document.getElementById(gridId); if (!arr.length) { el.style.display = 'none'; return; } el.style.display = 'block'; - grid.innerHTML = arr.map(f => filmCard(f)).join(''); + grid.innerHTML = arr.map(function(f) { return filmCard(f); }).join(''); } renderGrid(urgent, 'urgent-grid', 'urgent-section'); renderGrid(kino, 'kino-grid', 'kino-section'); renderGrid(tv, 'tv-grid', 'tv-section'); + queueInfoLoad(allFilms); } function filmCard(f) { - const sel = selected.has(f.tid); - const dlState = downloads[f.tid]; - const daysClass = f.days_left <= 3 ? 'days-urgent' : 'days-ok'; - const daysLabel = f.days_left === 0 ? 'heute' : f.days_left === 1 ? '1 Tag' : `${f.days_left} Tage`; + var sel = selected.has(f.tid); + var dlState = downloads[f.tid]; + var daysClass = f.days_left <= 3 ? 'days-urgent' : 'days-ok'; + var daysLabel = f.days_left === 0 ? 'heute' : f.days_left === 1 ? '1 Tag' : f.days_left + ' Tage'; - let cardClass = 'film-card'; + var cardClass = 'film-card'; if (sel) cardClass += ' selected'; if (dlState === 'done') cardClass += ' downloaded'; if (dlState === 'running') cardClass += ' downloading'; - let statusEl = `
${sel ? '✓' : ''}
`; - let extraLine = ''; + var statusEl = '
' + (sel ? '\u2713' : '') + '
'; + var extraLine = ''; if (dlState === 'running') { - statusEl = `
⬇
`; - extraLine = `
Download läuft...
`; + statusEl = '
\u2b07
'; + extraLine = '
Download l\u00e4uft...
'; } else if (dlState === 'done') { - statusEl = `
✅
`; - extraLine = `
Gespeichert
`; + statusEl = '
\u2705
'; + extraLine = '
Gespeichert
'; } - return `
- ${statusEl} -
${f.title}
-
- ${f.station} - ${f.cinema ? 'Kino' : 'TV'} - ${daysLabel} -
- ${extraLine} -
`; + var infoText = filmInfoCache[f.tid] || ''; + var infoClass = infoText ? 'film-info loaded' : 'film-info'; + + return '
' + + statusEl + + '
' + f.title + '
' + + '
' + + '' + f.station + '' + + '' + (f.cinema ? 'Kino' : 'TV') + '' + + '' + daysLabel + '' + + '
' + + '
' + infoText + '
' + + extraLine + + '
'; } function toggleFilm(tid) { @@ -442,7 +494,7 @@ function updateToolbar() { } function selectUrgent() { - allFilms.filter(f => f.days_left <= 7 && downloads[f.tid] !== 'done').forEach(f => selected.add(f.tid)); + allFilms.filter(function(f) { return f.days_left <= 7 && downloads[f.tid] !== 'done'; }).forEach(function(f) { selected.add(f.tid); }); updateToolbar(); renderFilms(); } @@ -454,38 +506,34 @@ function clearSelection() { } async function startDownloads() { - const tids = Array.from(selected); + var tids = Array.from(selected); if (!tids.length) return; document.getElementById('dl-btn').disabled = true; - - showToast(`Starte ${tids.length} Download(s)...`); - - tids.forEach(tid => { downloads[tid] = 'running'; }); + showToast('Starte ' + tids.length + ' Download(s)...'); + tids.forEach(function(tid) { downloads[tid] = 'running'; }); selected.clear(); updateToolbar(); renderFilms(); - const resp = await fetch('/api/download', { + var resp = await fetch('/api/download', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({tids}) + body: JSON.stringify({tids: tids}) }); - const result = await resp.json(); - - result.results.forEach(r => { + var result = await resp.json(); + result.results.forEach(function(r) { downloads[r.tid] = r.ok ? 'done' : 'error'; }); renderFilms(); - - const ok = result.results.filter(r => r.ok).length; - showToast(`✅ ${ok}/${tids.length} Downloads gestartet`); + var ok = result.results.filter(function(r) { return r.ok; }).length; + showToast('\u2705 ' + ok + '/' + tids.length + ' Downloads gestartet'); } function showToast(msg) { - const t = document.getElementById('toast'); + var t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); - setTimeout(() => t.classList.remove('show'), 3500); + setTimeout(function() { t.classList.remove('show'); }, 3500); } loadFilms(); @@ -494,6 +542,7 @@ loadFilms(); """ + @app.route("/") def index(): return render_template_string(HTML)