refactor: media cards use proper <a href> links instead of JS navigation
MediaCard, MediaRow, CollageHero, HeroSection, and MediaDetail cast cards now render as <a> tags with real hrefs. Modified clicks (ctrl+click, middle-click) navigate natively, enabling open-in-new-tab and link copying. Plain left-clicks continue through the existing showDetail flow for side-effects like focus-state saving.
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
<!-- Diagonal collage grid -->
|
||||
<div class="collage-grid">
|
||||
<template v-for="(item, index) in collageItems" :key="item.id">
|
||||
<div
|
||||
:ref="(el) => setItemRef(el as HTMLElement, index)"
|
||||
<component
|
||||
:is="index !== 0 ? 'a' : 'div'"
|
||||
:ref="(el: HTMLElement | null) => setItemRef(el, index)"
|
||||
class="collage-item"
|
||||
:class="[
|
||||
`collage-item-${index}`,
|
||||
@@ -16,7 +17,8 @@
|
||||
},
|
||||
]"
|
||||
v-bind="getItemAttrs(index)"
|
||||
@click="handleItemClick(item, index)"
|
||||
:href="index !== 0 ? getItemHref(item) : undefined"
|
||||
@click="handleItemClick($event, item, index)"
|
||||
@focus="focusedIndex = index"
|
||||
>
|
||||
<!-- Featured item with hexagonal clip -->
|
||||
@@ -109,7 +111,13 @@
|
||||
<button class="btn btn-primary" @click.stop="handlePlay(item)">
|
||||
▶ {{ getPlayLabel(item) }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click.stop="$emit('info', item)">ℹ Info</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>
|
||||
@@ -118,7 +126,7 @@
|
||||
>★ {{ getRating(item)?.toFixed(1) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
@@ -425,7 +433,7 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
const item = collageItems.value[focusedIndex.value]
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (item) handleItemClick(item, focusedIndex.value)
|
||||
if (item) activateItem(item, focusedIndex.value)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -606,13 +614,36 @@ function getPlayLabel(item: MediaItem): string {
|
||||
return props.hasResumePosition(getPlayableFile(item)) ? "Continue" : "Play"
|
||||
}
|
||||
|
||||
function handleItemClick(item: MediaItem, index: number) {
|
||||
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 handleItemClick(event: MouseEvent, item: MediaItem, index: number) {
|
||||
if (index === 0) {
|
||||
emit("info", item)
|
||||
return
|
||||
}
|
||||
// Let modified clicks navigate natively
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
emit("select", item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -642,6 +673,8 @@ function handleItemClick(item: MediaItem, index: number) {
|
||||
filter 0.3s ease,
|
||||
opacity 0.3s ease,
|
||||
visibility 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Media (images and videos) fill the collage item */
|
||||
|
||||
@@ -20,7 +20,13 @@
|
||||
<button class="btn btn-primary" @click="handlePlay" :disabled="!playableFile">
|
||||
▶ Play
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="$emit('info', item)">ℹ More Info</button>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
:href="detailHref"
|
||||
@click.prevent="$emit('info', item)"
|
||||
>
|
||||
ℹ More Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -85,6 +91,10 @@ const overview = computed(() => {
|
||||
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
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
<component
|
||||
:is="href ? 'a' : 'div'"
|
||||
class="media-card"
|
||||
v-bind="navAttributes"
|
||||
:data-item-id="item.id"
|
||||
@click="$emit('click')"
|
||||
:href="href || undefined"
|
||||
@click="handleClick"
|
||||
@keydown.enter.prevent="$emit('click')"
|
||||
>
|
||||
<div class="media-card-poster">
|
||||
@@ -78,7 +80,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -91,12 +93,30 @@ const props = defineProps<{
|
||||
item: MediaItem
|
||||
navRow?: number
|
||||
navCol?: number
|
||||
href?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
// Let modified clicks (middle-click, ctrl+click, etc.) navigate natively
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Prevent default navigation for plain left-clicks and synthetic clicks
|
||||
// so that parent handlers can manage side-effects and routing
|
||||
event.preventDefault()
|
||||
emit("click")
|
||||
}
|
||||
|
||||
const navAttributes = computed(() => {
|
||||
if (props.navRow !== undefined && props.navCol !== undefined) {
|
||||
return navAttrs(props.navRow, props.navCol)
|
||||
|
||||
@@ -106,14 +106,14 @@
|
||||
</div>
|
||||
|
||||
<div v-if="limitedMovieCast.length > 0" class="cast-list" data-sync-scroll-row="true">
|
||||
<div
|
||||
<a
|
||||
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"
|
||||
:href="`#/?q=${encodeURIComponent(castMember.name)}`"
|
||||
:title="`Search for ${castMember.name}`"
|
||||
@click="handleCastSelect(castMember.name)"
|
||||
@click.prevent="handleCastSelect(castMember.name)"
|
||||
>
|
||||
<img
|
||||
v-if="castMember.profile_path && !castMember.profile_path.startsWith('/')"
|
||||
@@ -133,7 +133,7 @@
|
||||
castMember.character
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
|
||||
@@ -10,13 +10,14 @@
|
||||
:item="item"
|
||||
:nav-row="rowIndex"
|
||||
:nav-col="index"
|
||||
:href="getItemHref(item)"
|
||||
@click="$emit('select', item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MediaItem } from "../types"
|
||||
import type { MediaItem, EpisodeWithSeries } from "../types"
|
||||
import MediaCard from "./MediaCard.vue"
|
||||
|
||||
defineProps<{
|
||||
@@ -28,4 +29,12 @@ defineProps<{
|
||||
defineEmits<{
|
||||
select: [MediaItem]
|
||||
}>()
|
||||
|
||||
function getItemHref(item: MediaItem): string | undefined {
|
||||
if (item.type === "episode") {
|
||||
const epData = item.data as EpisodeWithSeries
|
||||
return `#/series/${epData.series.id}`
|
||||
}
|
||||
return `#/${item.type}/${item.id}`
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -89,6 +89,7 @@ export function useMediaWebSocket() {
|
||||
const torrentsB = annotateTorrents(b.torrents, b.root_id)
|
||||
return {
|
||||
...a,
|
||||
id: getContentHash(a.id),
|
||||
torrents: mergeTorrentDicts(torrentsA, torrentsB),
|
||||
info: a.info || b.info,
|
||||
cover_path: a.cover_path || b.cover_path,
|
||||
@@ -171,6 +172,7 @@ export function useMediaWebSocket() {
|
||||
}
|
||||
return {
|
||||
...a,
|
||||
id: getContentHash(a.id),
|
||||
seasons: Array.from(seasonMap.values()).sort((a, b) => a.season_number - b.season_number),
|
||||
info: a.info || b.info,
|
||||
cover_path: a.cover_path || b.cover_path,
|
||||
|
||||
@@ -473,6 +473,8 @@ html:not(.mouse-active) .btn-secondary.nav-focused {
|
||||
box-shadow var(--transition-medium);
|
||||
position: relative;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
html.mouse-active .media-card:hover,
|
||||
|
||||
Reference in New Issue
Block a user