Add ability to expand an episode row to view additional information.

This commit is contained in:
SergeantPanda 2025-08-05 16:19:50 -05:00
parent cc8064877d
commit 4b6792ccbe
3 changed files with 156 additions and 47 deletions

View file

@ -298,8 +298,8 @@ class SeriesViewSet(viewsets.ReadOnlyModelViewSet):
'release_date': episode.release_date,
'plot': episode.description,
'duration': episode.duration,
'duration_secs': episode.duration * 60 if episode.duration else None,
'rating': episode.rating,
'movie_image': episode.custom_properties.get('info', {}).get('movie_image') if episode.custom_properties else None,
'container_extension': episode.container_extension,
'type': 'episode',
'series': {

View file

@ -222,6 +222,7 @@ const SeriesModal = ({ series, opened, onClose }) => {
const [detailedSeries, setDetailedSeries] = useState(null);
const [loadingDetails, setLoadingDetails] = useState(false);
const [activeTab, setActiveTab] = useState(null);
const [expandedEpisode, setExpandedEpisode] = useState(null);
useEffect(() => {
if (opened && series) {
@ -324,6 +325,10 @@ const SeriesModal = ({ series, opened, onClose }) => {
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const handleEpisodeRowClick = (episode) => {
setExpandedEpisode(expandedEpisode === episode.id ? null : episode.id);
};
if (!series) return null;
// Use detailed data if available, otherwise use basic series data
@ -539,52 +544,155 @@ const SeriesModal = ({ series, opened, onClose }) => {
</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}
<React.Fragment key={episode.id}>
<Table.Tr
style={{ cursor: 'pointer' }}
onClick={() => handleEpisodeRowClick(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>
)}
</Stack>
</Table.Td>
<Table.Td>
<Text size="xs" color="dimmed">
{formatDuration(episode.duration)}
</Text>
</Table.Td>
<Table.Td>
<Text size="xs" color="dimmed">
{episode.release_date ? new Date(episode.release_date).toLocaleDateString() : 'N/A'}
</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>
{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.release_date ? new Date(episode.release_date).toLocaleDateString() : 'N/A'}
</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>
{expandedEpisode === episode.id && (
<Table.Tr>
<Table.Td colSpan={5} style={{ backgroundColor: '#2A2A2E', padding: '16px' }}>
<Stack spacing="sm">
{/* Episode Image and Description Row */}
<Flex gap="md">
{/* Episode Image */}
{episode.movie_image && (
<Box style={{ flexShrink: 0 }}>
<Image
src={episode.movie_image}
width={120}
height={160}
alt={episode.name}
fit="cover"
style={{ borderRadius: '4px' }}
/>
</Box>
)}
{/* Episode Description */}
<Box style={{ flex: 1 }}>
{episode.description && (
<Box>
<Text size="sm" weight={500} mb={4}>Description</Text>
<Text size="sm" color="dimmed">
{episode.description}
</Text>
</Box>
)}
</Box>
</Flex>
{/* Additional Episode Details */}
<Group spacing="xl">
{episode.rating && (
<Box>
<Text size="xs" weight={500} color="dimmed" mb={2}>Rating</Text>
<Badge color="yellow" size="sm">{episode.rating}</Badge>
</Box>
)}
{episode.director && (
<Box>
<Text size="xs" weight={500} color="dimmed" mb={2}>Director</Text>
<Text size="sm">{episode.director}</Text>
</Box>
)}
{episode.actors && (
<Box>
<Text size="xs" weight={500} color="dimmed" mb={2}>Cast</Text>
<Text size="sm" lineClamp={2}>{episode.actors}</Text>
</Box>
)}
</Group>
{/* Technical Details */}
{(episode.bitrate || episode.video || episode.audio) && (
<Box>
<Text size="xs" weight={500} color="dimmed" mb={4}>Technical Details</Text>
<Stack spacing={2}>
{episode.bitrate && episode.bitrate > 0 && (
<Text size="xs" color="dimmed">
<strong>Bitrate:</strong> {episode.bitrate} kbps
</Text>
)}
{episode.video && Object.keys(episode.video).length > 0 && (
<Text size="xs" color="dimmed">
<strong>Video:</strong>{' '}
{episode.video.codec_long_name || episode.video.codec_name}
{episode.video.width && episode.video.height
? `, ${episode.video.width}x${episode.video.height}`
: ''}
</Text>
)}
{episode.audio && Object.keys(episode.audio).length > 0 && (
<Text size="xs" color="dimmed">
<strong>Audio:</strong>{' '}
{episode.audio.codec_long_name || episode.audio.codec_name}
{episode.audio.channels
? `, ${episode.audio.channels} channels`
: ''}
</Text>
)}
</Stack>
</Box>
)}
{/* Provider Information */}
{episode.m3u_account && (
<Group spacing="md">
<Text size="xs" weight={500} color="dimmed">Provider:</Text>
<Badge color="blue" variant="light" size="sm">
{episode.m3u_account.name || episode.m3u_account}
</Badge>
</Group>
)}
</Stack>
</Table.Td>
</Table.Tr>
)}
</React.Fragment>
))}
</Table.Tbody>
</Table>

View file

@ -307,7 +307,7 @@ const useVODStore = create((set, get) => ({
description: episode.plot || '',
season_number: parseInt(seasonNumber) || 0,
episode_number: episode.episode_number || 0,
duration: episode.duration_secs ? Math.floor(episode.duration_secs / 60) : null,
duration: episode.duration ? Math.floor(episode.duration / 60) : null,
rating: episode.rating || '',
container_extension: episode.container_extension || '',
series: {
@ -318,6 +318,7 @@ const useVODStore = create((set, get) => ({
uuid: episode.id, // Use the stream ID as UUID for playback
logo: episode.movie_image ? { url: episode.movie_image } : null,
release_date: episode.release_date || null,
movie_image: episode.movie_image || null,
};
episodesData[episode.id] = episodeData;
});