From 26adabe55c617bbefdb4f221ea0302c4dec37f41 Mon Sep 17 00:00:00 2001 From: data Date: Mon, 16 Mar 2026 10:49:25 +0100 Subject: [PATCH] =?UTF-8?q?docs:=20Implementierungsplan=20f=C3=BCr=20TV-Ap?= =?UTF-8?q?p=20D-Pad=20Fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 Tasks mit exakten Zeilennummern und Code-Snippets: 1. Alphabet-Sidebar nur verfügbare Buchstaben 2. FocusManager Sidebar-Navigation 3. Episoden-Cards vergrößern 4. Gesehen-Button fokussierbar 5. Serie-als-gesehen Button 6. Finaler Test Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-16-tv-dpad-fixes.md | 531 ++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-16-tv-dpad-fixes.md diff --git a/docs/superpowers/plans/2026-03-16-tv-dpad-fixes.md b/docs/superpowers/plans/2026-03-16-tv-dpad-fixes.md new file mode 100644 index 0000000..43d6144 --- /dev/null +++ b/docs/superpowers/plans/2026-03-16-tv-dpad-fixes.md @@ -0,0 +1,531 @@ +# TV-App D-Pad & Usability Fixes - Implementierungsplan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 5 Fernbedienungs-/Usability-Probleme der TV-App auf Samsung Tizen beheben. + +**Architecture:** Reine Frontend-Änderungen an 4 Dateien — JavaScript (FocusManager + Template-Scripts), CSS (Episode-Grid, Mark-Button, neuer Button), HTML (Templates). Kein Backend-Change. Bestehender i18n-Key `status.mark_series` wird wiederverwendet. + +**Tech Stack:** Vanilla JavaScript, CSS3, Jinja2-Templates, aiohttp + +**Spec:** `docs/superpowers/specs/2026-03-16-tv-dpad-fixes-design.md` + +--- + +## Dateiübersicht + +| Datei | Änderung | +|-------|----------| +| `video-konverter/app/static/tv/js/tv.js` | FocusManager: Sidebar-Navigation, focusin-Handler | +| `video-konverter/app/static/tv/css/tv.css` | Episode-Grid größer, Mark-Button sichtbar, neuer Button-Style | +| `video-konverter/app/templates/tv/series.html` | Alphabet-IIFE: entfernen statt dimmen | +| `video-konverter/app/templates/tv/series_detail.html` | Mark-Button fokussierbar, "Serie als gesehen"-Button + JS | + +--- + +## Task 1: Alphabet-Sidebar — nur verfügbare Buchstaben anzeigen + +**Files:** +- Modify: `video-konverter/app/templates/tv/series.html:243-253` + +- [ ] **Step 1: IIFE ändern — `el.remove()` statt `el.classList.add('dimmed')`** + +In `series.html`, Zeile 243-253, das bestehende IIFE anpassen: + +```javascript +// Buchstaben ohne Treffer komplett entfernen (statt nur dimmen) +(function() { + var avail = {}; + document.querySelectorAll('.tv-view-grid [data-letter], .tv-view-list [data-letter], .tv-view-detail [data-letter]').forEach(function(item) { + var raw = item.dataset.letter; + avail[/^[A-Z]$/.test(raw) ? raw : '#'] = true; + }); + document.querySelectorAll('.tv-alpha-letter').forEach(function(el) { + if (!avail[el.dataset.letter]) el.remove(); + }); +})(); +``` + +Konkret: Zeile 251 ändern von: +```javascript + if (!avail[el.dataset.letter]) el.classList.add('dimmed'); +``` +zu: +```javascript + if (!avail[el.dataset.letter]) el.remove(); +``` + +- [ ] **Step 2: Manuell testen** + +Browser öffnen → TV-Serien-Seite → prüfen dass nur Buchstaben angezeigt werden, die tatsächlich Serien haben. + +- [ ] **Step 3: Commit** + +```bash +git add video-konverter/app/templates/tv/series.html +git commit -m "fix(tv): Alphabet-Sidebar zeigt nur verfügbare Buchstaben" +``` + +--- + +## Task 2: FocusManager — Sidebar per D-Pad erreichbar + +**Files:** +- Modify: `video-konverter/app/static/tv/js/tv.js:38-43` (focusin-Handler) +- Modify: `video-konverter/app/static/tv/js/tv.js:230` (nach ArrowUp-Nav-Block, vor Nearest-Neighbor) +- Modify: `video-konverter/app/static/tv/js/tv.js:240-243` (searchEls-Filter) + +**WICHTIG:** Alle 3 Änderungen sind atomar — zusammen einfügen! + +- [ ] **Step 1: focusin-Handler anpassen — Sidebar aus Content-Tracking ausschließen** + +In `tv.js`, Zeile 38-43, die Bedingung erweitern. Aktuell: + +```javascript + if (e.target && e.target.hasAttribute && e.target.hasAttribute("data-focusable")) { + if (!e.target.closest("#tv-nav")) { + // Nur echte Content-Elemente merken (nicht Filter/Controls) + if (e.target.closest(".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list, .tv-episode-grid, .tv-tabs, .tv-detail-actions, .tv-alpha-sidebar, .tv-view-switch, .tv-filter-bar, .tv-season-actions, .profiles-grid")) { + this._lastContentFocus = e.target; + } + } + } +``` + +Ändern zu (`.tv-alpha-sidebar` aus der Content-Areas-Liste entfernen UND Sidebar explizit in äußerer Bedingung ausschließen): + +```javascript + if (e.target && e.target.hasAttribute && e.target.hasAttribute("data-focusable")) { + if (!e.target.closest("#tv-nav") && !e.target.closest(".tv-alpha-sidebar")) { + // Nur echte Content-Elemente merken (nicht Nav/Sidebar) + if (e.target.closest(".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list, .tv-episode-grid, .tv-tabs, .tv-detail-actions, .tv-view-switch, .tv-filter-bar, .tv-season-actions, .profiles-grid")) { + this._lastContentFocus = e.target; + } + } + } +``` + +- [ ] **Step 2: Sidebar-Navigationslogik einfügen — nach ArrowUp-Block (Zeile 230)** + +In `tv.js`, nach Zeile 230 (dem Ende des `if (!inNav && direction === "ArrowUp")` Blocks), VOR dem Nearest-Neighbor-Block (Zeile 232: `const currentRect = ...`), folgenden Code einfügen: + +```javascript + // ===== Alphabet-Sidebar Navigation ===== + const inSidebar = active.closest(".tv-alpha-sidebar"); + + if (inSidebar) { + if (direction === "ArrowLeft") { + // Zurueck zum Content + if (this._lastContentFocus && document.contains(this._lastContentFocus)) { + this._lastContentFocus.focus(); + } else { + const firstCard = document.querySelector(".tv-grid [data-focusable], .tv-card[data-focusable]"); + if (firstCard) firstCard.focus(); + } + e.preventDefault(); + return; + } + if (direction === "ArrowUp" || direction === "ArrowDown") { + // Sequentiell durch Buchstaben + const letters = Array.from(document.querySelectorAll(".tv-alpha-letter[data-focusable]")) + .filter(el => el.offsetHeight > 0); + const idx = letters.indexOf(active); + const next = direction === "ArrowDown" ? idx + 1 : idx - 1; + if (next >= 0 && next < letters.length) { + letters[next].focus(); + letters[next].scrollIntoView({ block: "nearest", behavior: "smooth" }); + } + e.preventDefault(); + return; + } + } + + // ArrowRight am rechten Grid-Rand -> Sidebar + if (!inNav && !inSidebar && direction === "ArrowRight") { + const sidebar = document.getElementById("alpha-sidebar"); + if (sidebar && sidebar.offsetHeight > 0) { + // Pruefen ob es noch ein Element rechts im Content gibt + const contentEls = focusables.filter(el => !el.closest("#tv-nav") && !el.closest(".tv-alpha-sidebar")); + const currentRect_r = active.getBoundingClientRect(); + const rightNeighbor = contentEls.some(el => { + const r = el.getBoundingClientRect(); + return r.left > currentRect_r.right + 5 && Math.abs(r.top - currentRect_r.top) < 100; + }); + if (!rightNeighbor) { + // Kein Content rechts -> zur Sidebar springen + const sidebarLetters = Array.from(sidebar.querySelectorAll("[data-focusable]")) + .filter(el => el.offsetHeight > 0); + if (sidebarLetters.length > 0) { + // Naechstgelegenen Buchstaben vertikal finden + const cy_r = currentRect_r.top + currentRect_r.height / 2; + let best = sidebarLetters[0]; + let bestDist_r = Infinity; + sidebarLetters.forEach(l => { + const lr = l.getBoundingClientRect(); + const d = Math.abs(lr.top + lr.height / 2 - cy_r); + if (d < bestDist_r) { bestDist_r = d; best = l; } + }); + this._lastContentFocus = active; + best.focus(); + e.preventDefault(); + return; + } + } + } + } +``` + +- [ ] **Step 3: Nearest-Neighbor searchEls-Filter erweitern — Sidebar ausschließen** + +In `tv.js`, die Zeilen mit dem `searchEls`-Filter (ca. Zeile 240-243, nach Einfügung verschoben) ändern von: + +```javascript + const searchEls = inNav + ? focusables.filter(el => el.closest("#tv-nav")) + : focusables.filter(el => !el.closest("#tv-nav")); +``` + +zu: + +```javascript + const searchEls = inNav + ? focusables.filter(el => el.closest("#tv-nav")) + : focusables.filter(el => !el.closest("#tv-nav") && !el.closest(".tv-alpha-sidebar")); +``` + +- [ ] **Step 4: Manuell testen** + +Browser/Tizen-Emulator → Serien-Seite: +1. D-Pad rechts am Grid-Rand → Sidebar bekommt Focus +2. D-Pad hoch/runter in Sidebar → sequentiell durch Buchstaben +3. D-Pad links in Sidebar → zurück zur letzten Card +4. Enter in Sidebar → filtert Serien nach Buchstabe + +- [ ] **Step 5: Commit** + +```bash +git add video-konverter/app/static/tv/js/tv.js +git commit -m "fix(tv): Alphabet-Sidebar per D-Pad/Fernbedienung navigierbar" +``` + +--- + +## Task 3: Episoden-Cards vergrößern + Laufzeit-Badge + +**Files:** +- Modify: `video-konverter/app/static/tv/css/tv.css:1817-1821` (Episode-Grid Basis) +- Modify: `video-konverter/app/static/tv/css/tv.css:2019-2020` (1200px Breakpoint) + +- [ ] **Step 1: Basis-Grid vergrößern** + +In `tv.css`, Zeile 1819 ändern von: + +```css + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); +``` + +zu: + +```css + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +``` + +Zeile 1820 ändern von: + +```css + gap: 0.8rem; +``` + +zu: + +```css + gap: 1rem; +``` + +- [ ] **Step 2: 1200px-Breakpoint anpassen** + +In `tv.css`, Zeile 2020 ändern von: + +```css + .tv-episode-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } +``` + +zu: + +```css + .tv-episode-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); } +``` + +- [ ] **Step 3: Laufzeit-Badge anpassen** + +In `tv.css`, die `.tv-ep-duration`-Regel finden (Zeile 452-461) und `white-space: nowrap;` hinzufügen sowie Font/Padding anpassen: + +Aktuell: +```css +.tv-ep-duration { + position: absolute; + bottom: 6px; + right: 6px; + background: rgba(0,0,0,0.75); + color: #fff; + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 3px; +} +``` + +Ändern zu: +```css +.tv-ep-duration { + position: absolute; + bottom: 6px; + right: 6px; + background: rgba(0,0,0,0.75); + color: #fff; + font-size: 0.8rem; + padding: 3px 8px; + border-radius: 3px; + white-space: nowrap; +} +``` + +- [ ] **Step 4: Manuell testen** + +Browser → Serien-Detail-Seite → prüfen dass Episoden-Cards größer sind und die Laufzeit vollständig sichtbar ist. + +- [ ] **Step 5: Commit** + +```bash +git add video-konverter/app/static/tv/css/tv.css +git commit -m "fix(tv): Episoden-Cards vergrößert, Laufzeit-Badge vollständig sichtbar" +``` + +--- + +## Task 4: "Gesehen"-Button auf Episode-Cards fokussierbar machen + +**Files:** +- Modify: `video-konverter/app/templates/tv/series_detail.html:129-132` (HTML) +- Modify: `video-konverter/app/static/tv/css/tv.css:1864-1899` (CSS) + +- [ ] **Step 1: HTML — `tabindex="-1"` entfernen, `data-focusable` hinzufügen** + +In `series_detail.html`, Zeile 129-132 ändern von: + +```html + +``` + +zu: + +```html + +``` + +- [ ] **Step 2: CSS — Mark-Button auf TV sichtbar machen (hover:none Media-Query)** + +In `tv.css`, nach der bestehenden `.tv-ep-tile-mark`-Regel (Zeile 1882, nach `z-index: 2;` und vor der hover/focus-Regel), folgende Media-Query einfügen: + +```css +/* TV-Geraete (kein Hover): Mark-Button immer leicht sichtbar */ +@media (hover: none) { + .tv-ep-tile-mark { + opacity: 0.5; + } +} +``` + +- [ ] **Step 3: CSS — Focus-Ring für Mark-Button** + +Die bestehende Regel in `tv.css`, Zeile 1894-1899 ist bereits vorhanden: + +```css +.tv-ep-tile-mark:hover, .tv-ep-tile-mark:focus { + border-color: var(--accent); + color: var(--accent); + outline: none; + opacity: 1; +} +``` + +Diese enthält `outline: none` — für D-Pad-Navigation brauchen wir stattdessen einen sichtbaren Focus-Ring. Ändern zu: + +```css +.tv-ep-tile-mark:hover, .tv-ep-tile-mark:focus { + border-color: var(--accent); + color: var(--accent); + opacity: 1; +} +.tv-ep-tile-mark:focus { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +``` + +- [ ] **Step 4: Manuell testen** + +Browser → Serien-Detail → D-Pad auf Episode-Card → dann D-Pad hoch/links zum Mark-Button → Enter drückt "Gesehen". Prüfen: +1. Button ist leicht sichtbar (auf TV/touch) +2. Button bekommt Focus-Ring bei D-Pad-Navigation +3. Enter toggled gesehen/ungesehen + +- [ ] **Step 5: Commit** + +```bash +git add video-konverter/app/templates/tv/series_detail.html video-konverter/app/static/tv/css/tv.css +git commit -m "fix(tv): Gesehen-Button per D-Pad fokussierbar, auf TV sichtbar" +``` + +--- + +## Task 5: "Serie als gesehen markieren"-Button + +**Files:** +- Modify: `video-konverter/app/templates/tv/series_detail.html:57-66` (HTML) +- Modify: `video-konverter/app/templates/tv/series_detail.html` (JS am Ende des Script-Blocks) +- Modify: `video-konverter/app/static/tv/css/tv.css` (neues Styling) + +- [ ] **Step 1: HTML — Button im Header-Aktionsbereich einfügen** + +In `series_detail.html`, nach dem Watchlist-Button (Zeile 65, vor `` auf Zeile 66), einfügen: + +```html + + +``` + +- [ ] **Step 2: JavaScript — `markSeriesWatched()` Funktion hinzufügen** + +In `series_detail.html`, im `