From 4754a0dcd708dbd82a8c9ff619557fa440d9148f Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 5 Aug 2025 14:00:05 -0500 Subject: [PATCH] Show epsiodes nested under a season tab. --- frontend/src/pages/VODs.jsx | 158 ++++++++++++++++++++++++++--- frontend/src/store/useVODStore.jsx | 12 ++- 2 files changed, 151 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/VODs.jsx b/frontend/src/pages/VODs.jsx index 108947ec..5eae2580 100644 --- a/frontend/src/pages/VODs.jsx +++ b/frontend/src/pages/VODs.jsx @@ -17,7 +17,10 @@ import { Stack, SegmentedControl, ActionIcon, - Modal + Modal, + Tabs, + Table, + Divider } from '@mantine/core'; import { Search, Play, Calendar, Clock, Star } from 'lucide-react'; import { useDisclosure } from '@mantine/hooks'; @@ -213,11 +216,12 @@ const SeriesCard = ({ series, onClick }) => { }; const SeriesModal = ({ series, opened, onClose }) => { - const { fetchSeriesInfo, loading } = useVODStore(); + const { fetchSeriesInfo, loading, episodes } = useVODStore(); const showVideo = useVideoStore((s) => s.showVideo); const env_mode = useSettingsStore((s) => s.environment.env_mode); const [detailedSeries, setDetailedSeries] = useState(null); const [loadingDetails, setLoadingDetails] = useState(false); + const [activeTab, setActiveTab] = useState('season-1'); useEffect(() => { if (opened && series) { @@ -244,14 +248,55 @@ const SeriesModal = ({ series, opened, onClose }) => { } }, [opened]); - // Get episodes from the detailed series response - const seriesEpisodes = detailedSeries?.episodes ? - Object.values(detailedSeries.episodes).flat().sort((a, b) => { - if (a.season_number !== b.season_number) { - return (a.season_number || 0) - (b.season_number || 0); + // Get episodes from the store based on the series ID + const seriesEpisodes = React.useMemo(() => { + if (!detailedSeries) return []; + + // First try to get episodes from the fetched data + if (detailedSeries.episodesList) { + return detailedSeries.episodesList.sort((a, b) => { + if (a.season_number !== b.season_number) { + return (a.season_number || 0) - (b.season_number || 0); + } + return (a.episode_number || 0) - (b.episode_number || 0); + }); + } + + // Otherwise, filter episodes from the global store + return Object.values(episodes) + .filter(episode => episode.series?.id === detailedSeries.id) + .sort((a, b) => { + if (a.season_number !== b.season_number) { + return (a.season_number || 0) - (b.season_number || 0); + } + return (a.episode_number || 0) - (b.episode_number || 0); + }); + }, [detailedSeries, episodes]); + + // Group episodes by season + const episodesBySeason = React.useMemo(() => { + const grouped = {}; + seriesEpisodes.forEach(episode => { + const season = episode.season_number || 1; + if (!grouped[season]) { + grouped[season] = []; } - return (a.episode_number || 0) - (b.episode_number || 0); - }) : []; + grouped[season].push(episode); + }); + return grouped; + }, [seriesEpisodes]); + + // Get available seasons sorted + const seasons = React.useMemo(() => { + return Object.keys(episodesBySeason).map(Number).sort((a, b) => a - b); + }, [episodesBySeason]); + + // Update active tab when seasons change + React.useEffect(() => { + if (seasons.length > 0 && !seasons.includes(parseInt(activeTab.replace('season-', '')))) { + setActiveTab(`season-${seasons[0]}`); + } + }, [seasons, activeTab]); const handlePlayEpisode = (episode) => { let streamUrl = `/proxy/vod/episode/${episode.uuid}`; @@ -263,6 +308,13 @@ const SeriesModal = ({ series, opened, onClose }) => { showVideo(streamUrl, 'vod', episode); }; + const formatDuration = (minutes) => { + if (!minutes) return ''; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; + }; + if (!series) return null; // Use detailed data if available, otherwise use basic series data @@ -446,20 +498,94 @@ const SeriesModal = ({ series, opened, onClose }) => { )} + + Episodes {loadingDetails ? ( - ) : ( - - {seriesEpisodes.map(episode => ( - - - + ) : seasons.length > 0 ? ( + + + {seasons.map(season => ( + + Season {season} + + ))} + + + {seasons.map(season => ( + + + + + Ep + Title + Duration + Year + Action + + + + {episodesBySeason[season].map(episode => ( + handlePlayEpisode(episode)} + > + + + {episode.episode_number || '?'} + + + + + + {episode.name} + + {episode.genre && ( + + {episode.genre} + + )} + + + + + {formatDuration(episode.duration)} + + + + + {episode.year} + + + + { + e.stopPropagation(); + handlePlayEpisode(episode); + }} + > + + + + + ))} + +
+
))} -
+ + ) : ( + + No episodes found for this series. + )} diff --git a/frontend/src/store/useVODStore.jsx b/frontend/src/store/useVODStore.jsx index 01b469c7..cfff5bf6 100644 --- a/frontend/src/store/useVODStore.jsx +++ b/frontend/src/store/useVODStore.jsx @@ -294,9 +294,10 @@ const useVODStore = create((set, get) => ({ youtube_trailer: response.youtube_trailer || '', }; - // Also update episodes if they're included in the response + let episodesData = {}; + + // Handle episodes - check if they're in the response if (response.episodes) { - const episodesData = {}; Object.entries(response.episodes).forEach(([seasonNumber, seasonEpisodes]) => { seasonEpisodes.forEach((episode) => { const episodeData = { @@ -315,6 +316,7 @@ const useVODStore = create((set, get) => ({ }, type: 'episode', uuid: episode.id, // Use the stream ID as UUID for playback + logo: episode.movie_image ? { url: episode.movie_image } : null, }; episodesData[episode.id] = episodeData; }); @@ -337,7 +339,11 @@ const useVODStore = create((set, get) => ({ loading: false, })); - return seriesInfo; + // Return series info with episodes array for easy access + return { + ...seriesInfo, + episodesList: Object.values(episodesData) + }; } catch (error) { console.error('Failed to fetch series info:', error); set({ error: 'Failed to load series details.', loading: false });