From 14e1fdbe3f001a399bb1205d32b2f5f90e3de8d6 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 14 Sep 2026 17:12:11 +0200 Subject: [PATCH] Web: Reihen ordnen und ausblenden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 des Abgleichs. Dieselbe Rechnung wie Arrangement in der App, samt ihrer wichtigsten Eigenschaft: unbekannte Reihen landen am Ende, statt zu verschwinden. Sie zu verwerfen wäre die einfachere Regel — und der Grund dafür, dass eine neu angelegte Mediathek nie aufgetaucht wäre. Ausgeblendete stehen in den Einstellungen weiter unten in der Liste, durchgestrichen. Ohne das könnte man sie nie wieder einblenden. Knöpfe statt Ziehen: auf dem Zeigegerät ist Ziehen schöner, auf einem Trackpad fummelig und mit der Tastatur gar nicht zu machen. Zwei Pfeile können alle drei. Co-Authored-By: Claude Opus 5 --- web/frontend/index.html | 126 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 4 deletions(-) diff --git a/web/frontend/index.html b/web/frontend/index.html index 2e2a1f2..1695d51 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -306,6 +306,18 @@ img { display: block; } } .hexrow input:focus { outline: none; border-color: var(--accent); } .hint { font-size: 12px; color: var(--text-3); margin: var(--s-snug) 0 0; min-height: 16px; } +.rowline { + display: flex; align-items: center; gap: var(--s-snug); + padding: var(--s-snug) 0; border-bottom: 1px solid var(--separator); +} +.rowline:last-child { border-bottom: none; } +.rowline.off span { color: var(--text-3); text-decoration: line-through; } +.tiny { + width: 30px; height: 30px; border-radius: var(--r-control); + background: var(--elevated); color: var(--text-2); font-size: 14px; padding: 0; +} +.tiny:hover:not(:disabled) { color: var(--text); } +.tiny:disabled { opacity: .3; cursor: default; } .setting { background: var(--surface); border: 1px solid var(--separator); border-radius: var(--r-sheet); padding: var(--s-loose); margin-bottom: var(--s-regular); @@ -517,6 +529,10 @@ const P = { heroHeight: +(localStorage.getItem('flixy.heroHeight') || 82), combineContinue: localStorage.getItem('flixy.combineContinue') !== 'false', hideWatched: localStorage.getItem('flixy.hideWatched') === 'true', + // Welche Reihen ausgeblendet sind und in welcher Reihenfolge der Rest + // steht — dieselbe Aufteilung wie `Arrangement` in der App. + hiddenRows: JSON.parse(localStorage.getItem('flixy.hiddenRows') || '[]'), + rowOrder: JSON.parse(localStorage.getItem('flixy.rowOrder') || '[]'), }; function applyPrefs() { @@ -531,10 +547,31 @@ function applyPrefs() { function setPref(key, value) { P[key] = value; - localStorage.setItem('flixy.' + key, value); + // Listen müssen als JSON abgelegt werden — ein Array landete sonst als + // "a,b,c" im Speicher und käme als Zeichenkette zurück. + localStorage.setItem('flixy.' + key, Array.isArray(value) ? JSON.stringify(value) : value); applyPrefs(); } +/* Wendet Auswahl und Reihenfolge auf die Reihen an. + ---------------------------------------------------------------------------- + Dieselbe Regel wie `Arrangement` in der App, samt ihrer wichtigsten + Eigenschaft: **unbekannte Einträge landen am Ende statt zu verschwinden.** + Sie zu verwerfen wäre die einfachere Regel — und der Grund dafür, dass eine + neu angelegte Mediathek nie aufgetaucht wäre. */ +function applyArrangement(rows) { + const visible = rows.filter((r) => !P.hiddenRows.includes(r.id)); + if (!P.rowOrder.length) return visible; + + const at = new Map(P.rowOrder.map((id, i) => [id, i])); + return visible + .map((row, i) => ({ row, known: at.has(row.id) ? at.get(row.id) : Infinity, i })) + // Bei gleichem Rang gilt die ursprüngliche Folge — sonst springen zwei + // unbekannte Reihen bei jedem Laden gegeneinander. + .sort((a, b) => (a.known - b.known) || (a.i - b.i)) + .map((x) => x.row); +} + /* ---------- Navigation ---------- */ let views = [], current = null; @@ -816,6 +853,13 @@ function renderSettings() { Gesehenes aus den Neuzugängen ausblenden +
+

Reihen der Startseite

+

Welche Reihen erscheinen und in welcher Folge. Was der Server neu + anlegt, kommt ans Ende, statt zu fehlen.

+
+
+

Server

@@ -860,6 +904,8 @@ function renderSettings() { check('set-combine', 'combineContinue'); check('set-hidewatched', 'hideWatched'); + renderRowArrangement(); + const z = el('set-zoom'), zo = el('set-zoom-out'); z.value = P.cardW; zo.textContent = P.cardW + ' px'; z.oninput = () => { zo.textContent = z.value + ' px'; setPref('cardW', +z.value); }; @@ -888,6 +934,11 @@ function renderSettings() { } /* ---------- Startseite ---------- */ + +/// Welche Reihen es gibt — für die Einstellungen. Erst nach dem ersten Laden +/// gefüllt: vorher weiß niemand, welche Mediatheken dieser Server hat. +let knownRows = []; + async function renderHome() { const c = el('content'); c.innerHTML = '
'; @@ -911,15 +962,18 @@ async function renderHome() { if (P.combineContinue) { const seen = new Set(); const merged = [...resume, ...next].filter((i) => !seen.has(i.Id) && seen.add(i.Id)); - rows = [{ title: 'Weiterschauen', items: merged, wide: true }]; + // Die zusammengelegte Reihe behält die Kennung der ersten — sonst wäre + // sie für die gespeicherte Reihenfolge eine neue und stünde wieder unten. + rows = [{ id: 'resume', title: 'Weiterschauen', items: merged, wide: true }]; } else { rows = [ - { title: 'Weiterschauen', items: resume, wide: true }, - { title: 'Als Nächstes', items: next, wide: true }, + { id: 'resume', title: 'Weiterschauen', items: resume, wide: true }, + { id: 'nextup', title: 'Als Nächstes', items: next, wide: true }, ]; } rows.push(...views.map((v, i) => ({ + id: 'latest.' + v.Id, title: `Neu in ${v.Name}`, // Gesehenes ausblenden gilt nur für Neuzugänge: in „Weiterschauen" wäre // es sinnlos, dort ist alles angefangen. @@ -931,6 +985,12 @@ async function renderHome() { }))); rows = rows.filter((r) => r.items.length); + // Was es bei diesem Server überhaupt gibt, merken — **vor** dem Ausblenden. + // Die Einstellungen müssen auch die Reihen anbieten, die gerade versteckt + // sind; sonst könnte man sie nie wieder einblenden. + knownRows = rows.map((r) => ({ id: r.id, title: r.title })); + rows = applyArrangement(rows); + const heroPool = P.heroSource === 'latestOnly' ? latest.flat() : [...resume, ...next, ...latest.flat()]; @@ -1101,6 +1161,64 @@ const cardSub = (it) => ? `S${it.ParentIndexNumber}:E${it.IndexNumber} · ${it.Name}` : (it.ProductionYear || ''); +/* Die Liste der Reihen — sichtbar machen, ausblenden, verschieben. + ---------------------------------------------------------------------------- + Knöpfe statt Ziehen: auf dem Zeigegerät ist Ziehen schöner, auf einem + Laptop-Trackpad mit einer Liste dieser Länge aber fummelig, und mit der + Tastatur gar nicht zu machen. Zwei Pfeile können alle drei. */ +function renderRowArrangement() { + const host = el('set-rows'); + if (!host) return; + + if (!knownRows.length) { + host.innerHTML = '

Erst sichtbar, wenn die Startseite einmal geladen war.

'; + return; + } + + // In der Folge, die gerade gilt — ausgeblendete hinten dran, damit man sie + // wiederfindet. + const ordered = applyArrangement(knownRows) + .concat(knownRows.filter((r) => P.hiddenRows.includes(r.id))); + + host.innerHTML = ''; + ordered.forEach((row, index) => { + const hidden = P.hiddenRows.includes(row.id); + const line = document.createElement('div'); + line.className = 'rowline' + (hidden ? ' off' : ''); + line.innerHTML = ` + + + `; + + line.querySelector('input').onchange = (e) => { + const next = e.target.checked + ? P.hiddenRows.filter((id) => id !== row.id) + : [...P.hiddenRows, row.id]; + setPref('hiddenRows', next); + renderRowArrangement(); + }; + + const move = (by) => { + // Die aktuelle Folge festschreiben, bevor darin verschoben wird: ohne + // gespeicherte Folge gäbe es nichts, worin man tauschen könnte. + const ids = ordered.map((r) => r.id); + const to = index + by; + if (to < 0 || to >= ids.length) return; + [ids[index], ids[to]] = [ids[to], ids[index]]; + setPref('rowOrder', ids); + renderRowArrangement(); + }; + const [up, down] = line.querySelectorAll('button'); + up.onclick = () => move(-1); + down.onclick = () => move(1); + + host.appendChild(line); + }); +} + /* ---------- Mediathek ---------- */ async function renderLibrary(id) { const c = el('content');