'use client' import { useEffect, useRef, useState, useCallback } from 'react' import type { TvSeries, TvSeason, TvEpisode } from '@/types' import FilterPanel from '@/components/FilterPanel' import VideoPlayerModal from '@/components/mixed/VideoPlayerModal' import EpisodeCard from './EpisodeCard' interface Props { libraryId: string } type ViewLevel = 'series' | 'seasons' | 'episodes' export default function TvView({ libraryId }: Props) { const [view, setView] = useState('series') const [series, setSeries] = useState([]) const [seasons, setSeasons] = useState([]) const [episodes, setEpisodes] = useState([]) const [selectedSeries, setSelectedSeries] = useState(null) const [selectedSeason, setSelectedSeason] = useState(null) const [playingEpisode, setPlayingEpisode] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [search, setSearch] = useState('') const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [showFilters, setShowFilters] = useState(true) const [menuOpen, setMenuOpen] = useState(false) const [confirming, setConfirming] = useState(false) const [deleting, setDeleting] = useState(false) const menuRef = useRef(null) const toggleTag = (tagId: string) => setSelectedTagIds((prev) => { const next = new Set(prev) next.has(tagId) ? next.delete(tagId) : next.add(tagId) return next }) const fetchSeries = useCallback(() => { setLoading(true) setError(null) fetch(`/api/tv?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then((data) => { setSeries(data); setLoading(false) }) .catch(() => { setError('Failed to load TV library'); setLoading(false) }) }, [libraryId]) useEffect(() => { fetchSeries() }, [fetchSeries]) const fetchAssignments = useCallback(() => { fetch(`/api/tags/library-assignments?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then(setAssignments) .catch(() => {}) }, [libraryId]) useEffect(() => { fetchAssignments() }, [fetchAssignments]) const openSeries = (s: TvSeries) => { setSelectedSeries(s) setView('seasons') setLoading(true) setError(null) fetch(`/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(s.id)}`) .then((r) => r.json()) .then((data) => { setSeasons(data); setLoading(false) }) .catch(() => { setError('Failed to load seasons'); setLoading(false) }) } const openSeason = (season: TvSeason) => { setSelectedSeason(season) setView('episodes') setLoading(true) setError(null) fetch( `/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(season.seriesId)}&seasonId=${encodeURIComponent(season.id)}` ) .then((r) => r.json()) .then((data) => { setEpisodes(data); setLoading(false) }) .catch(() => { setError('Failed to load episodes'); setLoading(false) }) } // Close menu on outside click useEffect(() => { if (!menuOpen) return const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuOpen(false) } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [menuOpen]) const goToSeries = () => { setView('series') setSelectedSeries(null) setSelectedSeason(null) setMenuOpen(false) setConfirming(false) } const goToSeasons = () => { setView('seasons') setSelectedSeason(null) setConfirming(false) } const handleDeleteSeries = () => { if (!selectedSeries) return setDeleting(true) fetch( `/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(selectedSeries.id)}`, { method: 'DELETE' } ) .then(() => { setSeries((prev) => prev.filter((s) => s.id !== selectedSeries.id)) goToSeries() setDeleting(false) }) .catch(() => setDeleting(false)) } const filtersActive = search !== '' || selectedTagIds.size > 0 const filteredSeries = series.filter((s) => { if (search && !s.title.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0) { const tags = assignments[`${libraryId}:${s.id}`] ?? [] if (![...selectedTagIds].every((id) => tags.includes(id))) return false } return true }) if (playingEpisode) { const videoUrl = `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(playingEpisode.videoPath)}` return ( setPlayingEpisode(null)} /> ) } return (
{/* Breadcrumb */}
{view !== 'series' ? ( ) : ( All Series )} {selectedSeries && ( <> / {view === 'episodes' ? ( ) : ( {selectedSeries.title} )} )} {selectedSeason && ( <> / {selectedSeason.title} )}
{view === 'series' && ( <>
{showFilters && (
)}
{loading ? ( ) : error ? ( ) : series.length === 0 ? (

No TV shows found

Each series should be a folder containing season subdirectories with video files.

) : (
{filteredSeries.map((s) => ( ))}
)}
)} {view === 'seasons' && selectedSeries && (
{/* Series info header */}
{selectedSeries.posterUrl && ( // eslint-disable-next-line @next/next/no-img-element {selectedSeries.title} )}

{selectedSeries.title}

{/* Kebab menu */}
{menuOpen && (
)}
{(selectedSeries.year || selectedSeries.genres.length > 0) && (
{selectedSeries.year && {selectedSeries.year}} {selectedSeries.genres.map((g) => ( {g} ))}
)} {selectedSeries.plot && (

{selectedSeries.plot}

)}
{/* Confirmation banner */} {confirming && (

Permanently delete this series and all its files?

)}
{loading ? ( ) : error ? ( ) : seasons.length === 0 ? (
No seasons found.
) : (
{seasons.map((season) => ( ))}
)}
)} {view === 'episodes' && selectedSeason && (
{loading ? ( ) : error ? ( ) : episodes.length === 0 ? (
No episodes found.
) : (
{episodes.map((ep) => ( setPlayingEpisode(ep)} /> ))}
)}
)}
) } function ErrorMsg({ message }: { message: string }) { return (
{message}
) } function SeriesLoadingGrid() { return (
{Array.from({ length: 12 }).map((_, i) => (
))}
) } function SeasonLoadingGrid() { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
) } function EpisodeLoadingGrid() { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
) }