feat(ui): refine detail navigation and cast row layout
This commit is contained in:
@@ -71,6 +71,7 @@
|
|||||||
@close="closeDetail"
|
@close="closeDetail"
|
||||||
@play="handlePlay"
|
@play="handlePlay"
|
||||||
@open-folder="handleOpenFolder"
|
@open-folder="handleOpenFolder"
|
||||||
|
@search-actor="handleActorSearch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Browse/Search pages -->
|
<!-- Browse/Search pages -->
|
||||||
@@ -505,6 +506,11 @@ function closeDetail() {
|
|||||||
router.push(`/${currentView.value}`);
|
router.push(`/${currentView.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleActorSearch(actorName: string) {
|
||||||
|
searchQuery.value = actorName;
|
||||||
|
router.push(`/${currentView.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert raw data to MediaItem format
|
// Convert raw data to MediaItem format
|
||||||
function movieToMediaItem(movie: Movie): MediaItem {
|
function movieToMediaItem(movie: Movie): MediaItem {
|
||||||
// Get resolution from first torrent if available
|
// Get resolution from first torrent if available
|
||||||
|
|||||||
@@ -146,14 +146,15 @@ function goToCategory() {
|
|||||||
|
|
||||||
// Handle search input focus - navigate to search if we have a query
|
// Handle search input focus - navigate to search if we have a query
|
||||||
function handleSearchFocus() {
|
function handleSearchFocus() {
|
||||||
// If on detail page, go back to browse first
|
// Intentionally no-op: focusing search should not navigate away from detail.
|
||||||
if (isDetailPage.value) {
|
|
||||||
goToCategory();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync local search to parent
|
// 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);
|
emit('search', val);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -94,31 +94,33 @@
|
|||||||
:best="index === 0"
|
:best="index === 0"
|
||||||
:selectable="!!version.playable_file"
|
:selectable="!!version.playable_file"
|
||||||
:disabled="!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)"
|
@activate="handleVersionActivate(version, $event)"
|
||||||
:title="version.playable_file ? 'Click to play/continue. Alt+Click to open folder.' : 'No playable file'"
|
:title="version.playable_file ? 'Click to play/continue. Alt+Click to open folder.' : 'No playable file'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="movieCast && movieCast.length > 0" class="cast-gallery cast-gallery-wide">
|
<div v-if="limitedMovieCast.length > 0" class="cast-list" data-sync-scroll-row="true">
|
||||||
<div class="cast-list">
|
<div
|
||||||
<div
|
v-for="(castMember, castIndex) in limitedMovieCast"
|
||||||
v-for="castMember in movieCast"
|
:key="`${castMember.name}-${castMember.character || ''}`"
|
||||||
:key="`${castMember.name}-${castMember.character || ''}`"
|
class="cast-card media-card"
|
||||||
class="cast-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
|
<img v-else :src="getCastPlaceholderUrl(castMember.gender)" :alt="`${castMember.name} placeholder portrait`" class="cast-photo cast-photo-fallback">
|
||||||
v-if="castMember.profile_path && !castMember.profile_path.startsWith('/')"
|
<div class="cast-copy">
|
||||||
:src="getCoverUrl(castMember.profile_path)"
|
<span class="cast-name">{{ castMember.name }}</span>
|
||||||
:alt="castMember.name"
|
<span v-if="castMember.character" class="cast-character">{{ castMember.character }}</span>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,6 +174,7 @@ const emit = defineEmits<{
|
|||||||
close: [];
|
close: [];
|
||||||
play: [string];
|
play: [string];
|
||||||
openFolder: [string];
|
openFolder: [string];
|
||||||
|
searchActor: [string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Track expanded episode for showing multiple releases
|
// 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;
|
return (props.item.data as Movie).info?.cast as CastMember[] | null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const limitedMovieCast = computed(() => {
|
||||||
|
if (!movieCast.value) return [];
|
||||||
|
return movieCast.value;
|
||||||
|
});
|
||||||
|
|
||||||
const movieRuntime = computed(() => {
|
const movieRuntime = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
if (props.item.type !== 'movies') return null;
|
||||||
return (props.item.data as Movie).info?.runtime;
|
return (props.item.data as Movie).info?.runtime;
|
||||||
@@ -473,6 +481,12 @@ function handleVersionActivate(version: Torrent, event: MouseEvent | KeyboardEve
|
|||||||
function handleOpenFolder(folderPath: string) {
|
function handleOpenFolder(folderPath: string) {
|
||||||
emit('openFolder', folderPath);
|
emit('openFolder', folderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCastSelect(castName: string) {
|
||||||
|
const name = castName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
emit('searchActor', name);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -547,6 +561,7 @@ function handleOpenFolder(folderPath: string) {
|
|||||||
'left main right';
|
'left main right';
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-main {
|
.content-main {
|
||||||
@@ -752,26 +767,59 @@ function handleOpenFolder(folderPath: string) {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cast-gallery {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cast-gallery-wide {
|
|
||||||
grid-area: cast;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cast-list {
|
.cast-list {
|
||||||
display: grid;
|
position: absolute;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(94px, 1fr));
|
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;
|
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 {
|
.cast-card {
|
||||||
|
flex: 0 0 94px;
|
||||||
|
width: 94px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
aspect-ratio: 2 / 3;
|
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 {
|
.cast-photo {
|
||||||
@@ -838,6 +886,17 @@ function handleOpenFolder(folderPath: string) {
|
|||||||
.sidebar-left {
|
.sidebar-left {
|
||||||
align-self: auto;
|
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 */
|
/* Showreel gallery */
|
||||||
|
|||||||
Reference in New Issue
Block a user