Improve search-detail back navigation and focus restore
This commit is contained in:
+63
-14
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<!-- Persistent Header overlay - single instance -->
|
<!-- Persistent Header overlay - single instance -->
|
||||||
<Header
|
<Header
|
||||||
:current-view="currentView"
|
:current-view="headerCurrentView"
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
:mpc-be-connected="mpcBeConnected"
|
:mpc-be-connected="mpcBeConnected"
|
||||||
:nav-row="1"
|
:nav-row="1"
|
||||||
@@ -426,6 +426,25 @@ function restoreBrowseFocus(path: string) {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSearchExitTargetFromFocusedCard(): { path: "/movies" | "/series"; itemId: string } | null {
|
||||||
|
const active = document.activeElement as HTMLElement | null
|
||||||
|
const focusedCard = active?.closest("[data-item-id]") as HTMLElement | null
|
||||||
|
if (!focusedCard) return null
|
||||||
|
|
||||||
|
const itemId = focusedCard.getAttribute("data-item-id")
|
||||||
|
const itemType = focusedCard.getAttribute("data-item-type")
|
||||||
|
if (!itemId || !itemType) return null
|
||||||
|
|
||||||
|
if (itemType === "movies") {
|
||||||
|
return { path: "/movies", itemId }
|
||||||
|
}
|
||||||
|
if (itemType === "series") {
|
||||||
|
return { path: "/series", itemId }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}) {
|
function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}) {
|
||||||
const currentQuery = getRouteSearchQuery()
|
const currentQuery = getRouteSearchQuery()
|
||||||
const targetPath = options.targetPath ?? getBrowsePath()
|
const targetPath = options.targetPath ?? getBrowsePath()
|
||||||
@@ -441,11 +460,6 @@ function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.path === targetPath) {
|
|
||||||
searchReturnPath.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const backPath =
|
const backPath =
|
||||||
typeof window.history.state?.back === "string"
|
typeof window.history.state?.back === "string"
|
||||||
? normalizeHistoryPath(window.history.state.back)
|
? normalizeHistoryPath(window.history.state.back)
|
||||||
@@ -459,11 +473,12 @@ function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}
|
|||||||
|
|
||||||
if (canRestoreWithBack) {
|
if (canRestoreWithBack) {
|
||||||
router.back()
|
router.back()
|
||||||
|
restoreBrowseFocus(targetPath)
|
||||||
} else {
|
} else {
|
||||||
void router.replace({ path: targetPath })
|
void router.replace({ path: targetPath }).finally(() => {
|
||||||
|
restoreBrowseFocus(targetPath)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreBrowseFocus(targetPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSearchQuery(nextValue: string) {
|
function updateSearchQuery(nextValue: string) {
|
||||||
@@ -501,6 +516,15 @@ function handleEscapeKey(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const path = route.path
|
const path = route.path
|
||||||
|
const activeSearchQuery = getRouteSearchQuery()
|
||||||
|
|
||||||
|
if (route.params.id && activeSearchQuery) {
|
||||||
|
event.preventDefault()
|
||||||
|
const targetPath = getBrowsePath()
|
||||||
|
router.push({ path: targetPath, query: { q: activeSearchQuery } })
|
||||||
|
restoreFocusForPage(getBrowseViewFromPath(targetPath))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// From movie detail -> movies list
|
// From movie detail -> movies list
|
||||||
if (path.startsWith("/movies/") && route.params.id) {
|
if (path.startsWith("/movies/") && route.params.id) {
|
||||||
@@ -520,6 +544,12 @@ function handleEscapeKey(event: KeyboardEvent) {
|
|||||||
|
|
||||||
if (getRouteSearchQuery()) {
|
if (getRouteSearchQuery()) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
const target = getSearchExitTargetFromFocusedCard()
|
||||||
|
if (target) {
|
||||||
|
lastViewedItemId.value = target.itemId
|
||||||
|
clearSearch({ preferBack: false, targetPath: target.path })
|
||||||
|
return
|
||||||
|
}
|
||||||
clearSearch({ preferBack: true })
|
clearSearch({ preferBack: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -552,6 +582,14 @@ onUnmounted(() => {
|
|||||||
// Handle back navigation (Escape key or Back button)
|
// Handle back navigation (Escape key or Back button)
|
||||||
function goBack() {
|
function goBack() {
|
||||||
const path = route.path
|
const path = route.path
|
||||||
|
const activeSearchQuery = getRouteSearchQuery()
|
||||||
|
|
||||||
|
if (route.params.id && activeSearchQuery) {
|
||||||
|
const targetPath = getBrowsePath()
|
||||||
|
router.push({ path: targetPath, query: { q: activeSearchQuery } })
|
||||||
|
restoreFocusForPage(getBrowseViewFromPath(targetPath))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// From movie detail -> movies list with focus restoration
|
// From movie detail -> movies list with focus restoration
|
||||||
if (path.startsWith("/movies/") && route.params.id) {
|
if (path.startsWith("/movies/") && route.params.id) {
|
||||||
@@ -576,6 +614,10 @@ const currentView = computed(() => {
|
|||||||
return (route.meta.view as "movies" | "series") || "movies"
|
return (route.meta.view as "movies" | "series") || "movies"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const headerCurrentView = computed<"movies" | "series" | "search">(() => {
|
||||||
|
return selectedItem.value && searchQuery.value ? "search" : currentView.value
|
||||||
|
})
|
||||||
|
|
||||||
// Header position based on current page
|
// Header position based on current page
|
||||||
const headerPosition = computed(() => {
|
const headerPosition = computed(() => {
|
||||||
if (selectedItem.value) {
|
if (selectedItem.value) {
|
||||||
@@ -619,7 +661,7 @@ watch(selectedItem, (item) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show detail by navigating to URL (clears search)
|
// Show detail by navigating to URL
|
||||||
function showDetail(item: MediaItem) {
|
function showDetail(item: MediaItem) {
|
||||||
// Save the item ID to restore focus when returning
|
// Save the item ID to restore focus when returning
|
||||||
lastViewedItemId.value = item.id
|
lastViewedItemId.value = item.id
|
||||||
@@ -646,17 +688,24 @@ function showDetail(item: MediaItem) {
|
|||||||
if (playableFile) {
|
if (playableFile) {
|
||||||
handlePlay(playableFile)
|
handlePlay(playableFile)
|
||||||
} else {
|
} else {
|
||||||
router.push(`/series/${epData.series.id}`)
|
const query = searchQuery.value ? { q: searchQuery.value } : undefined
|
||||||
|
router.push({ path: `/series/${epData.series.id}`, query })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Push without query to clear search and add to history
|
const query = searchQuery.value ? { q: searchQuery.value } : undefined
|
||||||
router.push({ path: `/${item.type}/${item.id}` })
|
router.push({ path: `/${item.type}/${item.id}`, query })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close detail by navigating back to list
|
// Close detail by navigating back to list
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
router.push(`/${currentView.value}`)
|
const activeSearchQuery = getRouteSearchQuery()
|
||||||
|
const targetPath = getBrowsePath()
|
||||||
|
if (activeSearchQuery) {
|
||||||
|
router.push({ path: targetPath, query: { q: activeSearchQuery } })
|
||||||
|
} else {
|
||||||
|
router.push(targetPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleActorSearch(actorName: string) {
|
function handleActorSearch(actorName: string) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<!-- Detail mode: show current category + Details -->
|
<!-- Detail mode: show current category + Details -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button class="header-nav-item" v-bind="navAttrs(navRow, 0)" @focus="goToCategory">
|
<button class="header-nav-item" v-bind="navAttrs(navRow, 0)" @focus="goToCategory">
|
||||||
{{ currentView === "movies" ? "Movies" : "Series" }}
|
{{ currentView === "search" ? "Search" : currentView === "movies" ? "Movies" : "Series" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="header-nav-item active" v-bind="navAttrs(navRow, 1, 1)">Details</button>
|
<button class="header-nav-item active" v-bind="navAttrs(navRow, 1, 1)">Details</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,7 +163,7 @@ interface RootEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
currentView: "movies" | "series"
|
currentView: "movies" | "series" | "search"
|
||||||
searchQuery: string
|
searchQuery: string
|
||||||
mpcBeConnected: boolean
|
mpcBeConnected: boolean
|
||||||
navRow: number
|
navRow: number
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
class="media-card"
|
class="media-card"
|
||||||
v-bind="navAttributes"
|
v-bind="navAttributes"
|
||||||
:data-item-id="item.id"
|
:data-item-id="item.id"
|
||||||
|
:data-item-type="item.type"
|
||||||
:href="href || undefined"
|
:href="href || undefined"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
@keydown.enter.prevent="$emit('click')"
|
@keydown.enter.prevent="$emit('click')"
|
||||||
|
|||||||
Reference in New Issue
Block a user