From 9641dfc1a95624a948256276fe1d249e81dd7032 Mon Sep 17 00:00:00 2001 From: lashman Date: Sun, 26 Apr 2026 07:48:34 +0300 Subject: [PATCH] type discover components --- src/components/discover/BrowseGrid.tsx | 5 ++-- src/components/discover/CanonicalLists.tsx | 9 ++++--- src/components/discover/DecadeStrip.tsx | 4 +-- src/components/discover/LibraryGapFinder.tsx | 8 +++--- src/components/discover/MoodChips.tsx | 4 +-- src/components/discover/Roulette.tsx | 26 +++++++++--------- src/components/discover/SpotlightHero.tsx | 21 +++++++-------- src/components/discover/TonightHero.tsx | 28 ++++++++++---------- 8 files changed, 52 insertions(+), 53 deletions(-) diff --git a/src/components/discover/BrowseGrid.tsx b/src/components/discover/BrowseGrid.tsx index a6eb4e5..80be87d 100644 --- a/src/components/discover/BrowseGrid.tsx +++ b/src/components/discover/BrowseGrid.tsx @@ -27,9 +27,8 @@ function isSameSelection(a: BrowseKey | null, b: BrowseKey | null): boolean { if (a.kind !== b.kind) return false if (a.kind === 'genre' && b.kind === 'genre') return a.label === b.label if (a.kind === 'language' && b.kind === 'language') return a.code === b.code - if ((a.kind === 'studio' && b.kind === 'studio') || (a.kind === 'network' && b.kind === 'network')) { - return (a as any).id === (b as any).id - } + if (a.kind === 'studio' && b.kind === 'studio') return a.id === b.id + if (a.kind === 'network' && b.kind === 'network') return a.id === b.id return false } diff --git a/src/components/discover/CanonicalLists.tsx b/src/components/discover/CanonicalLists.tsx index 949844e..ea3124c 100644 --- a/src/components/discover/CanonicalLists.tsx +++ b/src/components/discover/CanonicalLists.tsx @@ -5,6 +5,7 @@ import { useLibraryByTmdbId } from '../../hooks/use-jellyfin' import { usePreferencesStore } from '../../stores/preferences-store' import { mapTmdbToJf } from '../../lib/tmdb-mapping' import { filterToMissing } from '../../pages/discover/helpers' +import type { TmdbMovie } from '../../api/tmdb' interface CanonicalList { id: string @@ -12,7 +13,7 @@ interface CanonicalList { subtitle: string params: Record /** Optional client-side filter applied on top of TMDB results. */ - extra?: (m: { original_language?: string; vote_count?: number }) => boolean + extra?: (m: TmdbMovie) => boolean } /** @@ -123,9 +124,9 @@ function CanonicalRow({ list }: { list: CanonicalList }) { const lib = useLibraryByTmdbId() const hideAdult = usePreferencesStore(s => s.hideAdult) const items = useMemo(() => { - let raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' })) - if (list.extra) raw = raw.filter(list.extra as any) - return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!(m as any).adult), lib.data) + let raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' as const })) + if (list.extra) raw = raw.filter(list.extra) + return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.adult), lib.data) }, [movies.data, lib.data, hideAdult, list]) if (items.length === 0) return null return ( diff --git a/src/components/discover/DecadeStrip.tsx b/src/components/discover/DecadeStrip.tsx index 7c14d76..eab7a3d 100644 --- a/src/components/discover/DecadeStrip.tsx +++ b/src/components/discover/DecadeStrip.tsx @@ -140,8 +140,8 @@ function DecadeRow({ decade, kind }: { decade: Decade; kind: 'movie' | 'tv' }) { const data = kind === 'movie' ? movieQuery.data : tvQuery.data const items = useMemo(() => { - const raw = (data?.results || []).map(m => ({ ...m, media_type: kind })) - return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!(m as any).adult), lib.data) + const raw = (data?.results || []).map(m => ({ ...m, media_type: kind as const })) + return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.adult), lib.data) }, [data, lib.data, hideAdult, kind]) if (items.length === 0) return null diff --git a/src/components/discover/LibraryGapFinder.tsx b/src/components/discover/LibraryGapFinder.tsx index b0779e2..45e0314 100644 --- a/src/components/discover/LibraryGapFinder.tsx +++ b/src/components/discover/LibraryGapFinder.tsx @@ -150,8 +150,8 @@ function CuratedGapRow({ const movies = useTmdbDiscoverMovies(params) const hideAdult = usePreferencesStore(s => s.hideAdult) const items = useMemo(() => { - const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' })) - return mapTmdbToJf(filterToMissing(raw, libraryMap, hideAdult, m => !!(m as any).adult), libraryMap) + const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' as const })) + return mapTmdbToJf(filterToMissing(raw, libraryMap, hideAdult, m => !!m.adult), libraryMap) }, [movies.data, libraryMap, hideAdult]) if (items.length === 0) return null return ( @@ -183,8 +183,8 @@ function GapRow({ const movies = useTmdbDiscoverMovies(params) const hideAdult = usePreferencesStore(s => s.hideAdult) const items = useMemo(() => { - const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' })) - return mapTmdbToJf(filterToMissing(raw, libraryMap, hideAdult, m => !!(m as any).adult), libraryMap) + const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' as const })) + return mapTmdbToJf(filterToMissing(raw, libraryMap, hideAdult, m => !!m.adult), libraryMap) }, [movies.data, libraryMap, hideAdult]) if (items.length === 0) return null return ( diff --git a/src/components/discover/MoodChips.tsx b/src/components/discover/MoodChips.tsx index 887beec..e6ce8f0 100644 --- a/src/components/discover/MoodChips.tsx +++ b/src/components/discover/MoodChips.tsx @@ -112,9 +112,9 @@ function MoodMovieRow({ mood }: { mood: DiscoverMood }) { const lib = useLibraryByTmdbId() const hideAdult = usePreferencesStore(s => s.hideAdult) const items = useMemo(() => { - let raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' })) + let raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' as const })) if (mood.extra) raw = raw.filter(mood.extra) - return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!(m as any).adult), lib.data) + return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.adult), lib.data) }, [movies.data, lib.data, hideAdult, mood]) if (items.length === 0) return null return ( diff --git a/src/components/discover/Roulette.tsx b/src/components/discover/Roulette.tsx index b7a8f29..58c62b0 100644 --- a/src/components/discover/Roulette.tsx +++ b/src/components/discover/Roulette.tsx @@ -7,6 +7,7 @@ import { useLibraryByTmdbId } from '../../hooks/use-jellyfin' import { usePreferencesStore } from '../../stores/preferences-store' import { DISCOVER_MOODS } from '../../lib/discover-moods' import { filterToMissing } from '../../pages/discover/helpers' +import type { TmdbDiscoverItem } from '../../api/tmdb' const TMDB_IMG = 'https://image.tmdb.org/t/p' @@ -76,10 +77,10 @@ function RouletteModal({ const isLoading = kind === 'movie' ? movies.isLoading : tv.isLoading const pool = useMemo(() => { - const raw = (data?.results || []).map(m => ({ ...m, media_type: kind })) - let filtered = filterToMissing(raw, lib.data, hideAdult, m => !!(m as any).adult) - if (mood?.extra) filtered = filtered.filter(mood.extra as any) - return filtered.filter((m: any) => m.poster_path && m.overview) + const raw = (data?.results || []).map(m => ({ ...m, media_type: kind })) as TmdbDiscoverItem[] + let filtered = filterToMissing(raw, lib.data, hideAdult, m => !!m.adult) + if (mood?.extra) filtered = filtered.filter(mood.extra as (m: TmdbDiscoverItem) => boolean) + return filtered.filter(m => m.poster_path && m.overview) }, [data, lib.data, hideAdult, kind, mood]) const [pickIndex, setPickIndex] = useState(() => Math.floor(Math.random() * 20)) @@ -105,8 +106,8 @@ function RouletteModal({ function open() { if (!pick) return - const mediaType = (pick as any).media_type === 'tv' || (pick as any).first_air_date ? 'tv' : 'movie' - navigate(`/item/tmdb-${mediaType}-${(pick as any).id}`) + const mediaType = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie' + navigate(`/item/tmdb-${mediaType}-${pick.id}`) onClose() } @@ -123,13 +124,12 @@ function RouletteModal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [pickIndex, pool.length]) - const p = pick as any - const title = p?.title || p?.name || '' - const year = (p?.release_date || p?.first_air_date || '').slice(0, 4) - const rating = typeof p?.vote_average === 'number' ? p.vote_average.toFixed(1) : null - const overview: string = p?.overview || '' - const backdrop = p?.backdrop_path ? `${TMDB_IMG}/w1280${p.backdrop_path}` : null - const poster = p?.poster_path ? `${TMDB_IMG}/w500${p.poster_path}` : null + const title = pick?.title || pick?.name || '' + const year = (pick?.release_date || pick?.first_air_date || '').slice(0, 4) + const rating = typeof pick?.vote_average === 'number' ? pick.vote_average.toFixed(1) : null + const overview: string = pick?.overview || '' + const backdrop = pick?.backdrop_path ? `${TMDB_IMG}/w1280${pick.backdrop_path}` : null + const poster = pick?.poster_path ? `${TMDB_IMG}/w500${pick.poster_path}` : null return ( { const list = trending.data?.results || [] - const filtered = filterToMissing(list, lib.data, hideAdult, m => !!(m as any).adult) - return filtered.find(m => (m as any).backdrop_path && (m as any).overview) || null + const filtered = filterToMissing(list, lib.data, hideAdult, m => !!m.adult) + return filtered.find(m => m.backdrop_path && m.overview) || null }, [trending.data, lib.data, hideAdult]) if (!pick) return null - const p = pick as any - const title: string = p.title || p.name || 'Untitled' - const overview: string = p.overview || '' - const backdrop = `${TMDB_IMG}/w1280${p.backdrop_path}` - const year = (p.release_date || p.first_air_date || '').slice(0, 4) - const rating = typeof p.vote_average === 'number' ? p.vote_average.toFixed(1) : null - const mediaType: 'movie' | 'tv' = p.media_type === 'tv' || p.first_air_date ? 'tv' : 'movie' + const title: string = pick.title || pick.name || 'Untitled' + const overview: string = pick.overview || '' + const backdrop = `${TMDB_IMG}/w1280${pick.backdrop_path}` + const year = (pick.release_date || pick.first_air_date || '').slice(0, 4) + const rating = typeof pick.vote_average === 'number' ? pick.vote_average.toFixed(1) : null + const mediaType: 'movie' | 'tv' = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie' function open() { - navigate(`/item/tmdb-${mediaType}-${p.id}`) + navigate(`/item/tmdb-${mediaType}-${pick.id}`) } return ( @@ -54,7 +53,7 @@ export function SpotlightHero({ kind = 'all' }: { kind?: 'all' | 'movie' | 'tv' style={{ aspectRatio: '21/9', maxHeight: '440px' }} > { // Personalized pool wins when present + non-empty. - const personalRaw = (personalData?.results || []).map(m => ({ ...m, media_type: kind })) - const personalPool = filterToMissing(personalRaw, lib.data, hideAdult, m => !!(m as any).adult) - .filter((m: any) => m.backdrop_path && m.overview) + const personalRaw = (personalData?.results || []).map(m => ({ ...m, media_type: kind as const })) + const personalPool = filterToMissing(personalRaw, lib.data, hideAdult, m => !!m.adult) + .filter(m => m.backdrop_path && m.overview) if (personalPool.length > 0) { return { item: personalPool[0], reason: 'personalized' as const, genre: topGenre } } // Fallback: trending-day filtered to unowned. const trendRaw = trending.data?.results || [] - const trendPool = filterToMissing(trendRaw, lib.data, hideAdult, m => !!(m as any).adult) - .filter((m: any) => m.backdrop_path && m.overview) + const trendPool = filterToMissing(trendRaw, lib.data, hideAdult, m => !!m.adult) + .filter(m => m.backdrop_path && m.overview) if (trendPool.length > 0) { return { item: trendPool[0], reason: 'trending' as const, genre: null } } @@ -84,16 +84,16 @@ export default function TonightHero({ kind }: Props) { if (!pick) return null - const p = pick.item as any - const title: string = p.title || p.name || '' - const overview: string = p.overview || '' - const backdrop = `${TMDB_IMG}/w1280${p.backdrop_path}` - const year = (p.release_date || p.first_air_date || '').slice(0, 4) - const rating = typeof p.vote_average === 'number' ? p.vote_average.toFixed(1) : null - const mediaType: 'movie' | 'tv' = (p.media_type === 'tv' || p.first_air_date) ? 'tv' : 'movie' + const item = pick.item + const title: string = item.title || item.name || '' + const overview: string = item.overview || '' + const backdrop = `${TMDB_IMG}/w1280${item.backdrop_path}` + const year = (item.release_date || item.first_air_date || '').slice(0, 4) + const rating = typeof item.vote_average === 'number' ? item.vote_average.toFixed(1) : null + const mediaType: 'movie' | 'tv' = (item.media_type === 'tv' || item.first_air_date) ? 'tv' : 'movie' function open() { - navigate(`/item/tmdb-${mediaType}-${p.id}`) + navigate(`/item/tmdb-${mediaType}-${item.id}`) } const eyebrowText = pick.reason === 'personalized' @@ -108,7 +108,7 @@ export default function TonightHero({ kind }: Props) { className="mx-7 mb-9 relative rounded-2xl overflow-hidden ring-1 ring-border bg-elevated/40 shadow-[0_24px_48px_-16px_rgba(0,0,0,0.65)] h-[200px] sm:h-[240px] md:h-[300px] lg:h-[380px] xl:h-[440px]" >