diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index 11b17e5b..9c90bb1d 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -1,5 +1,11 @@ // frontend/src/pages/Guide.js -import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react'; +import React, { + useMemo, + useState, + useEffect, + useRef, + useCallback, +} from 'react'; import dayjs from 'dayjs'; import API from '../api'; import useChannelsStore from '../store/channels'; @@ -204,26 +210,28 @@ const GuideRow = React.memo(({ index, style, data }) => { channelPrograms.map((program) => renderProgram(program)) ) : ( <> - {Array.from({ length: Math.ceil(24 / 2) }).map((_, placeholderIndex) => ( - - No program data - - ))} + {Array.from({ length: Math.ceil(24 / 2) }).map( + (_, placeholderIndex) => ( + + No program data + + ) + )} )} @@ -435,29 +443,33 @@ export default function TVChannelGuide({ startDate, endDate }) { const [timeFormatSetting] = useLocalStorage('time-format', '12h'); const [dateFormatSetting] = useLocalStorage('date-format', 'mdy'); + // Use user preference for time format const timeFormat = timeFormatSetting === '12h' ? 'h:mm A' : 'HH:mm'; const dateFormat = dateFormatSetting === 'mdy' ? 'MMMM D' : 'D MMMM'; // Format day label using relative terms when possible (Today, Tomorrow, etc) - const formatDayLabel = useCallback((time) => { - const today = dayjs().startOf('day'); - const tomorrow = today.add(1, 'day'); - const weekLater = today.add(7, 'day'); + const formatDayLabel = useCallback( + (time) => { + const today = dayjs().startOf('day'); + const tomorrow = today.add(1, 'day'); + const weekLater = today.add(7, 'day'); - const day = time.startOf('day'); + const day = time.startOf('day'); - if (day.isSame(today, 'day')) { - return 'Today'; - } else if (day.isSame(tomorrow, 'day')) { - return 'Tomorrow'; - } else if (day.isBefore(weekLater)) { - // Within a week, show day name - return time.format('dddd'); - } else { - // Beyond a week, show month and day - return time.format(dateFormat); - } - }, [dateFormat]); + if (day.isSame(today, 'day')) { + return 'Today'; + } else if (day.isSame(tomorrow, 'day')) { + return 'Tomorrow'; + } else if (day.isBefore(weekLater)) { + // Within a week, show day name + return time.format('dddd'); + } else { + // Beyond a week, show month and day + return time.format(dateFormat); + } + }, + [dateFormat] + ); // Hourly marks with day labels const hourTimeline = useMemo(() => { @@ -503,7 +515,10 @@ export default function TVChannelGuide({ startDate, endDate }) { return; } - if (timelineRef.current && timelineRef.current.scrollLeft !== scrollLeft) { + if ( + timelineRef.current && + timelineRef.current.scrollLeft !== scrollLeft + ) { isSyncingScroll.current = true; timelineRef.current.scrollLeft = scrollLeft; requestAnimationFrame(() => { @@ -533,38 +548,35 @@ export default function TVChannelGuide({ startDate, endDate }) { return (minutesSinceStart / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH; }, [now, start, end]); - const syncScrollLeft = useCallback( - (nextLeft, behavior = 'auto') => { - const guideNode = guideRef.current; - const timelineNode = timelineRef.current; + const syncScrollLeft = useCallback((nextLeft, behavior = 'auto') => { + const guideNode = guideRef.current; + const timelineNode = timelineRef.current; - isSyncingScroll.current = true; + isSyncingScroll.current = true; - if (guideNode) { - if (typeof guideNode.scrollTo === 'function') { - guideNode.scrollTo({ left: nextLeft, behavior }); - } else { - guideNode.scrollLeft = nextLeft; - } + if (guideNode) { + if (typeof guideNode.scrollTo === 'function') { + guideNode.scrollTo({ left: nextLeft, behavior }); + } else { + guideNode.scrollLeft = nextLeft; } + } - if (timelineNode) { - if (typeof timelineNode.scrollTo === 'function') { - timelineNode.scrollTo({ left: nextLeft, behavior }); - } else { - timelineNode.scrollLeft = nextLeft; - } + if (timelineNode) { + if (typeof timelineNode.scrollTo === 'function') { + timelineNode.scrollTo({ left: nextLeft, behavior }); + } else { + timelineNode.scrollLeft = nextLeft; } + } - guideScrollLeftRef.current = nextLeft; - setGuideScrollLeft(nextLeft); + guideScrollLeftRef.current = nextLeft; + setGuideScrollLeft(nextLeft); - requestAnimationFrame(() => { - isSyncingScroll.current = false; - }); - }, - [] - ); + requestAnimationFrame(() => { + isSyncingScroll.current = false; + }); + }, []); // Scroll to the nearest half-hour mark ONLY on initial load useEffect(() => { @@ -651,7 +663,10 @@ export default function TVChannelGuide({ startDate, endDate }) { try { await useChannelsStore.getState().fetchRecordings(); } catch (error) { - console.warn('Failed to refresh recordings after saving series rule', error); + console.warn( + 'Failed to refresh recordings after saving series rule', + error + ); } notifications.show({ title: mode === 'new' ? 'Record new episodes' : 'Record all episodes', @@ -706,7 +721,8 @@ export default function TVChannelGuide({ startDate, endDate }) { (program, event) => { event.stopPropagation(); - const programStartMs = program.startMs ?? dayjs(program.start_time).valueOf(); + const programStartMs = + program.startMs ?? dayjs(program.start_time).valueOf(); const startOffsetMinutes = (programStartMs - start.valueOf()) / 60000; const leftPx = (startOffsetMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH; @@ -753,8 +769,7 @@ export default function TVChannelGuide({ startDate, endDate }) { : now.startOf('hour').add(30, 'minute'); const nowOffset = roundedNow.diff(start, 'minute'); const scrollPosition = - (nowOffset / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH - - MINUTE_BLOCK_WIDTH; + (nowOffset / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH - MINUTE_BLOCK_WIDTH; const scrollPos = Math.max(scrollPosition, 0); syncScrollLeft(scrollPos, 'smooth'); @@ -844,8 +859,7 @@ export default function TVChannelGuide({ startDate, endDate }) { const gapSize = 2; const widthPx = - (durationMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH - - gapSize * 2; + (durationMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH - gapSize * 2; const recording = recordingsByProgramId.get(program.id); @@ -955,7 +969,8 @@ export default function TVChannelGuide({ startDate, endDate }) { overflow: 'hidden', }} > - {programStart.format(timeFormat)} - {programEnd.format(timeFormat)} + {programStart.format(timeFormat)} -{' '} + {programEnd.format(timeFormat)} @@ -1039,10 +1054,7 @@ export default function TVChannelGuide({ startDate, endDate }) { [hourTimeline] ); - const virtualizedHeight = useMemo( - () => guideHeight || 600, - [guideHeight] - ); + const virtualizedHeight = useMemo(() => guideHeight || 600, [guideHeight]); const virtualizedWidth = useMemo(() => { if (guideWidth) { @@ -1507,9 +1519,32 @@ export default function TVChannelGuide({ startDate, endDate }) { }} > - - - + + + {recordingForProgram && ( <> + )} @@ -1586,56 +1640,78 @@ export default function TVChannelGuide({ startDate, endDate }) { > {(!rules || rules.length === 0) && ( - No series rules configured + + No series rules configured + )} - {rules && rules.map((r) => ( - - {r.title || r.tvg_id} — {r.mode === 'new' ? 'New episodes' : 'Every episode'} - - - - - - ))} + {rules && + rules.map((r) => ( + + + {r.title || r.tvg_id} —{' '} + {r.mode === 'new' ? 'New episodes' : 'Every episode'} + + + + + + + ))} )}