mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
refactor(epg/guide): streamline season/episode extraction and guide card layout
- Consolidate serializer season/episode parsing into a single to_representation pass: one custom_properties read, regex runs at most once per program - Compact card: S/E badge inline with title; secondary row shows sub_title with description fallback - Expanded card: sub_title and S/E badge share a row above the description - Fix secondary text color for legibility on dark backgrounds (#a0aec0 → #e2e8f0) - Fix expanded card overflow (height:100% → flex:1 1 0) so action buttons stay visible - Bump expanded card height 180 → 200px, description max-height 80 → 100px
This commit is contained in:
parent
ee112de84a
commit
fa0a963ad9
3 changed files with 74 additions and 52 deletions
|
|
@ -83,38 +83,26 @@ class EPGSourceSerializer(serializers.ModelSerializer):
|
|||
return instance
|
||||
|
||||
class ProgramDataSerializer(serializers.ModelSerializer):
|
||||
season = serializers.SerializerMethodField()
|
||||
episode = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ProgramData
|
||||
fields = ['id', 'start_time', 'end_time', 'title', 'sub_title', 'description', 'tvg_id', 'season', 'episode']
|
||||
fields = ['id', 'start_time', 'end_time', 'title', 'sub_title', 'description', 'tvg_id']
|
||||
|
||||
def _parse_onscreen(self, obj):
|
||||
"""Parse season/episode from onscreen_episode string (e.g. 'S12 E6')."""
|
||||
onscreen = (obj.custom_properties or {}).get('onscreen_episode', '')
|
||||
match = _ONSCREEN_RE.search(onscreen)
|
||||
if match:
|
||||
return int(match.group(1)), int(match.group(2))
|
||||
return None, None
|
||||
|
||||
def get_season(self, obj):
|
||||
if obj.custom_properties:
|
||||
season = obj.custom_properties.get('season')
|
||||
if season is not None:
|
||||
return season
|
||||
season, _ = self._parse_onscreen(obj)
|
||||
return season
|
||||
return None
|
||||
|
||||
def get_episode(self, obj):
|
||||
if obj.custom_properties:
|
||||
episode = obj.custom_properties.get('episode')
|
||||
if episode is not None:
|
||||
return episode
|
||||
_, episode = self._parse_onscreen(obj)
|
||||
return episode
|
||||
return None
|
||||
def to_representation(self, obj):
|
||||
data = super().to_representation(obj)
|
||||
cp = obj.custom_properties or {}
|
||||
season = cp.get('season')
|
||||
episode = cp.get('episode')
|
||||
# Only run the regex if at least one value is still missing
|
||||
if (season is None or episode is None) and cp.get('onscreen_episode'):
|
||||
match = _ONSCREEN_RE.search(cp['onscreen_episode'])
|
||||
if match:
|
||||
if season is None:
|
||||
season = int(match.group(1))
|
||||
if episode is None:
|
||||
episode = int(match.group(2))
|
||||
data['season'] = season
|
||||
data['episode'] = episode
|
||||
return data
|
||||
|
||||
class EPGDataSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -966,8 +966,13 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
transition: 'transform 0.1s ease-out',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: !isExpanded && (program.sub_title || seasonEpisodeLabel) ? 'space-between' : 'flex-start',
|
||||
height: '100%',
|
||||
justifyContent:
|
||||
!isExpanded && (program.sub_title || program.description)
|
||||
? 'space-between'
|
||||
: 'flex-start',
|
||||
flex: '1 1 0',
|
||||
minHeight: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
|
|
@ -994,13 +999,49 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
></div>
|
||||
)}
|
||||
{program.title}
|
||||
{!isExpanded && seasonEpisodeLabel && (
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="gray"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{seasonEpisodeLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Text>
|
||||
{(program.sub_title || seasonEpisodeLabel) && (
|
||||
{!isExpanded && (program.sub_title || program.description) && (
|
||||
<Text
|
||||
size="xs"
|
||||
fs={program.sub_title ? 'italic' : 'normal'}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
minWidth: 0,
|
||||
}}
|
||||
c={isPast ? '#718096' : '#e2e8f0'}
|
||||
>
|
||||
{program.sub_title || program.description}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
size="sm"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{format(programStart, timeFormat)} -{' '}
|
||||
{format(programEnd, timeFormat)}
|
||||
</Text>
|
||||
{isExpanded && (program.sub_title || seasonEpisodeLabel) && (
|
||||
<Group gap="xs" wrap="nowrap" align="center">
|
||||
{program.sub_title && (
|
||||
<Text
|
||||
size="xs"
|
||||
size="sm"
|
||||
fs="italic"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
|
|
@ -1008,7 +1049,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
overflow: 'hidden',
|
||||
minWidth: 0,
|
||||
}}
|
||||
c={isPast ? '#718096' : '#a0aec0'}
|
||||
c={isPast ? '#718096' : '#e2e8f0'}
|
||||
>
|
||||
{program.sub_title}
|
||||
</Text>
|
||||
|
|
@ -1025,28 +1066,16 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
)}
|
||||
</Group>
|
||||
)}
|
||||
<Text
|
||||
size="sm"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{format(programStart, timeFormat)} -{' '}
|
||||
{format(programEnd, timeFormat)}
|
||||
</Text>
|
||||
{(isExpanded || (!program.sub_title && !seasonEpisodeLabel)) && program.description && (
|
||||
{isExpanded && program.description && (
|
||||
<Text
|
||||
size="xs"
|
||||
style={{
|
||||
whiteSpace: isExpanded ? 'normal' : 'nowrap',
|
||||
textOverflow: isExpanded ? 'clip' : 'ellipsis',
|
||||
overflow: isExpanded ? 'auto' : 'hidden',
|
||||
whiteSpace: 'normal',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
mt={4}
|
||||
c={isPast ? '#718096' : '#cbd5e0'}
|
||||
mah={isExpanded ? '80px' : undefined}
|
||||
mah={'100px'}
|
||||
>
|
||||
{program.description}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import API from '../api.js';
|
||||
|
||||
export const PROGRAM_HEIGHT = 90;
|
||||
export const EXPANDED_PROGRAM_HEIGHT = 180;
|
||||
export const EXPANDED_PROGRAM_HEIGHT = 200;
|
||||
/** Layout constants */
|
||||
export const CHANNEL_WIDTH = 120; // Width of the channel/logo column
|
||||
export const HOUR_WIDTH = 450; // Increased from 300 to 450 to make each program wider
|
||||
|
|
@ -214,7 +214,12 @@ export const mapChannelsById = (guideChannels) => {
|
|||
return map;
|
||||
};
|
||||
|
||||
const _terminalStatuses = new Set(['stopped', 'completed', 'interrupted', 'failed']);
|
||||
const _terminalStatuses = new Set([
|
||||
'stopped',
|
||||
'completed',
|
||||
'interrupted',
|
||||
'failed',
|
||||
]);
|
||||
|
||||
export const mapRecordingsByProgramId = (recordings) => {
|
||||
const map = new Map();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue