import { motion } from 'framer-motion' import { useNavigate, type NavigateFunction } from 'react-router-dom' import { Play, Heart, Clock, Star, Plus, Share2, ChevronRight, RefreshCw, Tv2, Film as FilmIcon, Disc3, } from '../../lib/icons' import type { BaseItemDto } from '../../api/types' import type { TmdbMovie, TmdbTvShow } from '../../api/tmdb' import type { CinemetaMeta } from '../../api/cinemeta' import MetaBadges from '../ui/MetaBadges' import WatchlistButton from '../ui/WatchlistButton' import RequestButton from '../request/RequestButton' import ExternalLinks from './ExternalLinks' import HeroTechCard from './HeroTechCard' import CastMenu from './CastMenu' import { useYoutubeViewer } from '../../stores/youtube-viewer-store' import { usePreferencesStore } from '../../stores/preferences-store' import type { useRefreshItem, useSampleEpisode } from '../../hooks/use-jellyfin' import { buildShareUrl, copyToClipboard } from '../../lib/share' import { toast } from '../../stores/toast-store' interface Props { item: BaseItemDto itemType: string tmdbMovieData: TmdbMovie | null | undefined tmdbTvData: TmdbTvShow | null | undefined tmdbId: string | null | undefined posterUrl: string logoUrl: string | null backdropUrl: string | null title: string year?: number | null rating: string | null | undefined runtime: string | null taglineText: string | null | undefined genres: string[] resumeTime: string | null progress: number | null | undefined isFavorite: boolean | null | undefined playUrl: string resumeUrl: string trailer: { key: string; official?: boolean } | null | undefined cinemetaData: CinemetaMeta | null | undefined showTmdbRatings: boolean sampleEpisode: ReturnType refresh: ReturnType } export default function DetailHero({ item, itemType, tmdbMovieData, tmdbTvData, tmdbId, posterUrl, logoUrl, backdropUrl, title, year, rating, runtime, taglineText, genres, resumeTime, progress, isFavorite, playUrl, resumeUrl, trailer, cinemetaData, showTmdbRatings, sampleEpisode, refresh, }: Props) { const navigate = useNavigate() const tmdbData = itemType === 'Movie' ? tmdbMovieData : tmdbTvData const preRollTrailers = usePreferencesStore(s => s.preRollTrailers) const youtube = useYoutubeViewer() function handlePlay() { if (preRollTrailers && trailer && itemType === 'Movie') { youtube.show({ videoKey: trailer.key, title: `${title} - Trailer`, subtitle: trailer.official ? 'Official trailer' : 'Trailer', onClose: () => navigate(playUrl), }) return } navigate(playUrl) } return (
{backdropUrl && ( )}
{/* Left column: type pill + poster */} {itemType && } {posterUrl && ( { (e.target as HTMLImageElement).style.display = 'none' }} className="w-[180px] aspect-[2/3] object-cover rounded-xl ring-1 ring-white/10 shadow-[0_24px_60px_-12px_rgba(0,0,0,0.7)]" /> )} {/* Type pill (small screens only - shown above poster on md+) */}
{itemType && }
{itemType === 'Episode' && item.SeriesId && ( )} {logoUrl ? (
{title} { (e.target as HTMLImageElement).style.display = 'none' }} />
) : (

{title}

)} {taglineText && (

"{taglineText}"

)}
{year && {year}} {year && (rating || runtime) && } {rating && ( {rating} )} {rating && runtime && } {runtime && ( {runtime} )} {/* Ratings shown on the right-side card (and here only on small screens for accessibility) */} {showTmdbRatings && tmdbData?.vote_average != null && tmdbData.vote_average > 0 && ( <> {tmdbData.vote_average.toFixed(1)} TMDB )} {item.CommunityRating != null && item.CommunityRating > 0 && ( <> {item.CommunityRating.toFixed(1)} Jellyfin )} {cinemetaData?.imdbRating && Number(cinemetaData.imdbRating) > 0 && ( <> {Number(cinemetaData.imdbRating).toFixed(1)} IMDB )}
{genres.length > 0 && (
{genres.slice(0, 5).map(g => ( {g} ))}
)}
{resumeTime && ( )} {trailer && ( )} {item.Id && (item.Type === 'Movie' || item.Type === 'Series') && ( )} {tmdbId && (item.Type === 'Movie' || item.Type === 'Series') && ( )} { const url = buildShareUrl({ tmdbId, imdbId: item.ProviderIds?.Imdb, kind: itemType === 'Series' || itemType === 'Episode' ? 'tv' : 'movie', }) if (!url) { toast('No shareable link for this item', 'error') return } const ok = await copyToClipboard(url) toast(ok ? 'Link copied to clipboard' : 'Could not copy link', ok ? 'success' : 'error') }} > {item.Id && } { if (!item.Id) return refresh.mutate({ itemId: item.Id, replaceAllMetadata: false, replaceAllImages: false, }) }} disabled={refresh.isPending} title={ refresh.isPending ? 'Refreshing...' : refresh.isSuccess ? 'Refresh queued' : 'Re-scrape metadata + images from TMDB / TVDB' } >
{progress != null && progress > 0 && (
Watch progress {Math.round(progress)}%
)}
refresh.mutate({ itemId: item.Id!, replaceAllMetadata: false, replaceAllImages: false, }) : undefined } isRescanning={refresh.isPending} />
) } function TypePill({ itemType }: { itemType: string }) { return ( {itemType === 'Series' || itemType === 'Episode' ? ( ) : itemType === 'MusicAlbum' || itemType === 'MusicArtist' ? ( ) : ( )} {itemType === 'Series' ? 'Series' : itemType === 'Movie' ? 'Movie' : itemType} ) } function EpisodeBreadcrumb({ item, navigate }: { item: BaseItemDto; navigate: NavigateFunction }) { return ( ) } function Dot() { return · } function IconButton({ children, active, ...props }: React.ButtonHTMLAttributes & { active?: boolean }) { return ( ) }