fix remaining build errors
This commit is contained in:
+16
-6
@@ -461,12 +461,22 @@ export interface TmdbSearchMultiResult extends TmdbMovie {
|
|||||||
known_for?: any[]
|
known_for?: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TMDB discover result - union of movie and tv with optional media_type. */
|
/** TMDB discover result - permissive interface for movie / tv / multi. */
|
||||||
export type TmdbDiscoverItem =
|
export interface TmdbDiscoverItem {
|
||||||
| TmdbMovie
|
id: number
|
||||||
| TmdbTvShow
|
title?: string
|
||||||
| (TmdbMovie & { media_type: string })
|
name?: string
|
||||||
| (TmdbTvShow & { media_type: string })
|
overview?: string
|
||||||
|
poster_path?: string | null
|
||||||
|
backdrop_path?: string | null
|
||||||
|
release_date?: string
|
||||||
|
first_air_date?: string
|
||||||
|
vote_average?: number
|
||||||
|
vote_count?: number
|
||||||
|
adult?: boolean
|
||||||
|
original_language?: string
|
||||||
|
media_type?: string
|
||||||
|
}
|
||||||
|
|
||||||
/* ────────────────────────────────────────────────────────────── */
|
/* ────────────────────────────────────────────────────────────── */
|
||||||
/* Fetcher */
|
/* Fetcher */
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export default function DetailHero({
|
|||||||
<RequestButton
|
<RequestButton
|
||||||
tmdbId={Number(tmdbId)}
|
tmdbId={Number(tmdbId)}
|
||||||
kind={item.Type === 'Movie' ? 'movie' : 'tv'}
|
kind={item.Type === 'Movie' ? 'movie' : 'tv'}
|
||||||
tmdbData={tmdbData}
|
tmdbData={tmdbData || null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
TmdbKeyword,
|
TmdbKeyword,
|
||||||
TmdbReview,
|
TmdbReview,
|
||||||
TmdbVideo,
|
TmdbVideo,
|
||||||
|
TmdbCastMember,
|
||||||
} from '../../api/tmdb'
|
} from '../../api/tmdb'
|
||||||
import type { CinemetaMeta } from '../../api/cinemeta'
|
import type { CinemetaMeta } from '../../api/cinemeta'
|
||||||
import type { TvmazeShow } from '../../api/tvmaze'
|
import type { TvmazeShow } from '../../api/tvmaze'
|
||||||
@@ -66,8 +67,8 @@ interface Props {
|
|||||||
wikiTitle: string | null
|
wikiTitle: string | null
|
||||||
region: string
|
region: string
|
||||||
watchProviders: TmdbWatchProviders | null | undefined
|
watchProviders: TmdbWatchProviders | null | undefined
|
||||||
cast: unknown[]
|
cast: TmdbCastMember[]
|
||||||
crew: unknown[]
|
crew: TmdbCastMember[]
|
||||||
keywords: TmdbKeyword[]
|
keywords: TmdbKeyword[]
|
||||||
reviews: TmdbReview[]
|
reviews: TmdbReview[]
|
||||||
videos: TmdbVideo[] | undefined
|
videos: TmdbVideo[] | undefined
|
||||||
@@ -76,7 +77,7 @@ interface Props {
|
|||||||
libraryMap: Map<string, { id: string; name: string; type: string }> | undefined
|
libraryMap: Map<string, { id: string; name: string; type: string }> | undefined
|
||||||
overview: string
|
overview: string
|
||||||
overviewSource: string | null
|
overviewSource: string | null
|
||||||
sources: { Id?: string }[]
|
sources: { Id?: string | null }[]
|
||||||
activeSourceId: string | null
|
activeSourceId: string | null
|
||||||
onSourceChange: (id: string | null) => void
|
onSourceChange: (id: string | null) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function VersionsSelector({ item, selectedSourceId, onChange }: P
|
|||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{sources.map(src => {
|
{sources.map(src => {
|
||||||
const summary = summarizeSource(src)
|
const summary = summarizeSource(src)
|
||||||
const id = src.Id || src.id || ''
|
const id = src.Id || ''
|
||||||
const active = id === selectedSourceId
|
const active = id === selectedSourceId
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function DecadeRow({ decade, kind }: { decade: Decade; kind: 'movie' | 'tv' }) {
|
|||||||
const data = kind === 'movie' ? movieQuery.data : tvQuery.data
|
const data = kind === 'movie' ? movieQuery.data : tvQuery.data
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const raw = (data?.results || []).map(m => ({ ...m, media_type: kind as const }))
|
const raw = (data?.results || []).map(m => ({ ...m, media_type: kind }))
|
||||||
return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.adult), lib.data)
|
return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.adult), lib.data)
|
||||||
}, [data, lib.data, hideAdult, kind])
|
}, [data, lib.data, hideAdult, kind])
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, useEffect } from 'react'
|
import { useMemo, useState, useEffect, useCallback } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Shuffle, RotateCw, ArrowRight, Star, X } from '../../lib/icons'
|
import { Shuffle, RotateCw, ArrowRight, Star, X } from '../../lib/icons'
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function SpotlightHero({ kind = 'all' }: { kind?: 'all' | 'movie' | 'tv'
|
|||||||
const mediaType: 'movie' | 'tv' = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie'
|
const mediaType: 'movie' | 'tv' = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie'
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
|
if (!pick) return
|
||||||
navigate(`/item/tmdb-${mediaType}-${pick.id}`)
|
navigate(`/item/tmdb-${mediaType}-${pick.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useLibraryByTmdbId, useLibraryGenreDistribution } from '../../hooks/use
|
|||||||
import { usePreferencesStore } from '../../stores/preferences-store'
|
import { usePreferencesStore } from '../../stores/preferences-store'
|
||||||
import { filterToMissing } from '../../pages/discover/helpers'
|
import { filterToMissing } from '../../pages/discover/helpers'
|
||||||
import { tmdbMovieGenreId, tmdbTvGenreId } from '../../lib/tmdb-genres'
|
import { tmdbMovieGenreId, tmdbTvGenreId } from '../../lib/tmdb-genres'
|
||||||
|
import type { TmdbDiscoverItem } from '../../api/tmdb'
|
||||||
|
|
||||||
const TMDB_IMG = 'https://image.tmdb.org/t/p'
|
const TMDB_IMG = 'https://image.tmdb.org/t/p'
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export default function TonightHero({ kind }: Props) {
|
|||||||
|
|
||||||
const pick = useMemo(() => {
|
const pick = useMemo(() => {
|
||||||
// Personalized pool wins when present + non-empty.
|
// Personalized pool wins when present + non-empty.
|
||||||
const personalRaw = (personalData?.results || []).map(m => ({ ...m, media_type: kind as const }))
|
const personalRaw = (personalData?.results || []).map(m => ({ ...m, media_type: kind }))
|
||||||
const personalPool = filterToMissing(personalRaw, lib.data, hideAdult, m => !!m.adult)
|
const personalPool = filterToMissing(personalRaw, lib.data, hideAdult, m => !!m.adult)
|
||||||
.filter(m => m.backdrop_path && m.overview)
|
.filter(m => m.backdrop_path && m.overview)
|
||||||
if (personalPool.length > 0) {
|
if (personalPool.length > 0) {
|
||||||
@@ -84,7 +85,7 @@ export default function TonightHero({ kind }: Props) {
|
|||||||
|
|
||||||
if (!pick) return null
|
if (!pick) return null
|
||||||
|
|
||||||
const item = pick.item
|
const item = pick.item as TmdbDiscoverItem
|
||||||
const title: string = item.title || item.name || ''
|
const title: string = item.title || item.name || ''
|
||||||
const overview: string = item.overview || ''
|
const overview: string = item.overview || ''
|
||||||
const backdrop = `${TMDB_IMG}/w1280${item.backdrop_path}`
|
const backdrop = `${TMDB_IMG}/w1280${item.backdrop_path}`
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function AppHeader({ pinned, onTogglePin }: Props) {
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
includeItemTypes: ['Movie'],
|
includeItemTypes: ['Movie'],
|
||||||
fields: ['DateCreated', 'PrimaryImageAspectRatio', 'ImageTags'],
|
fields: ['DateCreated', 'PrimaryImageAspectRatio', 'ImageTags'] as any[],
|
||||||
}),
|
}),
|
||||||
getItemsApi(api!).getItems({
|
getItemsApi(api!).getItems({
|
||||||
userId: auth.userId,
|
userId: auth.userId,
|
||||||
@@ -96,7 +96,7 @@ export default function AppHeader({ pinned, onTogglePin }: Props) {
|
|||||||
limit: 50,
|
limit: 50,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
includeItemTypes: ['Episode'],
|
includeItemTypes: ['Episode'],
|
||||||
fields: ['DateCreated', 'PrimaryImageAspectRatio', 'ImageTags', 'SeriesName', 'SeriesId', 'SeriesPrimaryImageTag', 'ParentIndexNumber', 'IndexNumber'],
|
fields: ['DateCreated', 'PrimaryImageAspectRatio', 'ImageTags', 'SeriesName', 'SeriesId', 'SeriesPrimaryImageTag', 'ParentIndexNumber', 'IndexNumber'] as any[],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
const movies = movieRes.data.Items || []
|
const movies = movieRes.data.Items || []
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ interface Props {
|
|||||||
volume: number
|
volume: number
|
||||||
loopA: number | null
|
loopA: number | null
|
||||||
loopB: number | null
|
loopB: number | null
|
||||||
chapters: { StartPositionTicks: number; Name?: string | null; ImageTag?: string | null }[]
|
chapters: { StartPositionTicks?: number | null; Name?: string | null; ImageTag?: string | null }[]
|
||||||
bookmarksRefreshKey: number
|
bookmarksRefreshKey: number
|
||||||
previousItem: BaseItemDto | null | undefined
|
previousItem: BaseItemDto | null | undefined
|
||||||
nextItem: BaseItemDto | null | undefined
|
nextItem: BaseItemDto | null | undefined
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SyncPlayPanel from './SyncPlayPanel'
|
|||||||
import type { BaseItemDto } from '../../api/types'
|
import type { BaseItemDto } from '../../api/types'
|
||||||
|
|
||||||
export interface ChapterMarker {
|
export interface ChapterMarker {
|
||||||
StartPositionTicks: number
|
StartPositionTicks?: number | null
|
||||||
Name?: string | null
|
Name?: string | null
|
||||||
ImageTag?: string | null
|
ImageTag?: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export default function RequestModal({ open, onClose, tmdbId, kind, tmdbData }:
|
|||||||
monitored,
|
monitored,
|
||||||
searchOnAdd,
|
searchOnAdd,
|
||||||
title: match.title,
|
title: match.title,
|
||||||
titleSlug: match.titleSlug || '',
|
titleSlug: (match.title || '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
|
||||||
year: match.year || 0,
|
year: match.year || 0,
|
||||||
images: match.images,
|
images: match.images,
|
||||||
minimumAvailability: 'released',
|
minimumAvailability: 'released',
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function PersonSpotlightRow({ personId, name, role, profilePath,
|
|||||||
? (credits.crew || []).filter(c => c.job === 'Director')
|
? (credits.crew || []).filter(c => c.job === 'Director')
|
||||||
: (credits.cast || [])
|
: (credits.cast || [])
|
||||||
// Drop trivia / archival appearances and ultra-deep cuts.
|
// Drop trivia / archival appearances and ultra-deep cuts.
|
||||||
.filter(c => COMMERCIAL_DEPARTMENTS.includes(c.department || 'Acting'))
|
.filter(c => COMMERCIAL_DEPARTMENTS.includes((c as { department?: string }).department || 'Acting'))
|
||||||
|
|
||||||
// De-dupe by id (a director may have multiple crew credits on one film
|
// De-dupe by id (a director may have multiple crew credits on one film
|
||||||
// when they're also writer; a cast member may appear twice for episodic
|
// when they're also writer; a cast member may appear twice for episodic
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export default function QuickLookModal() {
|
|||||||
<RequestButton
|
<RequestButton
|
||||||
tmdbId={numericTmdb}
|
tmdbId={numericTmdb}
|
||||||
kind={item.Type === 'Movie' ? 'movie' : 'tv'}
|
kind={item.Type === 'Movie' ? 'movie' : 'tv'}
|
||||||
tmdbData={tmdbData}
|
tmdbData={(tmdbData || null) as (import('../../api/tmdb').TmdbMovie | import('../../api/tmdb').TmdbTvShow | null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function usePrebuffer(item: BaseItemDto | null | undefined, armed: boolea
|
|||||||
playbackInfoDto: {
|
playbackInfoDto: {
|
||||||
UserId: jellyfinClient.getAuthState()!.userId,
|
UserId: jellyfinClient.getAuthState()!.userId,
|
||||||
MaxStreamingBitrate: 140_000_000,
|
MaxStreamingBitrate: 140_000_000,
|
||||||
DeviceProfile: browserDeviceProfile(),
|
DeviceProfile: browserDeviceProfile() as any,
|
||||||
AutoOpenLiveStream: true,
|
AutoOpenLiveStream: true,
|
||||||
EnableDirectPlay: true,
|
EnableDirectPlay: true,
|
||||||
EnableDirectStream: true,
|
EnableDirectStream: true,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export async function startDownload(args: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob(chunks)
|
const blob = new Blob(chunks as BlobPart[])
|
||||||
const objectUrl = URL.createObjectURL(blob)
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
store.update(dl.id, {
|
store.update(dl.id, {
|
||||||
status: 'done',
|
status: 'done',
|
||||||
|
|||||||
+6
-2
@@ -250,7 +250,9 @@ export async function addToTraktWatchlist(item: BaseItemDto): Promise<boolean> {
|
|||||||
if (!token) return false
|
if (!token) return false
|
||||||
const body = await buildScrobbleBody(item, 0)
|
const body = await buildScrobbleBody(item, 0)
|
||||||
if (!body) return false
|
if (!body) return false
|
||||||
const payload = item.Type === 'Movie' ? { movies: [{ ids: body.movie.ids }] } : { shows: [{ ids: body.show.ids }] }
|
const payload = item.Type === 'Movie'
|
||||||
|
? { movies: [{ ids: (body as { movie: { ids: Record<string, string | number> } }).movie.ids }] }
|
||||||
|
: { shows: [{ ids: (body as { show: { ids: Record<string, string | number> } }).show.ids }] }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE}/sync/watchlist`, {
|
const res = await fetch(`${BASE}/sync/watchlist`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -269,7 +271,9 @@ export async function removeFromTraktWatchlist(item: BaseItemDto): Promise<boole
|
|||||||
if (!token) return false
|
if (!token) return false
|
||||||
const body = await buildScrobbleBody(item, 0)
|
const body = await buildScrobbleBody(item, 0)
|
||||||
if (!body) return false
|
if (!body) return false
|
||||||
const payload = item.Type === 'Movie' ? { movies: [{ ids: body.movie.ids }] } : { shows: [{ ids: body.show.ids }] }
|
const payload = item.Type === 'Movie'
|
||||||
|
? { movies: [{ ids: (body as { movie: { ids: Record<string, string | number> } }).movie.ids }] }
|
||||||
|
: { shows: [{ ids: (body as { show: { ids: Record<string, string | number> } }).show.ids }] }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE}/sync/watchlist/remove`, {
|
const res = await fetch(`${BASE}/sync/watchlist/remove`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ export default function PersonPage() {
|
|||||||
const filmography = useMemo(() => {
|
const filmography = useMemo(() => {
|
||||||
const cast = (person?.combined_credits?.cast || []).map(c => ({ ...c, _kind: 'cast' as const }))
|
const cast = (person?.combined_credits?.cast || []).map(c => ({ ...c, _kind: 'cast' as const }))
|
||||||
const crew = (person?.combined_credits?.crew || []).map(c => ({ ...c, _kind: 'crew' as const }))
|
const crew = (person?.combined_credits?.crew || []).map(c => ({ ...c, _kind: 'crew' as const }))
|
||||||
let merged: ((TmdbCombinedCreditCast | TmdbCombinedCreditCrew) & { _kind: 'cast' | 'crew' })[] = [
|
type MergedCredit =
|
||||||
|
| (TmdbCombinedCreditCast & { _kind: 'cast' })
|
||||||
|
| (TmdbCombinedCreditCrew & { _kind: 'crew' })
|
||||||
|
let merged: MergedCredit[] = [
|
||||||
...cast,
|
...cast,
|
||||||
...crew,
|
...crew,
|
||||||
]
|
]
|
||||||
@@ -37,7 +40,7 @@ export default function PersonPage() {
|
|||||||
if (filter === 'Acting') {
|
if (filter === 'Acting') {
|
||||||
merged = merged.filter(c => c._kind === 'cast')
|
merged = merged.filter(c => c._kind === 'cast')
|
||||||
} else {
|
} else {
|
||||||
merged = merged.filter(c => c._kind === 'crew' && c.department === filter)
|
merged = merged.filter((c): c is MergedCredit & { _kind: 'crew' } => c._kind === 'crew' && c.department === filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +263,7 @@ export default function PersonPage() {
|
|||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-text-3 truncate leading-tight mt-0.5">
|
<p className="text-[11px] text-text-3 truncate leading-tight mt-0.5">
|
||||||
{c.character ? c.character : c.job}
|
{c.character ? c.character : (c as TmdbCombinedCreditCrew).job}
|
||||||
{year && (
|
{year && (
|
||||||
<>
|
<>
|
||||||
<span className="text-text-5 mx-1">·</span>
|
<span className="text-text-5 mx-1">·</span>
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ export default function PlayerPage() {
|
|||||||
const fromQueue = searchParams.get('resume') === 'true'
|
const fromQueue = searchParams.get('resume') === 'true'
|
||||||
if (showResumePromptPref && !fromQueue && pos > threshold) {
|
if (showResumePromptPref && !fromQueue && pos > threshold) {
|
||||||
setResumePromptOpen(true)
|
setResumePromptOpen(true)
|
||||||
resumePromptShownRef.current = item.Id
|
resumePromptShownRef.current = item.Id ?? null
|
||||||
}
|
}
|
||||||
}, [item?.Id, searchParams, showResumePromptPref])
|
}, [item?.Id, searchParams, showResumePromptPref])
|
||||||
|
|
||||||
@@ -986,7 +986,8 @@ export default function PlayerPage() {
|
|||||||
if (resolvedSource?.SupportsDirectPlay) {
|
if (resolvedSource?.SupportsDirectPlay) {
|
||||||
const el = (playerRef.current as { el?: HTMLElement } | null)?.el
|
const el = (playerRef.current as { el?: HTMLElement } | null)?.el
|
||||||
const video = el?.querySelector('video') as HTMLVideoElement | null
|
const video = el?.querySelector('video') as HTMLVideoElement | null
|
||||||
const native = video.audioTracks as { length: number; [i: number]: { enabled: boolean } } | undefined
|
if (!video) return
|
||||||
|
const native = video.audioTracks as { length: number; [i: number]: { enabled: boolean; language?: string } } | undefined
|
||||||
if (native && native.length > 1) {
|
if (native && native.length > 1) {
|
||||||
const target = audioTracks.find(t => t.Index === jfIndex)
|
const target = audioTracks.find(t => t.Index === jfIndex)
|
||||||
const targetLang = (target?.Language || '').toLowerCase()
|
const targetLang = (target?.Language || '').toLowerCase()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTmdbDetailEnrichment } from '../hooks/use-tmdb-detail'
|
|||||||
import { useLibraryByTmdbId } from '../hooks/use-jellyfin'
|
import { useLibraryByTmdbId } from '../hooks/use-jellyfin'
|
||||||
import { useFanartMovie, useFanartTv } from '../hooks/use-external'
|
import { useFanartMovie, useFanartTv } from '../hooks/use-external'
|
||||||
import { getTmdbImageUrl, pickTmdbLogo } from '../api/tmdb'
|
import { getTmdbImageUrl, pickTmdbLogo } from '../api/tmdb'
|
||||||
|
import type { TmdbMovie, TmdbTvShow } from '../api/tmdb'
|
||||||
import { pickBestFanartImage } from '../api/fanart'
|
import { pickBestFanartImage } from '../api/fanart'
|
||||||
import { mapTmdbToJf } from '../lib/tmdb-mapping'
|
import { mapTmdbToJf } from '../lib/tmdb-mapping'
|
||||||
import ContentRow from '../components/ui/ContentRow'
|
import ContentRow from '../components/ui/ContentRow'
|
||||||
@@ -262,7 +263,7 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{kind === 'tv' && data.number_of_seasons > 0 && (
|
{kind === 'tv' && (data.number_of_seasons ?? 0) > 0 && (
|
||||||
<>
|
<>
|
||||||
<Dot />
|
<Dot />
|
||||||
<span className="inline-flex items-center gap-1 tabular-nums">
|
<span className="inline-flex items-center gap-1 tabular-nums">
|
||||||
@@ -287,7 +288,7 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 flex-wrap">
|
<div className="flex items-center gap-2.5 flex-wrap">
|
||||||
<RequestButton tmdbId={tmdbId} kind={kind} tmdbData={data} />
|
<RequestButton tmdbId={tmdbId} kind={kind} tmdbData={data as TmdbMovie | TmdbTvShow | null} />
|
||||||
{matchedLocal && (
|
{matchedLocal && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/item/${matchedLocal.id}`)}
|
onClick={() => navigate(`/item/${matchedLocal.id}`)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '../../hooks/use-tmdb'
|
} from '../../hooks/use-tmdb'
|
||||||
import { useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
import { useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
||||||
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
||||||
|
import type { TmdbDiscoverItem } from '../../api/tmdb'
|
||||||
import { tmdbMovieGenreId, tmdbTvGenreId } from '../../lib/tmdb-genres'
|
import { tmdbMovieGenreId, tmdbTvGenreId } from '../../lib/tmdb-genres'
|
||||||
import ContentRow from '../../components/ui/ContentRow'
|
import ContentRow from '../../components/ui/ContentRow'
|
||||||
import { usePreferencesStore } from '../../stores/preferences-store'
|
import { usePreferencesStore } from '../../stores/preferences-store'
|
||||||
@@ -253,7 +254,7 @@ export function GenreRow({ genre }: { genre: { label: string; subtitle: string }
|
|||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const m = (movies.data?.results || []).map(x => ({ ...x, media_type: 'movie' }))
|
const m = (movies.data?.results || []).map(x => ({ ...x, media_type: 'movie' }))
|
||||||
const t = (tv.data?.results || []).map(x => ({ ...x, media_type: 'tv' }))
|
const t = (tv.data?.results || []).map(x => ({ ...x, media_type: 'tv' }))
|
||||||
const out: { id: number; media_type: string }[] = []
|
const out: TmdbDiscoverItem[] = []
|
||||||
const max = Math.max(m.length, t.length)
|
const max = Math.max(m.length, t.length)
|
||||||
for (let i = 0; i < max && out.length < 20; i++) {
|
for (let i = 0; i < max && out.length < 20; i++) {
|
||||||
if (m[i]) out.push(m[i])
|
if (m[i]) out.push(m[i])
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ServerDashboardSection() {
|
|||||||
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
||||||
|
|
||||||
const uptime = useMemo(() => {
|
const uptime = useMemo(() => {
|
||||||
const start = info?.ServerStartTime
|
const start = (info as { ServerStartTime?: string } | null | undefined)?.ServerStartTime
|
||||||
if (!start) return null
|
if (!start) return null
|
||||||
const ms = Date.now() - new Date(start).getTime()
|
const ms = Date.now() - new Date(start).getTime()
|
||||||
const days = Math.floor(ms / 86_400_000)
|
const days = Math.floor(ms / 86_400_000)
|
||||||
|
|||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/* ── Browser API extensions ── */
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Navigator {
|
||||||
|
mediaCapabilities?: {
|
||||||
|
decodingInfo(configuration: unknown): Promise<unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoPlaybackQuality {
|
||||||
|
droppedVideoFrames: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HTMLVideoElement {
|
||||||
|
getVideoPlaybackQuality?(): VideoPlaybackQuality
|
||||||
|
audioTracks?: { length: number; [i: number]: { enabled: boolean; language?: string } }
|
||||||
|
mozPreservesPitch?: boolean
|
||||||
|
webkitPreservesPitch?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Document {
|
||||||
|
pictureInPictureElement?: Element | null
|
||||||
|
exitPictureInPicture?(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
webkitAudioContext?: typeof AudioContext
|
||||||
|
SubtitlesOctopus?: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
-29
@@ -9,35 +9,6 @@ interface ImportMeta {
|
|||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Browser API extensions ── */
|
|
||||||
|
|
||||||
interface Navigator {
|
|
||||||
mediaCapabilities?: {
|
|
||||||
decodingInfo(configuration: unknown): Promise<unknown>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VideoPlaybackQuality {
|
|
||||||
droppedVideoFrames: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HTMLVideoElement {
|
|
||||||
getVideoPlaybackQuality?(): VideoPlaybackQuality
|
|
||||||
audioTracks?: { length: number; [i: number]: { enabled: boolean } }
|
|
||||||
mozPreservesPitch?: boolean
|
|
||||||
webkitPreservesPitch?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Document {
|
|
||||||
pictureInPictureElement?: Element | null
|
|
||||||
exitPictureInPicture?(): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Window {
|
|
||||||
webkitAudioContext?: typeof AudioContext
|
|
||||||
SubtitlesOctopus?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||||
declare module 'react' {
|
declare module 'react' {
|
||||||
interface CSSProperties {
|
interface CSSProperties {
|
||||||
|
|||||||
Reference in New Issue
Block a user