mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-25 11:04:07 +00:00
Show epsiodes nested under a season tab.
This commit is contained in:
parent
b11270ce4a
commit
4754a0dcd7
2 changed files with 151 additions and 19 deletions
|
|
@ -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 }) => {
|
|||
</Box>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title order={4}>Episodes</Title>
|
||||
|
||||
{loadingDetails ? (
|
||||
<Flex justify="center" py="xl">
|
||||
<Loader />
|
||||
</Flex>
|
||||
) : (
|
||||
<Grid>
|
||||
{seriesEpisodes.map(episode => (
|
||||
<Grid.Col span={6} key={episode.id}>
|
||||
<VODCard vod={episode} onClick={handlePlayEpisode} />
|
||||
</Grid.Col>
|
||||
) : seasons.length > 0 ? (
|
||||
<Tabs value={activeTab} onTabChange={setActiveTab}>
|
||||
<Tabs.List>
|
||||
{seasons.map(season => (
|
||||
<Tabs.Tab key={season} value={`season-${season}`}>
|
||||
Season {season}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
{seasons.map(season => (
|
||||
<Tabs.Panel key={season} value={`season-${season}`} pt="md">
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th style={{ width: '60px' }}>Ep</Table.Th>
|
||||
<Table.Th>Title</Table.Th>
|
||||
<Table.Th style={{ width: '80px' }}>Duration</Table.Th>
|
||||
<Table.Th style={{ width: '60px' }}>Year</Table.Th>
|
||||
<Table.Th style={{ width: '80px' }}>Action</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{episodesBySeason[season].map(episode => (
|
||||
<Table.Tr
|
||||
key={episode.id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handlePlayEpisode(episode)}
|
||||
>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="outline">
|
||||
{episode.episode_number || '?'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack spacing={2}>
|
||||
<Text size="sm" weight={500}>
|
||||
{episode.name}
|
||||
</Text>
|
||||
{episode.genre && (
|
||||
<Text size="xs" color="dimmed">
|
||||
{episode.genre}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" color="dimmed">
|
||||
{formatDuration(episode.duration)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" color="dimmed">
|
||||
{episode.year}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePlayEpisode(episode);
|
||||
}}
|
||||
>
|
||||
<Play size={12} />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Grid>
|
||||
</Tabs>
|
||||
) : (
|
||||
<Text color="dimmed" align="center" py="xl">
|
||||
No episodes found for this series.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue