refactor: hero featured item is a plain link like all other cards
Remove the separate Play and Info buttons from the CollageHero featured item. All collage items (including the featured one) are now uniform <a href> links that navigate to the detail page. - CollageHero: remove hasResumePosition prop, play/info emits, and associated play button logic - App.vue: remove @play and @info bindings from CollageHero - Delete unused HeroSection.vue component and its styles
This commit is contained in:
@@ -3,9 +3,8 @@
|
||||
<!-- Diagonal collage grid -->
|
||||
<div class="collage-grid">
|
||||
<template v-for="(item, index) in collageItems" :key="item.id">
|
||||
<component
|
||||
:is="index !== 0 ? 'a' : 'div'"
|
||||
:ref="(el: HTMLElement | null) => setItemRef(el, index)"
|
||||
<a
|
||||
:ref="(el: unknown) => setItemRef(el as HTMLElement | null, index)"
|
||||
class="collage-item"
|
||||
:class="[
|
||||
`collage-item-${index}`,
|
||||
@@ -17,8 +16,8 @@
|
||||
},
|
||||
]"
|
||||
v-bind="getItemAttrs(index)"
|
||||
:href="index !== 0 ? getItemHref(item) : undefined"
|
||||
@click="handleItemClick($event, item, index)"
|
||||
:href="getItemHref(item)"
|
||||
@click="handleItemClick($event, item)"
|
||||
@focus="focusedIndex = index"
|
||||
>
|
||||
<!-- Featured item with hexagonal clip -->
|
||||
@@ -107,18 +106,6 @@
|
||||
<span v-if="getResolution(item)" class="meta-quality">{{ getResolution(item) }}</span>
|
||||
</div>
|
||||
<p v-if="getOverview(item)" class="collage-overview">{{ getOverview(item) }}</p>
|
||||
<div class="collage-buttons">
|
||||
<button class="btn btn-primary" @click.stop="handlePlay(item)">
|
||||
▶ {{ getPlayLabel(item) }}
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
:href="getItemHref(item)"
|
||||
@click.prevent="$emit('info', item)"
|
||||
>
|
||||
ℹ Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collage-item-hover" v-else>
|
||||
<span class="hover-title">{{ item.title }}</span>
|
||||
@@ -126,7 +113,7 @@
|
||||
>★ {{ getRating(item)?.toFixed(1) }}</span
|
||||
>
|
||||
</div>
|
||||
</component>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
@@ -433,7 +420,7 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
const item = collageItems.value[focusedIndex.value]
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (item) activateItem(item, focusedIndex.value)
|
||||
if (item) activateItem(item)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -476,12 +463,9 @@ function isItemVisible(index: number): boolean {
|
||||
const props = defineProps<{
|
||||
items: MediaItem[]
|
||||
featuredItem?: MediaItem | null
|
||||
hasResumePosition: (filePath: string | null) => boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
play: [string]
|
||||
info: [MediaItem]
|
||||
select: [MediaItem]
|
||||
}>()
|
||||
|
||||
@@ -589,48 +573,15 @@ function getOverview(item: MediaItem): string | null {
|
||||
return overview.length > 150 ? overview.slice(0, 150) + "..." : overview
|
||||
}
|
||||
|
||||
function getPlayableFile(item: MediaItem): string | null {
|
||||
if (item.type === "movies") {
|
||||
const movie = item.data as Movie
|
||||
return Object.values(movie.torrents || {})[0]?.playable_file ?? null
|
||||
}
|
||||
const series = item.data as Series
|
||||
for (const season of series.seasons || []) {
|
||||
for (const episode of season.episodes || []) {
|
||||
for (const torrent of Object.values(episode.torrents || {})) {
|
||||
if (torrent.playable_file) return torrent.playable_file
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function handlePlay(item: MediaItem) {
|
||||
const file = getPlayableFile(item)
|
||||
if (file) emit("play", file)
|
||||
}
|
||||
|
||||
function getPlayLabel(item: MediaItem): string {
|
||||
return props.hasResumePosition(getPlayableFile(item)) ? "Continue" : "Play"
|
||||
}
|
||||
|
||||
function getItemHref(item: MediaItem): string {
|
||||
return `#/${item.type}/${item.id}`
|
||||
}
|
||||
|
||||
function activateItem(item: MediaItem, index: number) {
|
||||
if (index === 0) {
|
||||
emit("info", item)
|
||||
} else {
|
||||
emit("select", item)
|
||||
}
|
||||
function activateItem(item: MediaItem) {
|
||||
emit("select", item)
|
||||
}
|
||||
|
||||
function handleItemClick(event: MouseEvent, item: MediaItem, index: number) {
|
||||
if (index === 0) {
|
||||
emit("info", item)
|
||||
return
|
||||
}
|
||||
function handleItemClick(event: MouseEvent, item: MediaItem) {
|
||||
// Let modified clicks navigate natively
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
@@ -1089,16 +1040,6 @@ html:not(.mouse-active) .collage-item.nav-focused .hex-focus-outline {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.collage-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.collage-buttons .btn {
|
||||
padding: 10px 24px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Hover info for non-featured items */
|
||||
.collage-item-hover {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<section class="hero">
|
||||
<div
|
||||
v-if="coverUrl"
|
||||
class="hero-background"
|
||||
:style="{ backgroundImage: `url('${coverUrl}')` }"
|
||||
></div>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">{{ item.title }}</h1>
|
||||
<div class="hero-meta">
|
||||
<span v-if="item.year" class="hero-year">{{ item.year }}</span>
|
||||
<span v-if="rating" class="hero-rating" :class="ratingClass">
|
||||
★ {{ rating.toFixed(1) }}
|
||||
</span>
|
||||
<span v-if="resolution" class="hero-quality">{{ resolution }}</span>
|
||||
<span v-if="quality" class="hero-quality">{{ quality }}</span>
|
||||
</div>
|
||||
<p v-if="overview" class="hero-overview">{{ overview }}</p>
|
||||
<div class="hero-buttons">
|
||||
<button class="btn btn-primary" @click="handlePlay" :disabled="!playableFile">
|
||||
▶ Play
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
:href="detailHref"
|
||||
@click.prevent="$emit('info', item)"
|
||||
>
|
||||
ℹ More Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import type { MediaItem, Movie, Series } from "../types"
|
||||
import { getCoverUrl } from "../api"
|
||||
|
||||
const props = defineProps<{
|
||||
item: MediaItem
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
play: [string]
|
||||
info: [MediaItem]
|
||||
}>()
|
||||
|
||||
const coverUrl = computed(() => {
|
||||
return getCoverUrl(props.item.cover_path, props.item.root_id)
|
||||
})
|
||||
|
||||
const resolution = computed(() => {
|
||||
if (props.item.type === "movies") {
|
||||
const movie = props.item.data as Movie
|
||||
const torrents = Object.values(movie.torrents || {})
|
||||
return torrents.length > 0 ? torrents[0].resolution : null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const quality = computed(() => {
|
||||
if (props.item.type === "movies") {
|
||||
const movie = props.item.data as Movie
|
||||
const torrents = Object.values(movie.torrents || {})
|
||||
return torrents.length > 0 ? torrents[0].quality : null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const rating = computed(() => {
|
||||
if (props.item.type === "movies") {
|
||||
return (props.item.data as Movie).info?.rating
|
||||
}
|
||||
return (props.item.data as Series).info?.rating
|
||||
})
|
||||
|
||||
const ratingClass = computed(() => {
|
||||
if (!rating.value) return ""
|
||||
if (rating.value >= 7.5) return "rating-high"
|
||||
if (rating.value >= 6) return "rating-medium"
|
||||
return "rating-low"
|
||||
})
|
||||
|
||||
const overview = computed(() => {
|
||||
if (props.item.type === "movies") {
|
||||
const o = (props.item.data as Movie).info?.overview
|
||||
return o ? (o.length > 200 ? o.slice(0, 200) + "..." : o) : null
|
||||
}
|
||||
const o = (props.item.data as Series).info?.overview
|
||||
return o ? (o.length > 200 ? o.slice(0, 200) + "..." : o) : null
|
||||
})
|
||||
|
||||
const detailHref = computed(() => {
|
||||
return `#/${props.item.type}/${props.item.id}`
|
||||
})
|
||||
|
||||
const playableFile = computed(() => {
|
||||
if (props.item.type === "movies") {
|
||||
const movie = props.item.data as Movie
|
||||
const torrents = Object.values(movie.torrents || {})
|
||||
return torrents.length > 0 ? torrents[0].playable_file : null
|
||||
}
|
||||
// For series, get first available file from episodes
|
||||
const series = props.item.data as Series
|
||||
for (const season of series.seasons || []) {
|
||||
for (const episode of season.episodes || []) {
|
||||
const torrents = Object.values(episode.torrents || {})
|
||||
for (const torrent of torrents) {
|
||||
if (torrent.playable_file) {
|
||||
return torrent.playable_file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
function handlePlay() {
|
||||
if (playableFile.value) {
|
||||
emit("play", playableFile.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero-rating {
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.rating-high {
|
||||
color: #46d369;
|
||||
}
|
||||
|
||||
.rating-medium {
|
||||
color: #f9a825;
|
||||
}
|
||||
|
||||
.rating-low {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.hero-overview {
|
||||
max-width: 500px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user