- {`Are you sure you want to clear EPG assignments for ${channelIds?.length || 0} selected channels?
-
-This will set all selected channels to use dummy EPG data.
-
-This action cannot be undone.`}
+
+
+ You are about to apply the following changes to{' '}
+ {channelIds?.length || 0} selected channel
+ {(channelIds?.length || 0) !== 1 ? 's' : ''}:
+
+
+
+ {getConfirmationMessage().map((change, index) => (
+
+ {change}
+
+ ))}
+
+
+
+ This action cannot be undone.
+
}
- confirmLabel="Clear EPGs"
+ confirmLabel="Apply Changes"
cancelLabel="Cancel"
- actionKey="batch-clear-epgs"
+ actionKey="batch-update-channels"
onSuppressChange={suppressWarning}
size="md"
/>
diff --git a/frontend/src/components/forms/Channels.jsx b/frontend/src/components/forms/Channels.jsx
deleted file mode 100644
index 97efea54..00000000
--- a/frontend/src/components/forms/Channels.jsx
+++ /dev/null
@@ -1,729 +0,0 @@
-import React, { useState, useEffect, useRef, useMemo } from 'react';
-import { useFormik } from 'formik';
-import * as Yup from 'yup';
-import useChannelsStore from '../../store/channels';
-import API from '../../api';
-import useStreamProfilesStore from '../../store/streamProfiles';
-import useStreamsStore from '../../store/streams';
-import { useChannelLogoSelection } from '../../hooks/useSmartLogos';
-import LazyLogo from '../LazyLogo';
-import ChannelGroupForm from './ChannelGroup';
-import usePlaylistsStore from '../../store/playlists';
-import logo from '../../images/logo.png';
-import {
- Box,
- Button,
- Modal,
- TextInput,
- NativeSelect,
- Text,
- Group,
- ActionIcon,
- Center,
- Grid,
- Flex,
- Select,
- Divider,
- Stack,
- useMantineTheme,
- Popover,
- ScrollArea,
- Tooltip,
- NumberInput,
- Image,
- UnstyledButton,
-} from '@mantine/core';
-import { ListOrdered, SquarePlus, SquareX, X } from 'lucide-react';
-import useEPGsStore from '../../store/epgs';
-import { Dropzone } from '@mantine/dropzone';
-import { notifications } from '@mantine/notifications';
-import { FixedSizeList as List } from 'react-window';
-
-const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
- const theme = useMantineTheme();
-
- const listRef = useRef(null);
- const logoListRef = useRef(null);
- const groupListRef = useRef(null);
-
- const channelGroups = useChannelsStore((s) => s.channelGroups);
- const { logos, ensureLogosLoaded } = useChannelLogoSelection();
- const streams = useStreamsStore((state) => state.streams);
- const streamProfiles = useStreamProfilesStore((s) => s.profiles);
- const playlists = usePlaylistsStore((s) => s.playlists);
- const epgs = useEPGsStore((s) => s.epgs);
- const tvgs = useEPGsStore((s) => s.tvgs);
- const tvgsById = useEPGsStore((s) => s.tvgsById);
-
- const [logoPreview, setLogoPreview] = useState(null);
- const [channelStreams, setChannelStreams] = useState([]);
- const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false);
- const [epgPopoverOpened, setEpgPopoverOpened] = useState(false);
- const [logoPopoverOpened, setLogoPopoverOpened] = useState(false);
- const [selectedEPG, setSelectedEPG] = useState('');
- const [tvgFilter, setTvgFilter] = useState('');
- const [logoFilter, setLogoFilter] = useState('');
-
- const [groupPopoverOpened, setGroupPopoverOpened] = useState(false);
- const [groupFilter, setGroupFilter] = useState('');
- const groupOptions = Object.values(channelGroups);
-
- const addStream = (stream) => {
- const streamSet = new Set(channelStreams);
- streamSet.add(stream);
- setChannelStreams(Array.from(streamSet));
- };
-
- const removeStream = (stream) => {
- const streamSet = new Set(channelStreams);
- streamSet.delete(stream);
- setChannelStreams(Array.from(streamSet));
- };
-
- const handleLogoChange = async (files) => {
- if (files.length === 1) {
- const file = files[0];
-
- // Validate file size on frontend first
- if (file.size > 5 * 1024 * 1024) {
- // 5MB
- notifications.show({
- title: 'Error',
- message: 'File too large. Maximum size is 5MB.',
- color: 'red',
- });
- return;
- }
-
- try {
- const retval = await API.uploadLogo(file);
- // Note: API.uploadLogo already adds the logo to the store, no need to fetch
- setLogoPreview(retval.cache_url);
- formik.setFieldValue('logo_id', retval.id);
- } catch (error) {
- console.error('Logo upload failed:', error);
- // Error notification is already handled in API.uploadLogo
- }
- } else {
- setLogoPreview(null);
- }
- };
-
- const formik = useFormik({
- initialValues: {
- name: '',
- channel_number: '', // Change from 0 to empty string for consistency
- channel_group_id:
- Object.keys(channelGroups).length > 0
- ? Object.keys(channelGroups)[0]
- : '',
- stream_profile_id: '0',
- tvg_id: '',
- tvc_guide_stationid: '',
- epg_data_id: '',
- logo_id: '',
- },
- validationSchema: Yup.object({
- name: Yup.string().required('Name is required'),
- channel_group_id: Yup.string().required('Channel group is required'),
- }),
- onSubmit: async (values, { setSubmitting }) => {
- let response;
-
- try {
- const formattedValues = { ...values };
-
- // Convert empty or "0" stream_profile_id to null for the API
- if (
- !formattedValues.stream_profile_id ||
- formattedValues.stream_profile_id === '0'
- ) {
- formattedValues.stream_profile_id = null;
- }
-
- // Ensure tvg_id is properly included (no empty strings)
- formattedValues.tvg_id = formattedValues.tvg_id || null;
-
- // Ensure tvc_guide_stationid is properly included (no empty strings)
- formattedValues.tvc_guide_stationid =
- formattedValues.tvc_guide_stationid || null;
-
- if (channel) {
- // If there's an EPG to set, use our enhanced endpoint
- if (values.epg_data_id !== (channel.epg_data_id ?? '')) {
- // Use the special endpoint to set EPG and trigger refresh
- const epgResponse = await API.setChannelEPG(
- channel.id,
- values.epg_data_id
- );
-
- // Remove epg_data_id from values since we've handled it separately
- const { epg_data_id, ...otherValues } = formattedValues;
-
- // Update other channel fields if needed
- if (Object.keys(otherValues).length > 0) {
- response = await API.updateChannel({
- id: channel.id,
- ...otherValues,
- streams: channelStreams.map((stream) => stream.id),
- });
- }
- } else {
- // No EPG change, regular update
- response = await API.updateChannel({
- id: channel.id,
- ...formattedValues,
- streams: channelStreams.map((stream) => stream.id),
- });
- }
- } else {
- // New channel creation - use the standard method
- response = await API.addChannel({
- ...formattedValues,
- streams: channelStreams.map((stream) => stream.id),
- });
- }
- } catch (error) {
- console.error('Error saving channel:', error);
- }
-
- formik.resetForm();
- API.requeryChannels();
-
- // Refresh channel profiles to update the membership information
- useChannelsStore.getState().fetchChannelProfiles();
-
- setSubmitting(false);
- setTvgFilter('');
- setLogoFilter('');
- onClose();
- },
- });
-
- useEffect(() => {
- if (channel) {
- if (channel.epg_data_id) {
- const epgSource = epgs[tvgsById[channel.epg_data_id]?.epg_source];
- setSelectedEPG(epgSource ? `${epgSource.id}` : '');
- }
-
- formik.setValues({
- name: channel.name || '',
- channel_number:
- channel.channel_number !== null ? channel.channel_number : '',
- channel_group_id: channel.channel_group_id
- ? `${channel.channel_group_id}`
- : '',
- stream_profile_id: channel.stream_profile_id
- ? `${channel.stream_profile_id}`
- : '0',
- tvg_id: channel.tvg_id || '',
- tvc_guide_stationid: channel.tvc_guide_stationid || '',
- epg_data_id: channel.epg_data_id ?? '',
- logo_id: channel.logo_id ? `${channel.logo_id}` : '',
- });
-
- setChannelStreams(channel.streams || []);
- } else {
- formik.resetForm();
- setTvgFilter('');
- setLogoFilter('');
- }
- }, [channel, tvgsById, channelGroups]);
-
- // Memoize logo options to prevent infinite re-renders during background loading
- const logoOptions = useMemo(() => {
- return [{ id: '0', name: 'Default' }].concat(Object.values(logos));
- }, [logos]); // Only depend on logos object
-
- const renderLogoOption = ({ option, checked }) => {
- return (
-
-
-
- );
- };
-
- // Update the handler for when channel group modal is closed
- const handleChannelGroupModalClose = (newGroup) => {
- setChannelGroupModalOpen(false);
-
- // If a new group was created and returned, update the form with it
- if (newGroup && newGroup.id) {
- // Preserve all current form values while updating just the channel_group_id
- formik.setValues({
- ...formik.values,
- channel_group_id: `${newGroup.id}`,
- });
- }
- };
-
- if (!isOpen) {
- return <>>;
- }
-
- const filteredTvgs = tvgs
- .filter((tvg) => tvg.epg_source == selectedEPG)
- .filter(
- (tvg) =>
- tvg.name.toLowerCase().includes(tvgFilter.toLowerCase()) ||
- tvg.tvg_id.toLowerCase().includes(tvgFilter.toLowerCase())
- );
-
- const filteredLogos = logoOptions.filter((logo) =>
- logo.name.toLowerCase().includes(logoFilter.toLowerCase())
- );
-
- const filteredGroups = groupOptions.filter((group) =>
- group.name.toLowerCase().includes(groupFilter.toLowerCase())
- );
-
- return (
-
-
- Channels
-
- }
- styles={{ content: { '--mantine-color-body': '#27272A' } }}
- >
-
-
- );
-};
-
-export default ChannelsForm;
diff --git a/frontend/src/components/forms/DummyEPG.jsx b/frontend/src/components/forms/DummyEPG.jsx
index a449d0c6..9f9346da 100644
--- a/frontend/src/components/forms/DummyEPG.jsx
+++ b/frontend/src/components/forms/DummyEPG.jsx
@@ -7,6 +7,7 @@ import {
Group,
Modal,
NumberInput,
+ Paper,
Select,
Stack,
Text,
@@ -16,6 +17,7 @@ import {
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import API from '../../api';
+import useEPGsStore from '../../store/epgs';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
@@ -25,6 +27,16 @@ dayjs.extend(utc);
dayjs.extend(timezone);
const DummyEPGForm = ({ epg, isOpen, onClose }) => {
+ // Get all EPGs from the store
+ const epgs = useEPGsStore((state) => state.epgs);
+
+ // Filter for dummy EPG sources only
+ const dummyEpgs = useMemo(() => {
+ return Object.values(epgs)
+ .filter((e) => e.source_type === 'dummy')
+ .sort((a, b) => a.name.localeCompare(b.name));
+ }, [epgs]);
+
// Separate state for each field to prevent focus loss
const [titlePattern, setTitlePattern] = useState('');
const [timePattern, setTimePattern] = useState('');
@@ -37,6 +49,11 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
useState('');
const [endedTitleTemplate, setEndedTitleTemplate] = useState('');
const [endedDescriptionTemplate, setEndedDescriptionTemplate] = useState('');
+ const [fallbackTitleTemplate, setFallbackTitleTemplate] = useState('');
+ const [fallbackDescriptionTemplate, setFallbackDescriptionTemplate] =
+ useState('');
+ const [channelLogoUrl, setChannelLogoUrl] = useState('');
+ const [programPosterUrl, setProgramPosterUrl] = useState('');
const [timezoneOptions, setTimezoneOptions] = useState([]);
const [loadingTimezones, setLoadingTimezones] = useState(true);
@@ -59,11 +76,16 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
upcoming_description_template: '',
ended_title_template: '',
ended_description_template: '',
+ fallback_title_template: '',
+ fallback_description_template: '',
+ channel_logo_url: '',
+ program_poster_url: '',
name_source: 'channel',
stream_index: 1,
category: '',
include_date: true,
include_live: false,
+ include_new: false,
},
},
validate: {
@@ -101,12 +123,15 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
titleGroups: {},
timeGroups: {},
dateGroups: {},
+ calculatedPlaceholders: {},
formattedTitle: '',
formattedDescription: '',
formattedUpcomingTitle: '',
formattedUpcomingDescription: '',
formattedEndedTitle: '',
formattedEndedDescription: '',
+ formattedChannelLogoUrl: '',
+ formattedProgramPosterUrl: '',
error: null,
};
@@ -166,6 +191,21 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
...result.dateGroups,
};
+ // Add normalized versions of all groups for cleaner URLs
+ // These remove all non-alphanumeric characters and convert to lowercase
+ Object.keys(allGroups).forEach((key) => {
+ const value = allGroups[key];
+ if (value) {
+ // Remove all non-alphanumeric characters (except spaces temporarily)
+ // then replace spaces with nothing, and convert to lowercase
+ const normalized = String(value)
+ .replace(/[^a-zA-Z0-9\s]/g, '')
+ .replace(/\s+/g, '')
+ .toLowerCase();
+ allGroups[`${key}_normalize`] = normalized;
+ }
+ });
+
// Calculate formatted time strings if time was extracted
if (result.timeGroups.hour) {
try {
@@ -186,10 +226,91 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
const sourceTimezone = form.values.custom_properties?.timezone || 'UTC';
const outputTimezone = form.values.custom_properties?.output_timezone;
+ // Determine the base date to use
+ let baseDate = dayjs().tz(sourceTimezone);
+
+ // If date was extracted from pattern, use that instead of today
+ if (result.dateGroups.month && result.dateGroups.day) {
+ const monthValue = result.dateGroups.month;
+ let extractedMonth;
+
+ // Parse month - can be numeric (1-12) or text (Jan, January, Oct, October, etc.)
+ if (/^\d+$/.test(monthValue)) {
+ // Numeric month
+ extractedMonth = parseInt(monthValue);
+ } else {
+ // Text month - convert to number (1-12)
+ const monthLower = monthValue.toLowerCase();
+ const monthNames = [
+ 'january',
+ 'february',
+ 'march',
+ 'april',
+ 'may',
+ 'june',
+ 'july',
+ 'august',
+ 'september',
+ 'october',
+ 'november',
+ 'december',
+ ];
+ const monthAbbr = [
+ 'jan',
+ 'feb',
+ 'mar',
+ 'apr',
+ 'may',
+ 'jun',
+ 'jul',
+ 'aug',
+ 'sep',
+ 'oct',
+ 'nov',
+ 'dec',
+ ];
+
+ // Try full month names first
+ let monthIndex = monthNames.findIndex((m) => m === monthLower);
+ if (monthIndex === -1) {
+ // Try abbreviated month names
+ monthIndex = monthAbbr.findIndex((m) => m === monthLower);
+ }
+
+ if (monthIndex !== -1) {
+ extractedMonth = monthIndex + 1; // Convert 0-indexed to 1-12
+ } else {
+ // If we can't parse it, default to current month
+ extractedMonth = dayjs().month() + 1;
+ }
+ }
+
+ const extractedDay = parseInt(result.dateGroups.day);
+ const extractedYear = result.dateGroups.year
+ ? parseInt(result.dateGroups.year)
+ : dayjs().year(); // Default to current year if not provided
+
+ // Validate that we have valid numeric values
+ if (
+ !isNaN(extractedMonth) &&
+ !isNaN(extractedDay) &&
+ !isNaN(extractedYear) &&
+ extractedMonth >= 1 &&
+ extractedMonth <= 12 &&
+ extractedDay >= 1 &&
+ extractedDay <= 31
+ ) {
+ // Create a specific date string and parse it in the source timezone
+ // This ensures DST is calculated correctly for the target date
+ const dateString = `${extractedYear}-${extractedMonth.toString().padStart(2, '0')}-${extractedDay.toString().padStart(2, '0')}`;
+ baseDate = dayjs.tz(dateString, sourceTimezone);
+ }
+ }
+
if (outputTimezone && outputTimezone !== sourceTimezone) {
- // Create a date in the source timezone
- const sourceDate = dayjs()
- .tz(sourceTimezone)
+ // Create a date in the source timezone with extracted or current date
+ // Set the time on the date, which will use the DST rules for that specific date
+ const sourceDate = baseDate
.set('hour', hour24)
.set('minute', minute)
.set('second', 0);
@@ -201,11 +322,18 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
hour24 = outputDate.hour();
const convertedMinute = outputDate.minute();
- // Format 24-hour time string with converted time
+ // Add date placeholders based on the OUTPUT timezone
+ // This ensures {date}, {month}, {day}, {year} reflect the converted timezone
+ allGroups.date = outputDate.format('YYYY-MM-DD');
+ allGroups.month = outputDate.month() + 1; // dayjs months are 0-indexed
+ allGroups.day = outputDate.date();
+ allGroups.year = outputDate.year();
+
+ // Format 24-hour start time string with converted time
if (convertedMinute > 0) {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:${convertedMinute.toString().padStart(2, '0')}`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:${convertedMinute.toString().padStart(2, '0')}`;
} else {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:00`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:00`;
}
// Convert to 12-hour format with converted time
@@ -217,19 +345,34 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
hour12 = hour24 - 12;
}
- // Format 12-hour time string with converted time
+ // Format 12-hour start time string with converted time
if (convertedMinute > 0) {
- allGroups.time = `${hour12}:${convertedMinute.toString().padStart(2, '0')} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12}:${convertedMinute.toString().padStart(2, '0')} ${ampmDisplay}`;
} else {
- allGroups.time = `${hour12} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12} ${ampmDisplay}`;
}
+
+ // Format long versions that always include minutes
+ allGroups.starttime_long = `${hour12}:${convertedMinute.toString().padStart(2, '0')} ${ampmDisplay}`;
+ allGroups.starttime24_long = `${hour24.toString().padStart(2, '0')}:${convertedMinute.toString().padStart(2, '0')}`;
} else {
// No timezone conversion - use original logic
- // Format 24-hour time string
+ // Add date placeholders based on the source timezone
+ const sourceDate = baseDate
+ .set('hour', hour24)
+ .set('minute', minute)
+ .set('second', 0);
+
+ allGroups.date = sourceDate.format('YYYY-MM-DD');
+ allGroups.month = sourceDate.month() + 1; // dayjs months are 0-indexed
+ allGroups.day = sourceDate.date();
+ allGroups.year = sourceDate.year();
+
+ // Format 24-hour start time string
if (minute > 0) {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
} else {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:00`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:00`;
}
// Convert to 12-hour format
@@ -241,15 +384,69 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
hour12 = hour24 - 12;
}
- // Format 12-hour time string
+ // Format 12-hour start time string
if (minute > 0) {
- allGroups.time = `${hour12}:${minute.toString().padStart(2, '0')} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12}:${minute.toString().padStart(2, '0')} ${ampmDisplay}`;
} else {
- allGroups.time = `${hour12} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12} ${ampmDisplay}`;
}
+
+ // Format long version that always includes minutes
+ allGroups.starttime_long = `${hour12}:${minute.toString().padStart(2, '0')} ${ampmDisplay}`;
}
+
+ // Calculate end time based on program duration
+ const programDuration =
+ form.values.custom_properties?.program_duration || 180;
+
+ // Calculate end time by adding duration to start time
+ const startMinutes = hour24 * 60 + minute;
+ const endMinutes = startMinutes + programDuration;
+
+ let endHour24 = Math.floor(endMinutes / 60) % 24; // Wrap around 24 hours
+ const endMinute = endMinutes % 60;
+
+ // Format 24-hour end time string
+ if (endMinute > 0) {
+ allGroups.endtime24 = `${endHour24.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`;
+ } else {
+ allGroups.endtime24 = `${endHour24.toString().padStart(2, '0')}:00`;
+ }
+
+ // Convert to 12-hour format for endtime
+ const endAmpmDisplay = endHour24 < 12 ? 'AM' : 'PM';
+ let endHour12 = endHour24;
+ if (endHour24 === 0) {
+ endHour12 = 12;
+ } else if (endHour24 > 12) {
+ endHour12 = endHour24 - 12;
+ }
+
+ // Format 12-hour end time string
+ if (endMinute > 0) {
+ allGroups.endtime = `${endHour12}:${endMinute.toString().padStart(2, '0')} ${endAmpmDisplay}`;
+ } else {
+ allGroups.endtime = `${endHour12} ${endAmpmDisplay}`;
+ }
+
+ // Format long version that always includes minutes
+ allGroups.endtime_long = `${endHour12}:${endMinute.toString().padStart(2, '0')} ${endAmpmDisplay}`;
+
+ // Store calculated placeholders for display in preview
+ result.calculatedPlaceholders = {
+ starttime: allGroups.starttime,
+ starttime24: allGroups.starttime24,
+ starttime_long: allGroups.starttime_long,
+ endtime: allGroups.endtime,
+ endtime24: allGroups.endtime24,
+ endtime_long: allGroups.endtime_long,
+ date: allGroups.date,
+ month: allGroups.month,
+ day: allGroups.day,
+ year: allGroups.year,
+ };
} catch (e) {
- // If parsing fails, leave time/time24 as placeholders
+ // If parsing fails, leave starttime/endtime as placeholders
console.error('Error formatting time:', e);
}
}
@@ -305,6 +502,30 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
);
}
+ // Format channel logo URL
+ if (channelLogoUrl && (result.titleMatch || result.timeMatch)) {
+ result.formattedChannelLogoUrl = channelLogoUrl.replace(
+ /\{(\w+)\}/g,
+ (match, key) => {
+ const value = allGroups[key];
+ // URL encode the value to handle spaces and special characters
+ return value ? encodeURIComponent(String(value)) : match;
+ }
+ );
+ }
+
+ // Format program poster URL
+ if (programPosterUrl && (result.titleMatch || result.timeMatch)) {
+ result.formattedProgramPosterUrl = programPosterUrl.replace(
+ /\{(\w+)\}/g,
+ (match, key) => {
+ const value = allGroups[key];
+ // URL encode the value to handle spaces and special characters
+ return value ? encodeURIComponent(String(value)) : match;
+ }
+ );
+ }
+
return result;
}, [
titlePattern,
@@ -317,8 +538,11 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
upcomingDescriptionTemplate,
endedTitleTemplate,
endedDescriptionTemplate,
+ channelLogoUrl,
+ programPosterUrl,
form.values.custom_properties?.timezone,
form.values.custom_properties?.output_timezone,
+ form.values.custom_properties?.program_duration,
]);
useEffect(() => {
@@ -347,11 +571,17 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
custom.upcoming_description_template || '',
ended_title_template: custom.ended_title_template || '',
ended_description_template: custom.ended_description_template || '',
+ fallback_title_template: custom.fallback_title_template || '',
+ fallback_description_template:
+ custom.fallback_description_template || '',
+ channel_logo_url: custom.channel_logo_url || '',
+ program_poster_url: custom.program_poster_url || '',
name_source: custom.name_source || 'channel',
stream_index: custom.stream_index || 1,
category: custom.category || '',
include_date: custom.include_date ?? true,
include_live: custom.include_live ?? false,
+ include_new: custom.include_new ?? false,
},
});
@@ -368,6 +598,12 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
);
setEndedTitleTemplate(custom.ended_title_template || '');
setEndedDescriptionTemplate(custom.ended_description_template || '');
+ setFallbackTitleTemplate(custom.fallback_title_template || '');
+ setFallbackDescriptionTemplate(
+ custom.fallback_description_template || ''
+ );
+ setChannelLogoUrl(custom.channel_logo_url || '');
+ setProgramPosterUrl(custom.program_poster_url || '');
} else {
form.reset();
setTitlePattern('');
@@ -380,6 +616,10 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
setUpcomingDescriptionTemplate('');
setEndedTitleTemplate('');
setEndedDescriptionTemplate('');
+ setFallbackTitleTemplate('');
+ setFallbackDescriptionTemplate('');
+ setChannelLogoUrl('');
+ setProgramPosterUrl('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [epg]);
@@ -420,9 +660,84 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
fetchTimezones();
}, []);
+ // Function to import settings from an existing dummy EPG
+ const handleImportFromTemplate = (templateId) => {
+ const template = dummyEpgs.find((e) => e.id === parseInt(templateId));
+ if (!template) return;
+
+ const custom = template.custom_properties || {};
+
+ // Update form values
+ form.setValues({
+ name: `${template.name} (Copy)`,
+ is_active: template.is_active ?? true,
+ source_type: 'dummy',
+ custom_properties: {
+ title_pattern: custom.title_pattern || '',
+ time_pattern: custom.time_pattern || '',
+ date_pattern: custom.date_pattern || '',
+ timezone:
+ custom.timezone || custom.timezone_offset?.toString() || 'US/Eastern',
+ output_timezone: custom.output_timezone || '',
+ program_duration: custom.program_duration || 180,
+ sample_title: custom.sample_title || '',
+ title_template: custom.title_template || '',
+ description_template: custom.description_template || '',
+ upcoming_title_template: custom.upcoming_title_template || '',
+ upcoming_description_template:
+ custom.upcoming_description_template || '',
+ ended_title_template: custom.ended_title_template || '',
+ ended_description_template: custom.ended_description_template || '',
+ fallback_title_template: custom.fallback_title_template || '',
+ fallback_description_template:
+ custom.fallback_description_template || '',
+ channel_logo_url: custom.channel_logo_url || '',
+ program_poster_url: custom.program_poster_url || '',
+ name_source: custom.name_source || 'channel',
+ stream_index: custom.stream_index || 1,
+ category: custom.category || '',
+ include_date: custom.include_date ?? true,
+ include_live: custom.include_live ?? false,
+ include_new: custom.include_new ?? false,
+ },
+ });
+
+ // Update all individual state variables to match
+ setTitlePattern(custom.title_pattern || '');
+ setTimePattern(custom.time_pattern || '');
+ setDatePattern(custom.date_pattern || '');
+ setSampleTitle(custom.sample_title || '');
+ setTitleTemplate(custom.title_template || '');
+ setDescriptionTemplate(custom.description_template || '');
+ setUpcomingTitleTemplate(custom.upcoming_title_template || '');
+ setUpcomingDescriptionTemplate(custom.upcoming_description_template || '');
+ setEndedTitleTemplate(custom.ended_title_template || '');
+ setEndedDescriptionTemplate(custom.ended_description_template || '');
+ setFallbackTitleTemplate(custom.fallback_title_template || '');
+ setFallbackDescriptionTemplate(custom.fallback_description_template || '');
+ setChannelLogoUrl(custom.channel_logo_url || '');
+ setProgramPosterUrl(custom.program_poster_url || '');
+
+ notifications.show({
+ title: 'Template Imported',
+ message: `Settings imported from "${template.name}". Don't forget to change the name!`,
+ color: 'blue',
+ });
+ };
+
const handleSubmit = async (values) => {
try {
if (epg?.id) {
+ // Validate that we have a valid EPG object before updating
+ if (!epg || typeof epg !== 'object' || !epg.id) {
+ notifications.show({
+ title: 'Error',
+ message: 'Invalid EPG data. Please close and reopen this form.',
+ color: 'red',
+ });
+ return;
+ }
+
await API.updateEPG({ ...values, id: epg.id });
notifications.show({
title: 'Success',
@@ -456,6 +771,32 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
>