From cac3348ec1f0ce41be6fa4babdd6f3ac7220c2aa Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 29 May 2026 18:18:30 +0000 Subject: [PATCH] Fix detail-view focus scope and entry targets --- frontend/src/App.vue | 61 ++++++++++++--- frontend/src/components/SeriesFullView.vue | 8 +- .../src/composables/useKeyboardNavigation.ts | 77 ++++++++++++++++--- 3 files changed, 123 insertions(+), 23 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6537ab1..2d9b637 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -62,7 +62,7 @@
-
+
@@ -152,7 +152,7 @@
-
+
{ void refreshPlayerStatus() void refreshResumePositions() + setActiveNavigationScope(isDetailOpen.value ? "detail" : "browse") document.addEventListener("keydown", handleEscapeKey) window.addEventListener("mediahive:gamepad-action", onGamepadAction as EventListener) }) @@ -759,6 +760,45 @@ function handleActorSearch(actorName: string) { updateSearchQuery(actorName) } +function focusDetailEntryTarget(item: MediaItem): boolean { + const detailPanel = document.querySelector('[data-nav-scope="detail"]') as HTMLElement | null + if (!detailPanel) return false + + let target: HTMLElement | null = null + + if (item.type === "movies") { + // Best release is rendered first and mapped to row 2 / col 0. + target = detailPanel.querySelector( + '[data-nav-release-item="true"][data-nav-row="2"][data-nav-col="0"][data-nav-focusable="true"]', + ) as HTMLElement | null + } else if (item.type === "series") { + // Initial episode tile (first season, first episode) maps to row 2 / col 0. + target = detailPanel.querySelector( + '.episode-tile[data-nav-row="2"][data-nav-col="0"][data-nav-focusable="true"]', + ) as HTMLElement | null + } + + if (target) { + focusElement(target) + return true + } + + const navButtons = Array.from(document.querySelectorAll(".header-nav-item.active")) + const detailsNav = navButtons.find((button) => button.textContent?.trim() === "Details") + if (detailsNav?.hasAttribute("data-nav-focusable")) { + focusElement(detailsNav) + return true + } + + const firstFocusable = detailPanel.querySelector('[data-nav-focusable="true"]') as HTMLElement | null + if (firstFocusable) { + focusElement(firstFocusable) + return true + } + + return false +} + // Convert raw data to MediaItem format function movieToMediaItem(movie: Movie): MediaItem { // Get resolution from first torrent if available @@ -1058,13 +1098,14 @@ function rehydrateSearchResult(result: SearchResultItem): MediaItem { // Watch for detail page entry/exit to manage focus watch(selectedItem, (item, oldItem) => { if (item && !oldItem) { - // Skip auto-focus if we have a specific episode to focus on (from search) - if (item.type === "series" && focusEpisode.value) { - return - } - // Entering detail page - focus Play button (row 2, col 0) after transition - focusAt(2, 0, 150) + // Entering detail page - constrain navigation to detail panel immediately, + // then shift focus into it while the slide transition runs. + setActiveNavigationScope("detail") + setTimeout(() => { + focusDetailEntryTarget(item) + }, 20) } else if (!item && oldItem) { + setActiveNavigationScope("browse") // Leaving detail page (browser back, Escape, etc.) - restore focus to the item card const page = currentView.value === "series" ? "series" : "movies" restoreFocusForPage(page) diff --git a/frontend/src/components/SeriesFullView.vue b/frontend/src/components/SeriesFullView.vue index 182b1fd..e1aed72 100644 --- a/frontend/src/components/SeriesFullView.vue +++ b/frontend/src/components/SeriesFullView.vue @@ -1,5 +1,5 @@