feat(ui): refine detail navigation and cast row layout

This commit is contained in:
2026-05-22 21:20:05 +00:00
parent 491e707b6f
commit 04aa858338
3 changed files with 100 additions and 34 deletions
+6 -5
View File
@@ -146,14 +146,15 @@ function goToCategory() {
// Handle search input focus - navigate to search if we have a query
function handleSearchFocus() {
// If on detail page, go back to browse first
if (isDetailPage.value) {
goToCategory();
}
// Intentionally no-op: focusing search should not navigate away from detail.
}
// Sync local search to parent
watch(localSearch, (val) => {
watch(localSearch, (val, prevVal) => {
// When user starts typing from detail page, exit detail and show search results.
if (isDetailPage.value && !prevVal && !!val.trim()) {
goToCategory();
}
emit('search', val);
});
+88 -29
View File
@@ -94,31 +94,33 @@
:best="index === 0"
:selectable="!!version.playable_file"
:disabled="!version.playable_file"
v-bind="navAttrs(2, index)"
v-bind="navAttrs(2, index, index === 0 ? 0 : undefined)"
@activate="handleVersionActivate(version, $event)"
:title="version.playable_file ? 'Click to play/continue. Alt+Click to open folder.' : 'No playable file'"
/>
</div>
</div>
<div v-if="movieCast && movieCast.length > 0" class="cast-gallery cast-gallery-wide">
<div class="cast-list">
<div
v-for="castMember in movieCast"
:key="`${castMember.name}-${castMember.character || ''}`"
class="cast-card"
<div v-if="limitedMovieCast.length > 0" class="cast-list" data-sync-scroll-row="true">
<div
v-for="(castMember, castIndex) in limitedMovieCast"
:key="`${castMember.name}-${castMember.character || ''}`"
class="cast-card media-card"
v-bind="navAttrs(2, movieVersions.length + castIndex)"
role="button"
:title="`Search for ${castMember.name}`"
@click="handleCastSelect(castMember.name)"
>
<img
v-if="castMember.profile_path && !castMember.profile_path.startsWith('/')"
:src="getCoverUrl(castMember.profile_path)"
:alt="castMember.name"
class="cast-photo"
>
<img
v-if="castMember.profile_path && !castMember.profile_path.startsWith('/')"
:src="getCoverUrl(castMember.profile_path)"
:alt="castMember.name"
class="cast-photo"
>
<img v-else :src="getCastPlaceholderUrl(castMember.gender)" :alt="`${castMember.name} placeholder portrait`" class="cast-photo cast-photo-fallback">
<div class="cast-copy">
<span class="cast-name">{{ castMember.name }}</span>
<span v-if="castMember.character" class="cast-character">{{ castMember.character }}</span>
</div>
<img v-else :src="getCastPlaceholderUrl(castMember.gender)" :alt="`${castMember.name} placeholder portrait`" class="cast-photo cast-photo-fallback">
<div class="cast-copy">
<span class="cast-name">{{ castMember.name }}</span>
<span v-if="castMember.character" class="cast-character">{{ castMember.character }}</span>
</div>
</div>
</div>
@@ -172,6 +174,7 @@ const emit = defineEmits<{
close: [];
play: [string];
openFolder: [string];
searchActor: [string];
}>();
// Track expanded episode for showing multiple releases
@@ -388,6 +391,11 @@ const movieCast = computed(() => {
return (props.item.data as Movie).info?.cast as CastMember[] | null;
});
const limitedMovieCast = computed(() => {
if (!movieCast.value) return [];
return movieCast.value;
});
const movieRuntime = computed(() => {
if (props.item.type !== 'movies') return null;
return (props.item.data as Movie).info?.runtime;
@@ -473,6 +481,12 @@ function handleVersionActivate(version: Torrent, event: MouseEvent | KeyboardEve
function handleOpenFolder(folderPath: string) {
emit('openFolder', folderPath);
}
function handleCastSelect(castName: string) {
const name = castName.trim();
if (!name) return;
emit('searchActor', name);
}
</script>
<style scoped>
@@ -547,6 +561,7 @@ function handleOpenFolder(folderPath: string) {
'left main right';
gap: 32px;
align-items: start;
position: relative;
}
.content-main {
@@ -752,26 +767,59 @@ function handleOpenFolder(folderPath: string) {
line-height: 1.6;
}
.cast-gallery {
margin-bottom: 12px;
}
.cast-gallery-wide {
grid-area: cast;
margin-bottom: 0;
}
.cast-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(94px, 1fr));
position: absolute;
top: calc(-1 * (var(--header-height) + 30px));
left: calc(-50vw + 50% + 40vw - 0.8rem);
width: calc(100vw - (40vw - 0.8rem));
margin: 0;
padding-top: 4px;
padding-right: 32px;
padding-bottom: 8px;
padding-left: 0;
z-index: 2;
display: flex;
flex-wrap: nowrap;
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.cast-list::-webkit-scrollbar {
height: 0;
display: none;
}
.cast-card {
flex: 0 0 94px;
width: 94px;
position: relative;
overflow: hidden;
border-radius: 12px;
aspect-ratio: 2 / 3;
cursor: pointer;
}
.cast-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
border: 0 solid rgba(255, 255, 255, 0.95);
pointer-events: none;
transition: border-width 120ms ease;
}
.cast-card:focus-visible,
.cast-card.nav-focused {
outline: none;
}
.cast-card:focus-visible::after,
.cast-card.nav-focused::after {
border-width: 2px;
}
.cast-photo {
@@ -838,6 +886,17 @@ function handleOpenFolder(folderPath: string) {
.sidebar-left {
align-self: auto;
}
.cast-list {
position: static;
top: auto;
left: auto;
width: auto;
margin-left: 0;
margin-top: 0;
padding-left: 0;
padding-right: 0;
}
}
/* Showreel gallery */