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
-
-
-
-
- );
- })()}
+
)}