From 80292d3351858c489c5c3a2c8734c465e139d4c6 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 14 Sep 2026 16:36:43 +0200 Subject: [PATCH] =?UTF-8?q?Der=20Umschalter=20geh=C3=B6rt=20in=20die=20Lei?= =?UTF-8?q?ste,=20nicht=20in=20die=20Einstellungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wie auf dem iPad: ein Knopf in der Leiste selbst — an der Seite oben in der Ecke, oben ganz links. Ihn in die Einstellungen zu verbannen hieß, für einen Handgriff zwei Ebenen tief zu gehen. Dazu die Einstellungen, die bisher aus vier Zeilen bestanden: Aufmacher (Quelle, Anzahl, Höhe), Reihen (Weiterschauen zusammenlegen, Gesehenes ausblenden, Plakate oder Querformat), Kachelgröße, Akzentfarbe, Server. Sie wirken auch — die Startseite baut sich danach auf. Co-Authored-By: Claude Opus 5 --- web/frontend/index.html | 145 ++++++++++++++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 20 deletions(-) diff --git a/web/frontend/index.html b/web/frontend/index.html index 9f8d2c6..59720c0 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -124,11 +124,22 @@ img { display: block; } .island button.active { background: var(--elevated); color: var(--accent); } .island .sep { width: 1px; height: 22px; background: var(--separator); margin: 0 var(--s-tight); } +/* Der Umschalter zwischen oben und seitlich — wie auf dem iPad ein Knopf in + der Leiste selbst. Ihn in die Einstellungen zu verbannen hieße, für einen + Handgriff zwei Ebenen tief zu gehen. */ +.island .toggle-nav { + padding: var(--s-snug); color: var(--text-2); display: flex; align-items: center; + border-radius: var(--r-control); flex: 0 0 auto; +} +.island .toggle-nav:hover { color: var(--text); background: var(--elevated); } +.island .toggle-nav svg { width: 18px; height: 18px; display: block; } +#app.side .island .toggle-nav { align-self: flex-end; margin: 0 var(--s-snug) var(--s-snug); } + /* ---- Aufmacher ----------------------------------------------------------- Randlos bis an den oberen Rand, die Insel schwebt darüber. Genau die Entscheidung, die in der App getroffen wurde. */ #hero { - position: relative; height: 82vh; min-height: 420px; overflow: hidden; + position: relative; height: var(--hero-h, 82vh); min-height: 380px; overflow: hidden; } #hero .shot { position: absolute; inset: 0; background-size: cover; background-position: center; @@ -278,7 +289,12 @@ img { display: block; } .seg button.on { background: var(--accent); color: #fff; } input[type=range] { width: 100%; accent-color: var(--accent); } .range-row { display: flex; align-items: center; gap: var(--s-regular); } -.range-row output { min-width: 56px; text-align: right; color: var(--text-2); font-size: 13px; } +.range-row output { min-width: 82px; text-align: right; color: var(--text-2); font-size: 13px; } +.check { + display: flex; align-items: center; gap: var(--s-snug); + padding: var(--s-snug) 0; font-size: 14px; color: var(--text-2); cursor: pointer; +} +.check input { accent-color: var(--accent); width: 17px; height: 17px; } /* ---- Zustände ------------------------------------------------------------ */ #content { padding-bottom: var(--s-section); } @@ -444,14 +460,22 @@ const PALETTE = [ const P = { accent: localStorage.getItem('flixy.accent') || '#C04AC9', - nav: localStorage.getItem('flixy.nav') || 'top', // 'top' | 'side' + nav: localStorage.getItem('flixy.nav') || 'top', // 'top' | 'side' cardW: +(localStorage.getItem('flixy.cardW') || 150), + // Startseite — dieselben Stellschrauben wie in der App. + heroSource: localStorage.getItem('flixy.heroSource') || 'continueFirst', + heroCount: +(localStorage.getItem('flixy.heroCount') || 5), + heroHeight: +(localStorage.getItem('flixy.heroHeight') || 82), + combineContinue: localStorage.getItem('flixy.combineContinue') !== 'false', + hideWatched: localStorage.getItem('flixy.hideWatched') === 'true', + rowLayout: localStorage.getItem('flixy.rowLayout') || 'poster', // 'poster' | 'wide' }; function applyPrefs() { const r = document.documentElement.style; r.setProperty('--accent', P.accent); r.setProperty('--card-w', P.cardW + 'px'); + r.setProperty('--hero-h', P.heroHeight + 'vh'); el('app').classList.toggle('side', P.nav === 'side'); // An der Seite fährt nichts weg — die Regel gilt nur für die Insel oben. if (P.nav === 'side') el('chrome').classList.remove('away'); @@ -498,6 +522,24 @@ function buildIsland() { isl.appendChild(d); }; + // Der Umschalter zuerst, wie auf dem iPad: an der Seite oben in der Ecke, + // oben ganz links vor den Einträgen. + const nav = document.createElement('button'); + nav.className = 'toggle-nav'; + nav.title = P.nav === 'side' ? 'Leiste nach oben' : 'Leiste an die Seite'; + nav.setAttribute('aria-label', nav.title); + nav.innerHTML = P.nav === 'side' + // Rechteck mit Balken oben — die Leiste zieht nach oben. + ? '' + // Rechteck mit Spalte links — die Leiste zieht an die Seite. + : ''; + nav.onclick = () => { + setPref('nav', P.nav === 'side' ? 'top' : 'side'); + buildIsland(); + go(current); + }; + isl.appendChild(nav); + // Die Marke steht nur in der Spalte — oben nähme sie Platz weg, den die // Einträge brauchen. bit('brand', 'Flixy'); @@ -540,12 +582,33 @@ function renderSettings() {
-

Navigation

-

Oben als schwebende Insel oder links als Spalte. Oben blendet sie - beim Scrollen aus, an der Seite bleibt sie stehen.

-
- - +

Aufmacher

+

Woher die Titel oben kommen. Die Auswahl wechselt alle sechs Stunden; + Titel ohne Hintergrundbild ziehen dabei ein schlechteres Los.

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Reihen

+

Wie die Startseite darunter aufgebaut ist.

+ + +
+ +
@@ -574,10 +637,32 @@ function renderSettings() { sw.appendChild(b); }); - [...el('set-nav').children].forEach((b) => { - b.classList.toggle('on', b.dataset.v === P.nav); - b.onclick = () => { setPref('nav', b.dataset.v); renderSettings(); }; - }); + // Segmentierte Schalter, die alle gleich funktionieren. + const segment = (id, key, after) => { + [...el(id).children].forEach((b) => { + b.classList.toggle('on', b.dataset.v === String(P[key])); + b.onclick = () => { setPref(key, b.dataset.v); renderSettings(); after?.(); }; + }); + }; + segment('set-herosource', 'heroSource'); + segment('set-rowlayout', 'rowLayout'); + + // Regler, die alle gleich funktionieren. + const slider = (id, key, unit) => { + const r = el(id), o = el(id + '-out'); + r.value = P[key]; o.textContent = P[key] + unit; + r.oninput = () => { o.textContent = r.value + unit; setPref(key, +r.value); }; + }; + slider('set-herocount', 'heroCount', ' Titel'); + slider('set-heroheight', 'heroHeight', ' % Höhe'); + + const check = (id, key) => { + const c = el(id); + c.checked = P[key]; + c.onchange = () => setPref(key, c.checked); + }; + check('set-combine', 'combineContinue'); + check('set-hidewatched', 'hideWatched'); const z = el('set-zoom'), zo = el('set-zoom-out'); z.value = P.cardW; zo.textContent = P.cardW + ' px'; @@ -604,13 +689,33 @@ async function renderHome() { ...views.map((v) => get(`/Users/${S.userId}/Items/Latest`, { ParentId: v.Id })), ]); - const rows = [ - { title: 'Weiterschauen', items: resume, wide: true }, - { title: 'Als Nächstes', items: next, wide: true }, - ...views.map((v, i) => ({ title: `Neu in ${v.Name}`, items: latest[i] || [] })), - ].filter((r) => r.items.length); + // „Weiterschauen" und „Als Nächstes" sind zwei Fragen — oder eine, je + // nachdem, wie man es eingestellt hat. + let rows; + 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 }]; + } else { + rows = [ + { title: 'Weiterschauen', items: resume, wide: true }, + { title: 'Als Nächstes', items: next, wide: true }, + ]; + } - const featured = pickFeatured([...resume, ...next, ...latest.flat()]); + rows.push(...views.map((v, i) => ({ + title: `Neu in ${v.Name}`, + // Gesehenes ausblenden gilt nur für Neuzugänge: in „Weiterschauen" wäre + // es sinnlos, dort ist alles angefangen. + items: (latest[i] || []).filter((it) => !P.hideWatched || !it.UserData?.Played), + wide: P.rowLayout === 'wide', + }))); + rows = rows.filter((r) => r.items.length); + + const heroPool = P.heroSource === 'latestOnly' + ? latest.flat() + : [...resume, ...next, ...latest.flat()]; + const featured = pickFeatured(heroPool); c.innerHTML = ''; if (featured.length) c.appendChild(buildHero(featured)); @@ -642,7 +747,7 @@ function pickFeatured(items) { // es wenige, hat er seine Chance. .map((it) => ({ it, key: rnd() * (hasImg(it, 'Backdrop') ? 1 : 0.2) })) .sort((a, b) => b.key - a.key) - .slice(0, 5) + .slice(0, P.heroCount) .map((x) => x.it); }