From fa0a963ad9319b0328101dcd02546cb2f7c7b8b2 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 6 Mar 2026 10:44:11 -0600 Subject: [PATCH] refactor(epg/guide): streamline season/episode extraction and guide card layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/epg/serializers.py | 46 ++++++++------------- frontend/src/pages/Guide.jsx | 71 ++++++++++++++++++++++---------- frontend/src/pages/guideUtils.js | 9 +++- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py index 1179d934..c0b378a8 100644 --- a/apps/epg/serializers.py +++ b/apps/epg/serializers.py @@ -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): """ diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index edd46fd0..79d9bdb5 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -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', }} > )} {program.title} + {!isExpanded && seasonEpisodeLabel && ( + + {seasonEpisodeLabel} + + )} - {(program.sub_title || seasonEpisodeLabel) && ( + {!isExpanded && (program.sub_title || program.description) && ( + + {program.sub_title || program.description} + + )} + + {format(programStart, timeFormat)} -{' '} + {format(programEnd, timeFormat)} + + {isExpanded && (program.sub_title || seasonEpisodeLabel) && ( {program.sub_title && ( {program.sub_title} @@ -1025,28 +1066,16 @@ export default function TVChannelGuide({ startDate, endDate }) { )} )} - - {format(programStart, timeFormat)} -{' '} - {format(programEnd, timeFormat)} - - {(isExpanded || (!program.sub_title && !seasonEpisodeLabel)) && program.description && ( + {isExpanded && program.description && ( {program.description} diff --git a/frontend/src/pages/guideUtils.js b/frontend/src/pages/guideUtils.js index b46616a7..4bd53fdc 100644 --- a/frontend/src/pages/guideUtils.js +++ b/frontend/src/pages/guideUtils.js @@ -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();