Sliding layout between details and category views.

This commit is contained in:
2026-05-27 20:58:19 +00:00
parent e78774613a
commit 81be850e0c
+142 -93
View File
@@ -59,108 +59,115 @@
</div> </div>
</main> </main>
<!-- Detail page (movie or series) --> <div v-else class="page-slider">
<MediaDetail <div class="page-slider-track" :class="{ 'is-detail-open': isDetailOpen }">
v-else-if="selectedItem" <!-- Browse/Search page (left panel) -->
:item="selectedItem" <main class="main-content page-slider-panel">
:focus-episode="focusEpisode" <!-- Movies and Series views - both always rendered for smooth transitions -->
:has-resume-position="hasResumePosition" <div v-if="!searchQuery" class="view-container">
:get-root-name="getRootName" <Transition name="view-zoom" mode="out-in">
@close="closeDetail" <div v-if="currentView === 'movies'" key="movies" class="view-content">
@play="handlePlay" <!-- Hero for movies -->
@open-folder="handleOpenFolder" <CollageHero
@search-actor="handleActorSearch" v-if="movieCollageItems.length > 0"
/> :key="`movie-hero-${movieCollageItems.length}-${movieFeaturedItem?.id || 'none'}`"
:items="movieCollageItems"
<!-- Browse/Search pages --> :featured-item="movieFeaturedItem"
<main v-else class="main-content">
<!-- Movies and Series views - both always rendered for smooth transitions -->
<div v-if="!searchQuery" class="view-container">
<Transition name="view-zoom" mode="out-in">
<div v-if="currentView === 'movies'" key="movies" class="view-content">
<!-- Hero for movies -->
<CollageHero
v-if="movieCollageItems.length > 0"
:key="`movie-hero-${movieCollageItems.length}-${movieFeaturedItem?.id || 'none'}`"
:items="movieCollageItems"
:featured-item="movieFeaturedItem"
@select="showDetail"
/>
<!-- Spacer for header overlay -->
<div class="header-spacer"></div>
<!-- Movie categories -->
<template v-for="(category, categoryIndex) in moviesByGenre" :key="category.name">
<section class="media-section" v-if="category.items.length > 0">
<h2 class="section-title">{{ category.name }} ({{ category.items.length }})</h2>
<MediaRow
:items="category.items"
:row-index="categoryIndex + 2"
@select="showDetail" @select="showDetail"
/> />
</section> <!-- Spacer for header overlay -->
</template> <div class="header-spacer"></div>
</div> <!-- Movie categories -->
<div v-else-if="currentView === 'series'" key="series" class="view-content"> <template v-for="(category, categoryIndex) in moviesByGenre" :key="category.name">
<!-- Hero for series --> <section class="media-section" v-if="category.items.length > 0">
<CollageHero <h2 class="section-title">{{ category.name }} ({{ category.items.length }})</h2>
v-if="seriesCollageItems.length > 0" <MediaRow
:key="`series-hero-${seriesCollageItems.length}-${seriesFeaturedItem?.id || 'none'}`" :items="category.items"
:items="seriesCollageItems" :row-index="categoryIndex + 2"
:featured-item="seriesFeaturedItem" @select="showDetail"
@select="showDetail" />
/> </section>
<!-- Spacer for header overlay --> </template>
<div class="header-spacer"></div> </div>
<!-- Series categories --> <div v-else-if="currentView === 'series'" key="series" class="view-content">
<template v-for="(category, categoryIndex) in seriesByGenre" :key="category.name"> <!-- Hero for series -->
<section class="media-section" v-if="category.items.length > 0"> <CollageHero
<h2 class="section-title">{{ category.name }} ({{ category.items.length }})</h2> v-if="seriesCollageItems.length > 0"
<MediaRow :key="`series-hero-${seriesCollageItems.length}-${seriesFeaturedItem?.id || 'none'}`"
:items="category.items" :items="seriesCollageItems"
:row-index="categoryIndex + 2" :featured-item="seriesFeaturedItem"
@select="showDetail" @select="showDetail"
/> />
</section> <!-- Spacer for header overlay -->
</template> <div class="header-spacer"></div>
<!-- Series categories -->
<template v-for="(category, categoryIndex) in seriesByGenre" :key="category.name">
<section class="media-section" v-if="category.items.length > 0">
<h2 class="section-title">{{ category.name }} ({{ category.items.length }})</h2>
<MediaRow
:items="category.items"
:row-index="categoryIndex + 2"
@select="showDetail"
/>
</section>
</template>
</div>
</Transition>
</div> </div>
</Transition>
</div>
<!-- Search results --> <!-- Search results -->
<template v-if="searchQuery"> <template v-if="searchQuery">
<template v-if="searchResults.length > 0"> <template v-if="searchResults.length > 0">
<!-- Hero with all results ranked by relevance --> <!-- Hero with all results ranked by relevance -->
<CollageHero <CollageHero
:key="`search-hero-${searchCollageItems.length}-${searchFeaturedItem?.id || 'none'}`" :key="`search-hero-${searchCollageItems.length}-${searchFeaturedItem?.id || 'none'}`"
:items="searchCollageItems" :items="searchCollageItems"
:featured-item="searchFeaturedItem" :featured-item="searchFeaturedItem"
@select="showDetail"
/>
<!-- Spacer for header overlay -->
<div class="header-spacer"></div>
<!-- Category sections -->
<template v-for="(category, categoryIndex) in searchCategories" :key="category.name">
<section class="media-section">
<h2 class="section-title">{{ category.name }} ({{ category.items.length }})</h2>
<MediaRow
:items="category.items"
:row-index="categoryIndex + 2"
@select="showDetail" @select="showDetail"
/> />
</section> <!-- Spacer for header overlay -->
<div class="header-spacer"></div>
<!-- Category sections -->
<template v-for="(category, categoryIndex) in searchCategories" :key="category.name">
<section class="media-section">
<h2 class="section-title">{{ category.name }} ({{ category.items.length }})</h2>
<MediaRow
:items="category.items"
:row-index="categoryIndex + 2"
@select="showDetail"
/>
</section>
</template>
</template>
<template v-else-if="!isSearching">
<!-- Empty hero area to maintain layout -->
<div class="empty-hero"></div>
<!-- Spacer for header overlay -->
<div class="header-spacer"></div>
<div class="no-results">
<p>No results found for "{{ searchQuery }}"</p>
</div>
</template>
</template> </template>
</template> </main>
<template v-else-if="!isSearching">
<!-- Empty hero area to maintain layout --> <!-- Detail page (right panel) -->
<div class="empty-hero"></div> <main class="main-content page-slider-panel page-slider-detail-panel">
<!-- Spacer for header overlay --> <MediaDetail
<div class="header-spacer"></div> v-if="detailItemForRender"
<div class="no-results"> v-show="isDetailOpen"
<p>No results found for "{{ searchQuery }}"</p> :item="detailItemForRender"
</div> :focus-episode="focusEpisode"
</template> :has-resume-position="hasResumePosition"
</template> :get-root-name="getRootName"
</main> @close="closeDetail"
@play="handlePlay"
@open-folder="handleOpenFolder"
@search-actor="handleActorSearch"
/>
</main>
</div>
</div>
</div> </div>
</template> </template>
@@ -602,6 +609,16 @@ const selectedItem = computed(() => {
return null return null
}) })
const isDetailOpen = computed(() => selectedItem.value !== null)
const lastDetailItem = ref<MediaItem | null>(null)
const detailItemForRender = computed(() => selectedItem.value ?? lastDetailItem.value)
watch(selectedItem, (item) => {
if (item) {
lastDetailItem.value = item
}
})
// Show detail by navigating to URL (clears search) // Show detail by navigating to URL (clears search)
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
@@ -1237,4 +1254,36 @@ async function handleOpenFolder(folderPath: string, explicitRootId?: string | nu
color: var(--text-secondary); color: var(--text-secondary);
font-size: 2rem; font-size: 2rem;
} }
.page-slider {
width: 100vw;
overflow-x: hidden;
}
.page-slider-track {
display: flex;
width: 200vw;
transition: transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
will-change: transform;
}
.page-slider-track.is-detail-open {
transform: translate3d(-100vw, 0, 0);
}
.page-slider-panel {
flex: 0 0 100vw;
width: 100vw;
min-width: 100vw;
}
.page-slider-detail-panel {
background: var(--bg-primary);
}
@media (prefers-reduced-motion: reduce) {
.page-slider-track {
transition: none;
}
}
</style> </style>