From 78dd9ebe039c5d251f418ca480093c1540b0ae9a Mon Sep 17 00:00:00 2001 From: data Date: Mon, 16 Mar 2026 11:15:31 +0100 Subject: [PATCH] fix(tv): Alphabet-Sidebar per D-Pad/Fernbedienung navigierbar Co-Authored-By: Claude Opus 4.6 --- video-konverter/app/static/tv/js/tv.js | 72 ++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/video-konverter/app/static/tv/js/tv.js b/video-konverter/app/static/tv/js/tv.js index f608c4b..a0e699e 100644 --- a/video-konverter/app/static/tv/js/tv.js +++ b/video-konverter/app/static/tv/js/tv.js @@ -36,9 +36,9 @@ class FocusManager { el => el.classList.remove("input-editing")); } 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")) { + 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; } } @@ -229,6 +229,70 @@ class FocusManager { } } + // ===== 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; + } + } + } + } + // Naechstes Element in Richtung finden (Nearest-Neighbor) const currentRect = active.getBoundingClientRect(); const cx = currentRect.left + currentRect.width / 2; @@ -240,7 +304,7 @@ class FocusManager { // Nur Elemente im gleichen Bereich (Nav oder Content) bevorzugen const searchEls = inNav ? focusables.filter(el => el.closest("#tv-nav")) - : focusables.filter(el => !el.closest("#tv-nav")); + : focusables.filter(el => !el.closest("#tv-nav") && !el.closest(".tv-alpha-sidebar")); for (const el of searchEls) { if (el === active) continue;