diff --git a/CHANGELOG.md b/CHANGELOG.md
index da99fb5c..f7c10fbe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Custom Dummy EPG form UI improvements: Reorganized the form into collapsible accordion sections (Pattern Configuration, Output Templates, Upcoming/Ended Templates, Fallback Templates, EPG Settings) for better organization. Field descriptions now appear in info icon popovers instead of taking up vertical space, making the form more compact and easier to navigate while keeping help text accessible.
- XC API M3U stream URLs: M3U generation for Xtream Codes API endpoints now use proper XC-style stream URLs (`/live/username/password/channel_id`) instead of UUID-based stream endpoints, ensuring full compatibility with XC clients. (Fixes #839)
### Fixed
diff --git a/frontend/src/components/forms/DummyEPG.jsx b/frontend/src/components/forms/DummyEPG.jsx
index 06731ac9..701dafd7 100644
--- a/frontend/src/components/forms/DummyEPG.jsx
+++ b/frontend/src/components/forms/DummyEPG.jsx
@@ -1,5 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
+ Accordion,
+ ActionIcon,
Box,
Button,
Checkbox,
@@ -8,12 +10,14 @@ import {
Modal,
NumberInput,
Paper,
+ Popover,
Select,
Stack,
Text,
TextInput,
Textarea,
} from '@mantine/core';
+import { Info } from 'lucide-react';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import API from '../../api';
@@ -26,6 +30,30 @@ import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
+// Helper component for labels with info popover
+const LabelWithInfo = ({ label, info, required }) => (
+
+
+ {label}
+ {required && (
+
+ *
+
+ )}
+
+
+
+
+
+
+
+
+ {info}
+
+
+
+);
+
const DummyEPGForm = ({ epg, isOpen, onClose }) => {
// Get all EPGs from the store
const epgs = useEPGsStore((state) => state.epgs);
@@ -822,355 +850,494 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
{...form.getInputProps('name')}
/>
- {/* Pattern Configuration */}
-
+ {/* Accordion for organized sections */}
+
+
+ Pattern Configuration
+
+
+
+ Define regex patterns to extract information from channel
+ titles or stream names. Use named capture groups like
+ (?<groupname>pattern).
+
-
- Define regex patterns to extract information from channel titles or
- stream names. Use named capture groups like
- (?<groupname>pattern).
-
+
+ }
+ required
+ withAsterisk={false}
+ data={[
+ { value: 'channel', label: 'Channel Name' },
+ { value: 'stream', label: 'Stream Name' },
+ ]}
+ {...form.getInputProps('custom_properties.name_source')}
+ />
-
+ {form.values.custom_properties?.name_source === 'stream' && (
+
+ }
+ placeholder="1"
+ min={1}
+ max={100}
+ {...form.getInputProps('custom_properties.stream_index')}
+ />
+ )}
- {form.values.custom_properties?.name_source === 'stream' && (
-
- )}
+
+ }
+ placeholder="(?\w+) \d+: (?.*) VS (?.*)"
+ required
+ withAsterisk={false}
+ value={titlePattern}
+ onChange={(e) => {
+ const value = e.target.value;
+ setTitlePattern(value);
+ form.setFieldValue(
+ 'custom_properties.title_pattern',
+ value
+ );
+ }}
+ error={form.errors['custom_properties.title_pattern']}
+ />
- {
- const value = e.target.value;
- setTitlePattern(value);
- form.setFieldValue('custom_properties.title_pattern', value);
- }}
- error={form.errors['custom_properties.title_pattern']}
- />
+
+ }
+ placeholder="@ (?\d+):(?\d+)(?AM|PM)"
+ value={timePattern}
+ onChange={(e) => {
+ const value = e.target.value;
+ setTimePattern(value);
+ form.setFieldValue(
+ 'custom_properties.time_pattern',
+ value
+ );
+ }}
+ />
- {
- const value = e.target.value;
- setTimePattern(value);
- form.setFieldValue('custom_properties.time_pattern', value);
- }}
- />
+
+ }
+ placeholder="@ (?\w+) (?\d+)"
+ value={datePattern}
+ onChange={(e) => {
+ const value = e.target.value;
+ setDatePattern(value);
+ form.setFieldValue(
+ 'custom_properties.date_pattern',
+ value
+ );
+ }}
+ />
+
+
+
- {
- const value = e.target.value;
- setDatePattern(value);
- form.setFieldValue('custom_properties.date_pattern', value);
- }}
- />
+
+ Output Templates
+
+
+
+ Use extracted groups from your patterns to format EPG titles
+ and descriptions. Reference groups using {'{groupname}'}{' '}
+ syntax. For cleaner URLs, use {'{groupname_normalize}'} to
+ get alphanumeric-only lowercase versions.
+
- {/* Output Templates */}
-
+
+ }
+ placeholder="{league} - {team1} vs {team2}"
+ value={titleTemplate}
+ onChange={(e) => {
+ const value = e.target.value;
+ setTitleTemplate(value);
+ form.setFieldValue(
+ 'custom_properties.title_template',
+ value
+ );
+ }}
+ />
-
- Use extracted groups from your patterns to format EPG titles and
- descriptions. Reference groups using {'{groupname}'} syntax. For
- cleaner URLs, use {'{groupname_normalize}'} to get alphanumeric-only
- lowercase versions.
-
+
+ }
+ placeholder="{starttime} - {endtime}"
+ value={subtitleTemplate}
+ onChange={(e) => {
+ const value = e.target.value;
+ setSubtitleTemplate(value);
+ form.setFieldValue(
+ 'custom_properties.subtitle_template',
+ value
+ );
+ }}
+ />
- {
- const value = e.target.value;
- setTitleTemplate(value);
- form.setFieldValue('custom_properties.title_template', value);
- }}
- />
+
+ }
+ placeholder="Watch {team1} take on {team2} in this exciting {league} matchup from {starttime} to {endtime}!"
+ minRows={2}
+ value={descriptionTemplate}
+ onChange={(e) => {
+ const value = e.target.value;
+ setDescriptionTemplate(value);
+ form.setFieldValue(
+ 'custom_properties.description_template',
+ value
+ );
+ }}
+ />
+
+
+
- {
- const value = e.target.value;
- setSubtitleTemplate(value);
- form.setFieldValue('custom_properties.subtitle_template', value);
- }}
- />
+
+ Upcoming/Ended Templates
+
+
+
+ Customize how programs appear before and after the event. If
+ left empty, will use the main title/description with
+ "Upcoming:" or "Ended:" prefix.
+
-
+
+
-
{/* Testing & Preview */}
@@ -1186,8 +1353,12 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
+ }
placeholder="League 01: Team 1 VS Team 2 @ Oct 17 8:00PM ET"
value={sampleTitle}
onChange={(e) => {