diff --git a/frontend/src/components/ProgramPreview.jsx b/frontend/src/components/ProgramPreview.jsx new file mode 100644 index 00000000..7da27ca2 --- /dev/null +++ b/frontend/src/components/ProgramPreview.jsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { + ActionIcon, + Box, + Group, + Progress, + Stack, + Text, + Tooltip, +} from '@mantine/core'; +import { ChevronDown, ChevronRight, Radio } from 'lucide-react'; + +const formatProgramTime = (seconds) => { + const absSeconds = Math.abs(seconds); + const hours = Math.floor(absSeconds / 3600); + const minutes = Math.floor((absSeconds % 3600) / 60); + const secs = Math.floor(absSeconds % 60); + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${minutes}:${secs.toString().padStart(2, '0')}`; +}; + +const ProgramPreview = ({ program, loading, fetched, label = 'Now Playing:' }) => { + const [isExpanded, setIsExpanded] = useState(false); + + if (loading) { + return ( + + + Loading EPG data... + + ); + } + + if (fetched && !program) { + return ( + + + No current program (EPG may need refresh) + + ); + } + + if (!program) { + return null; + } + + const now = new Date(); + const startTime = program.start_time ? new Date(program.start_time) : null; + const endTime = program.end_time ? new Date(program.end_time) : null; + + let elapsed = 0, remaining = 0, percentage = 0; + let hasValidTime = false; + if (startTime && endTime) { + const totalDuration = (endTime - startTime) / 1000; + if (totalDuration > 0) { + hasValidTime = true; + elapsed = (now - startTime) / 1000; + remaining = (endTime - now) / 1000; + percentage = Math.min(100, Math.max(0, (elapsed / totalDuration) * 100)); + } + } + + return ( + <> + + + + {label} + + + + {program.title} + + + setIsExpanded(!isExpanded)} + style={{ flexShrink: 0 }} + > + {isExpanded ? : } + + + + {isExpanded && program.description && ( + + + {program.description} + + + )} + + {isExpanded && hasValidTime && ( + + + + {formatProgramTime(elapsed)} elapsed + + + {formatProgramTime(remaining)} remaining + + + + + )} + + ); +}; + +export default ProgramPreview; diff --git a/frontend/src/components/cards/StreamConnectionCard.jsx b/frontend/src/components/cards/StreamConnectionCard.jsx index 30509a86..fc5dd570 100644 --- a/frontend/src/components/cards/StreamConnectionCard.jsx +++ b/frontend/src/components/cards/StreamConnectionCard.jsx @@ -11,7 +11,6 @@ import { Card, Center, Group, - Progress, Select, Stack, Text, @@ -19,18 +18,16 @@ import { useMantineTheme, } from '@mantine/core'; import { - ChevronDown, - ChevronRight, CirclePlay, Gauge, HardDriveDownload, HardDriveUpload, - Radio, SquareX, Timer, Users, Video, } from 'lucide-react'; +import ProgramPreview from '../ProgramPreview'; import { toFriendlyDuration, formatExactDuration, @@ -120,7 +117,7 @@ const StreamConnectionCard = ({ const [currentM3UProfile, setCurrentM3UProfile] = useState(null); // Add state for current M3U profile const [data, setData] = useState([]); const [previewedStream, setPreviewedStream] = useState(null); - const [isProgramDescExpanded, setIsProgramDescExpanded] = useState(false); + const theme = useMantineTheme(); @@ -663,48 +660,11 @@ const StreamConnectionCard = ({ {/* Display current program on its own line */} {currentProgram && ( - - - - Now Playing: - - - {currentProgram.title} - - setIsProgramDescExpanded(!isProgramDescExpanded)} - style={{ flexShrink: 0 }} - > - {isProgramDescExpanded ? ( - - ) : ( - - )} - - + + + )} - {/* Expandable program description */} - {currentProgram && - isProgramDescExpanded && - currentProgram.description && ( - - - {currentProgram.description} - - - )} - - {/* Program progress bar */} - {currentProgram && - isProgramDescExpanded && - currentProgram.start_time && - currentProgram.end_time && ( - - )} - {/* Add stream selection dropdown and preview button */} {availableStreams.length > 0 && ( diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 38393d1d..bb367c87 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -33,9 +33,9 @@ import { Tooltip, UnstyledButton, useMantineTheme, - Progress, } from '@mantine/core'; -import { ListOrdered, Radio, SquarePlus, Undo2, X, Zap } from 'lucide-react'; +import { ListOrdered, SquarePlus, Undo2, X, Zap } from 'lucide-react'; +import ProgramPreview from '../ProgramPreview'; import useEPGsStore from '../../store/epgs'; import { FixedSizeList as List } from 'react-window'; import { USER_LEVEL_LABELS, USER_LEVELS } from '../../constants'; @@ -137,7 +137,6 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { const [tvgFilter, setTvgFilter] = useState(''); const [logoFilter, setLogoFilter] = useState(''); const [currentProgram, setCurrentProgram] = useState(null); - const [isProgramDescExpanded, setIsProgramDescExpanded] = useState(false); const [isLoadingProgram, setIsLoadingProgram] = useState(false); const [hasFetchedProgram, setHasFetchedProgram] = useState(false); @@ -461,31 +460,43 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { } }, [defaultValues, channel, reset, epgs, tvgsById]); + const epgDataId = watch('epg_data_id'); + useEffect(() => { - const fetchCurrentProgram = async () => { - const epgDataId = watch('epg_data_id'); - if (epgDataId && epgDataId !== '0' && epgDataId !== '') { - setIsLoadingProgram(true); - setHasFetchedProgram(false); - try { - const program = await API.getCurrentProgramForEpg(epgDataId); + if (!epgDataId || epgDataId === '0' || epgDataId === '') { + setCurrentProgram(null); + setIsLoadingProgram(false); + setHasFetchedProgram(false); + return; + } + + let cancelled = false; + setIsLoadingProgram(true); + setHasFetchedProgram(false); + + API.getCurrentProgramForEpg(epgDataId) + .then((program) => { + if (!cancelled) { setCurrentProgram(program); - } catch (error) { + } + }) + .catch((error) => { + if (!cancelled) { console.error('Failed to fetch current program:', error); setCurrentProgram(null); - } finally { + } + }) + .finally(() => { + if (!cancelled) { setIsLoadingProgram(false); setHasFetchedProgram(true); } - } else { - setCurrentProgram(null); - setIsLoadingProgram(false); - setHasFetchedProgram(false); - } - }; + }); - fetchCurrentProgram(); - }, [watch('epg_data_id')]); + return () => { + cancelled = true; + }; + }, [epgDataId]); // Memoize logo options to prevent infinite re-renders during background loading const logoOptions = useMemo(() => { @@ -1251,101 +1262,14 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { - {isLoadingProgram && ( + {(isLoadingProgram || hasFetchedProgram || currentProgram) && ( - - - Loading EPG data... - - - )} - - {hasFetchedProgram && !currentProgram && !isLoadingProgram && ( - - - - No current program (EPG may need refresh) - - - )} - - {currentProgram && ( - - - - - Current Program: - - - - {currentProgram.title} - - - setIsProgramDescExpanded(!isProgramDescExpanded)} - style={{ flexShrink: 0 }} - > - {isProgramDescExpanded ? '▼' : '▶'} - - - - {isProgramDescExpanded && currentProgram.description && ( - - - {currentProgram.description} - - - )} - - {isProgramDescExpanded && - currentProgram.start_time && - currentProgram.end_time && - (() => { - const now = new Date(); - const startTime = new Date(currentProgram.start_time); - const endTime = new Date(currentProgram.end_time); - const totalDuration = (endTime - startTime) / 1000; - const elapsed = (now - startTime) / 1000; - const remaining = (endTime - now) / 1000; - const percentage = Math.min( - 100, - Math.max(0, (elapsed / totalDuration) * 100) - ); - - const formatProgramTime = (seconds) => { - const absSeconds = Math.abs(seconds); - const hours = Math.floor(absSeconds / 3600); - const minutes = Math.floor((absSeconds % 3600) / 60); - const secs = Math.floor(absSeconds % 60); - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; - } - return `${minutes}:${secs.toString().padStart(2, '0')}`; - }; - - return ( - - - - {formatProgramTime(elapsed)} elapsed - - - {formatProgramTime(remaining)} remaining - - - - - ); - })()} + )}