Enhancement: 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.
Some checks failed
CI Pipeline / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Has been cancelled
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
CI Pipeline / create-manifest (push) Has been cancelled

This commit is contained in:
SergeantPanda 2026-02-17 13:15:19 -06:00
parent 47853bdb5b
commit 0aa5986c12
2 changed files with 497 additions and 325 deletions

View file

@ -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

View file

@ -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 }) => (
<Group spacing={4} align="center">
<Text size="sm" fw={500}>
{label}
{required && (
<Text component="span" c="red" ml={4}>
*
</Text>
)}
</Text>
<Popover width={300} position="top" withArrow shadow="md">
<Popover.Target>
<ActionIcon size="xs" variant="subtle" color="gray">
<Info size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<Text size="xs">{info}</Text>
</Popover.Dropdown>
</Popover>
</Group>
);
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 */}
<Divider label="Pattern Configuration" labelPosition="center" />
{/* Accordion for organized sections */}
<Accordion defaultValue="patterns" variant="separated">
<Accordion.Item value="patterns">
<Accordion.Control>Pattern Configuration</Accordion.Control>
<Accordion.Panel>
<Stack spacing="md">
<Text size="sm" c="dimmed">
Define regex patterns to extract information from channel
titles or stream names. Use named capture groups like
(?&lt;groupname&gt;pattern).
</Text>
<Text size="sm" c="dimmed">
Define regex patterns to extract information from channel titles or
stream names. Use named capture groups like
(?&lt;groupname&gt;pattern).
</Text>
<Select
label={
<LabelWithInfo
label="Name Source"
info="Choose whether to parse the channel name or a stream name assigned to the channel"
required
/>
}
required
withAsterisk={false}
data={[
{ value: 'channel', label: 'Channel Name' },
{ value: 'stream', label: 'Stream Name' },
]}
{...form.getInputProps('custom_properties.name_source')}
/>
<Select
label="Name Source"
description="Choose whether to parse the channel name or a stream name assigned to the channel"
required
data={[
{ value: 'channel', label: 'Channel Name' },
{ value: 'stream', label: 'Stream Name' },
]}
{...form.getInputProps('custom_properties.name_source')}
/>
{form.values.custom_properties?.name_source === 'stream' && (
<NumberInput
label={
<LabelWithInfo
label="Stream Index"
info="Which stream to use (1 = first stream, 2 = second stream, etc.)"
/>
}
placeholder="1"
min={1}
max={100}
{...form.getInputProps('custom_properties.stream_index')}
/>
)}
{form.values.custom_properties?.name_source === 'stream' && (
<NumberInput
label="Stream Index"
description="Which stream to use (1 = first stream, 2 = second stream, etc.)"
placeholder="1"
min={1}
max={100}
{...form.getInputProps('custom_properties.stream_index')}
/>
)}
<TextInput
id="title_pattern"
name="title_pattern"
label={
<LabelWithInfo
label="Title Pattern"
info="Regex pattern to extract title information (e.g., team names, league). Example: (?<league>\w+) \d+: (?<team1>.*) VS (?<team2>.*)"
required
/>
}
placeholder="(?<league>\w+) \d+: (?<team1>.*) VS (?<team2>.*)"
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']}
/>
<TextInput
id="title_pattern"
name="title_pattern"
label="Title Pattern"
description="Regex pattern to extract title information (e.g., team names, league). Example: (?<league>\w+) \d+: (?<team1>.*) VS (?<team2>.*)"
placeholder="(?<league>\w+) \d+: (?<team1>.*) VS (?<team2>.*)"
required
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']}
/>
<TextInput
id="time_pattern"
name="time_pattern"
label={
<LabelWithInfo
label="Time Pattern (Optional)"
info="Extract time from channel titles. Required groups: 'hour' (1-12 or 0-23), 'minute' (0-59), 'ampm' (AM/PM - optional for 24-hour). Examples: @ (?<hour>\d+):(?<minute>\d+)(?<ampm>AM|PM) for '8:30PM' OR @ (?<hour>\d{1,2}):(?<minute>\d{2}) for '20:30'"
/>
}
placeholder="@ (?<hour>\d+):(?<minute>\d+)(?<ampm>AM|PM)"
value={timePattern}
onChange={(e) => {
const value = e.target.value;
setTimePattern(value);
form.setFieldValue(
'custom_properties.time_pattern',
value
);
}}
/>
<TextInput
id="time_pattern"
name="time_pattern"
label="Time Pattern (Optional)"
description="Extract time from channel titles. Required groups: 'hour' (1-12 or 0-23), 'minute' (0-59), 'ampm' (AM/PM - optional for 24-hour). Examples: @ (?<hour>\d+):(?<minute>\d+)(?<ampm>AM|PM) for '8:30PM' OR @ (?<hour>\d{1,2}):(?<minute>\d{2}) for '20:30'"
placeholder="@ (?<hour>\d+):(?<minute>\d+)(?<ampm>AM|PM)"
value={timePattern}
onChange={(e) => {
const value = e.target.value;
setTimePattern(value);
form.setFieldValue('custom_properties.time_pattern', value);
}}
/>
<TextInput
id="date_pattern"
name="date_pattern"
label={
<LabelWithInfo
label="Date Pattern (Optional)"
info="Extract date from channel titles. Groups: 'month' (name or number), 'day', 'year' (optional, defaults to current year). Examples: @ (?<month>\w+) (?<day>\d+) for 'Oct 17' OR (?<month>\d+)/(?<day>\d+)/(?<year>\d+) for '10/17/2025'"
/>
}
placeholder="@ (?<month>\w+) (?<day>\d+)"
value={datePattern}
onChange={(e) => {
const value = e.target.value;
setDatePattern(value);
form.setFieldValue(
'custom_properties.date_pattern',
value
);
}}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
<TextInput
id="date_pattern"
name="date_pattern"
label="Date Pattern (Optional)"
description="Extract date from channel titles. Groups: 'month' (name or number), 'day', 'year' (optional, defaults to current year). Examples: @ (?<month>\w+) (?<day>\d+) for 'Oct 17' OR (?<month>\d+)/(?<day>\d+)/(?<year>\d+) for '10/17/2025'"
placeholder="@ (?<month>\w+) (?<day>\d+)"
value={datePattern}
onChange={(e) => {
const value = e.target.value;
setDatePattern(value);
form.setFieldValue('custom_properties.date_pattern', value);
}}
/>
<Accordion.Item value="templates">
<Accordion.Control>Output Templates</Accordion.Control>
<Accordion.Panel>
<Stack spacing="md">
<Text size="sm" c="dimmed">
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.
</Text>
{/* Output Templates */}
<Divider label="Output Templates (Optional)" labelPosition="center" />
<TextInput
id="title_template"
name="title_template"
label={
<LabelWithInfo
label="Title Template"
info="Format the EPG title using extracted groups. Use {starttime} (12-hour: '10 PM'), {starttime24} (24-hour: '22:00'), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: {league} - {team1} vs {team2} ({starttime}-{endtime})"
/>
}
placeholder="{league} - {team1} vs {team2}"
value={titleTemplate}
onChange={(e) => {
const value = e.target.value;
setTitleTemplate(value);
form.setFieldValue(
'custom_properties.title_template',
value
);
}}
/>
<Text size="sm" c="dimmed">
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.
</Text>
<TextInput
id="subtitle_template"
name="subtitle_template"
label={
<LabelWithInfo
label="Subtitle Template (Optional)"
info="Format the EPG subtitle using extracted groups. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Example: {starttime} - {endtime}"
/>
}
placeholder="{starttime} - {endtime}"
value={subtitleTemplate}
onChange={(e) => {
const value = e.target.value;
setSubtitleTemplate(value);
form.setFieldValue(
'custom_properties.subtitle_template',
value
);
}}
/>
<TextInput
id="title_template"
name="title_template"
label="Title Template"
description="Format the EPG title using extracted groups. Use {starttime} (12-hour: '10 PM'), {starttime24} (24-hour: '22:00'), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: {league} - {team1} vs {team2} ({starttime}-{endtime})"
placeholder="{league} - {team1} vs {team2}"
value={titleTemplate}
onChange={(e) => {
const value = e.target.value;
setTitleTemplate(value);
form.setFieldValue('custom_properties.title_template', value);
}}
/>
<Textarea
id="description_template"
name="description_template"
label={
<LabelWithInfo
label="Description Template"
info="Format the EPG description using extracted groups. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: Watch {team1} take on {team2} on {date} from {starttime} to {endtime}!"
/>
}
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
);
}}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
<TextInput
id="subtitle_template"
name="subtitle_template"
label="Subtitle Template (Optional)"
description="Format the EPG subtitle using extracted groups. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Example: {starttime} - {endtime}"
placeholder="{starttime} - {endtime}"
value={subtitleTemplate}
onChange={(e) => {
const value = e.target.value;
setSubtitleTemplate(value);
form.setFieldValue('custom_properties.subtitle_template', value);
}}
/>
<Accordion.Item value="upcoming-ended">
<Accordion.Control>Upcoming/Ended Templates</Accordion.Control>
<Accordion.Panel>
<Stack spacing="md">
<Text size="sm" c="dimmed">
Customize how programs appear before and after the event. If
left empty, will use the main title/description with
"Upcoming:" or "Ended:" prefix.
</Text>
<Textarea
id="description_template"
name="description_template"
label="Description Template"
description="Format the EPG description using extracted groups. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: Watch {team1} take on {team2} on {date} from {starttime} to {endtime}!"
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
);
}}
/>
<TextInput
id="upcoming_title_template"
name="upcoming_title_template"
label={
<LabelWithInfo
label="Upcoming Title Template"
info="Title for programs before the event starts. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: {team1} vs {team2} starting at {starttime}."
/>
}
placeholder="{team1} vs {team2} starting at {starttime}."
value={upcomingTitleTemplate}
onChange={(e) => {
const value = e.target.value;
setUpcomingTitleTemplate(value);
form.setFieldValue(
'custom_properties.upcoming_title_template',
value
);
}}
/>
{/* Upcoming/Ended Templates */}
<Divider
label="Upcoming/Ended Templates (Optional)"
labelPosition="center"
/>
<Textarea
id="upcoming_description_template"
name="upcoming_description_template"
label={
<LabelWithInfo
label="Upcoming Description Template"
info="Description for programs before the event. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: Upcoming: Watch the {league} match up where the {team1} take on the {team2} on {date} from {starttime} to {endtime}!"
/>
}
placeholder="Upcoming: Watch the {league} match up where the {team1} take on the {team2} from {starttime} to {endtime}!"
minRows={2}
value={upcomingDescriptionTemplate}
onChange={(e) => {
const value = e.target.value;
setUpcomingDescriptionTemplate(value);
form.setFieldValue(
'custom_properties.upcoming_description_template',
value
);
}}
/>
<Text size="sm" c="dimmed">
Customize how programs appear before and after the event. If left
empty, will use the main title/description with "Upcoming:" or
"Ended:" prefix.
</Text>
<TextInput
id="ended_title_template"
name="ended_title_template"
label={
<LabelWithInfo
label="Ended Title Template"
info="Title for programs after the event has ended. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: {team1} vs {team2} started at {starttime}."
/>
}
placeholder="{team1} vs {team2} started at {starttime}."
value={endedTitleTemplate}
onChange={(e) => {
const value = e.target.value;
setEndedTitleTemplate(value);
form.setFieldValue(
'custom_properties.ended_title_template',
value
);
}}
/>
<TextInput
id="upcoming_title_template"
name="upcoming_title_template"
label="Upcoming Title Template"
description="Title for programs before the event starts. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: {team1} vs {team2} starting at {starttime}."
placeholder="{team1} vs {team2} starting at {starttime}."
value={upcomingTitleTemplate}
onChange={(e) => {
const value = e.target.value;
setUpcomingTitleTemplate(value);
form.setFieldValue(
'custom_properties.upcoming_title_template',
value
);
}}
/>
<Textarea
id="ended_description_template"
name="ended_description_template"
label={
<LabelWithInfo
label="Ended Description Template"
info="Description for programs after the event. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: The {league} match between {team1} and {team2} on {date} ran from {starttime} to {endtime}."
/>
}
placeholder="The {league} match between {team1} and {team2} ran from {starttime} to {endtime}."
minRows={2}
value={endedDescriptionTemplate}
onChange={(e) => {
const value = e.target.value;
setEndedDescriptionTemplate(value);
form.setFieldValue(
'custom_properties.ended_description_template',
value
);
}}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
<Textarea
id="upcoming_description_template"
name="upcoming_description_template"
label="Upcoming Description Template"
description="Description for programs before the event. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: Upcoming: Watch the {league} match up where the {team1} take on the {team2} on {date} from {starttime} to {endtime}!"
placeholder="Upcoming: Watch the {league} match up where the {team1} take on the {team2} from {starttime} to {endtime}!"
minRows={2}
value={upcomingDescriptionTemplate}
onChange={(e) => {
const value = e.target.value;
setUpcomingDescriptionTemplate(value);
form.setFieldValue(
'custom_properties.upcoming_description_template',
value
);
}}
/>
<Accordion.Item value="fallback">
<Accordion.Control>Fallback Templates</Accordion.Control>
<Accordion.Panel>
<Stack spacing="md">
<Text size="sm" c="dimmed">
When patterns don't match the channel/stream name, use these
custom fallback templates instead of the default placeholder
messages. Leave empty to use the built-in humorous fallback
descriptions.
</Text>
<TextInput
id="ended_title_template"
name="ended_title_template"
label="Ended Title Template"
description="Title for programs after the event has ended. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: {team1} vs {team2} started at {starttime}."
placeholder="{team1} vs {team2} started at {starttime}."
value={endedTitleTemplate}
onChange={(e) => {
const value = e.target.value;
setEndedTitleTemplate(value);
form.setFieldValue(
'custom_properties.ended_title_template',
value
);
}}
/>
<TextInput
id="fallback_title_template"
name="fallback_title_template"
label={
<LabelWithInfo
label="Fallback Title Template"
info="Custom title when patterns don't match. If empty, uses the channel/stream name."
/>
}
placeholder="No EPG data available"
value={fallbackTitleTemplate}
onChange={(e) => {
const value = e.target.value;
setFallbackTitleTemplate(value);
form.setFieldValue(
'custom_properties.fallback_title_template',
value
);
}}
/>
<Textarea
id="ended_description_template"
name="ended_description_template"
label="Ended Description Template"
description="Description for programs after the event. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Date/time placeholders respect Output Timezone settings. Example: The {league} match between {team1} and {team2} on {date} ran from {starttime} to {endtime}."
placeholder="The {league} match between {team1} and {team2} ran from {starttime} to {endtime}."
minRows={2}
value={endedDescriptionTemplate}
onChange={(e) => {
const value = e.target.value;
setEndedDescriptionTemplate(value);
form.setFieldValue(
'custom_properties.ended_description_template',
value
);
}}
/>
<Textarea
id="fallback_description_template"
name="fallback_description_template"
label={
<LabelWithInfo
label="Fallback Description Template"
info="Custom description when patterns don't match. If empty, uses built-in placeholder messages."
/>
}
placeholder="EPG information is currently unavailable for this channel."
minRows={2}
value={fallbackDescriptionTemplate}
onChange={(e) => {
const value = e.target.value;
setFallbackDescriptionTemplate(value);
form.setFieldValue(
'custom_properties.fallback_description_template',
value
);
}}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
{/* Fallback Templates */}
<Divider
label="Fallback Templates (Optional)"
labelPosition="center"
/>
<Accordion.Item value="settings">
<Accordion.Control>EPG Settings</Accordion.Control>
<Accordion.Panel>
<Stack spacing="md">
<Select
label={
<LabelWithInfo
label="Event Timezone"
info="The timezone of the event times in your channel titles. DST (Daylight Saving Time) is handled automatically! All timezones supported by pytz are available."
/>
}
placeholder={
loadingTimezones
? 'Loading timezones...'
: 'Select timezone'
}
data={timezoneOptions}
searchable
disabled={loadingTimezones}
{...form.getInputProps('custom_properties.timezone')}
/>
<Text size="sm" c="dimmed">
When patterns don't match the channel/stream name, use these custom
fallback templates instead of the default placeholder messages.
Leave empty to use the built-in humorous fallback descriptions.
</Text>
<Select
label={
<LabelWithInfo
label="Output Timezone (Optional)"
info="Display times in a different timezone than the event timezone. Leave empty to use the event timezone. Example: Event at 10 PM ET displayed as 9 PM CT."
/>
}
placeholder="Same as event timezone"
data={timezoneOptions}
searchable
clearable
disabled={loadingTimezones}
{...form.getInputProps('custom_properties.output_timezone')}
/>
<TextInput
id="fallback_title_template"
name="fallback_title_template"
label="Fallback Title Template"
description="Custom title when patterns don't match. If empty, uses the channel/stream name."
placeholder="No EPG data available"
value={fallbackTitleTemplate}
onChange={(e) => {
const value = e.target.value;
setFallbackTitleTemplate(value);
form.setFieldValue(
'custom_properties.fallback_title_template',
value
);
}}
/>
<NumberInput
label={
<LabelWithInfo
label="Program Duration (minutes)"
info="Default duration for each program"
/>
}
placeholder="180"
min={1}
max={1440}
{...form.getInputProps(
'custom_properties.program_duration'
)}
/>
<Textarea
id="fallback_description_template"
name="fallback_description_template"
label="Fallback Description Template"
description="Custom description when patterns don't match. If empty, uses built-in placeholder messages."
placeholder="EPG information is currently unavailable for this channel."
minRows={2}
value={fallbackDescriptionTemplate}
onChange={(e) => {
const value = e.target.value;
setFallbackDescriptionTemplate(value);
form.setFieldValue(
'custom_properties.fallback_description_template',
value
);
}}
/>
<TextInput
label={
<LabelWithInfo
label="Categories (Optional)"
info="EPG categories for these programs. Use commas to separate multiple (e.g., Sports, Live, HD). Note: Only added to the main event, not upcoming/ended filler programs."
/>
}
placeholder="Sports, Live"
{...form.getInputProps('custom_properties.category')}
/>
{/* EPG Settings */}
<Divider label="EPG Settings" labelPosition="center" />
<TextInput
label={
<LabelWithInfo
label="Channel Logo URL (Optional)"
info="Build a URL for the channel logo using regex groups. Example: https://example.com/logos/{league_normalize}/{team1_normalize}.png. Use {groupname_normalize} for cleaner URLs (alphanumeric-only, lowercase). This will be used as the channel <icon> in the EPG output."
/>
}
placeholder="https://example.com/logos/{league_normalize}/{team1_normalize}.png"
value={channelLogoUrl}
onChange={(e) => {
const value = e.target.value;
setChannelLogoUrl(value);
form.setFieldValue(
'custom_properties.channel_logo_url',
value
);
}}
/>
<Select
label="Event Timezone"
description="The timezone of the event times in your channel titles. DST (Daylight Saving Time) is handled automatically! All timezones supported by pytz are available."
placeholder={
loadingTimezones ? 'Loading timezones...' : 'Select timezone'
}
data={timezoneOptions}
searchable
disabled={loadingTimezones}
{...form.getInputProps('custom_properties.timezone')}
/>
<TextInput
label={
<LabelWithInfo
label="Program Poster URL (Optional)"
info="Build a URL for the program poster/icon using regex groups. Example: https://example.com/posters/{team1_normalize}-vs-{team2_normalize}.jpg. Use {groupname_normalize} for cleaner URLs (alphanumeric-only, lowercase). This will be used as the program <icon> in the EPG output."
/>
}
placeholder="https://example.com/posters/{team1_normalize}-vs-{team2_normalize}.jpg"
value={programPosterUrl}
onChange={(e) => {
const value = e.target.value;
setProgramPosterUrl(value);
form.setFieldValue(
'custom_properties.program_poster_url',
value
);
}}
/>
<Select
label="Output Timezone (Optional)"
description="Display times in a different timezone than the event timezone. Leave empty to use the event timezone. Example: Event at 10 PM ET displayed as 9 PM CT."
placeholder="Same as event timezone"
data={timezoneOptions}
searchable
clearable
disabled={loadingTimezones}
{...form.getInputProps('custom_properties.output_timezone')}
/>
<Checkbox
label={
<LabelWithInfo
label="Include Date Tag"
info="Include the <date> tag in EPG output with the program's start date (YYYY-MM-DD format). Added to all programs."
/>
}
{...form.getInputProps('custom_properties.include_date', {
type: 'checkbox',
})}
/>
<NumberInput
label="Program Duration (minutes)"
description="Default duration for each program"
placeholder="180"
min={1}
max={1440}
{...form.getInputProps('custom_properties.program_duration')}
/>
<Checkbox
label={
<LabelWithInfo
label="Include Live Tag"
info="Mark programs as live content with the <live /> tag in EPG output. Note: Only added to the main event, not upcoming/ended filler programs."
/>
}
{...form.getInputProps('custom_properties.include_live', {
type: 'checkbox',
})}
/>
<TextInput
label="Categories (Optional)"
description="EPG categories for these programs. Use commas to separate multiple (e.g., Sports, Live, HD). Note: Only added to the main event, not upcoming/ended filler programs."
placeholder="Sports, Live"
{...form.getInputProps('custom_properties.category')}
/>
<TextInput
label="Channel Logo URL (Optional)"
description="Build a URL for the channel logo using regex groups. Example: https://example.com/logos/{league_normalize}/{team1_normalize}.png. Use {groupname_normalize} for cleaner URLs (alphanumeric-only, lowercase). This will be used as the channel <icon> in the EPG output."
placeholder="https://example.com/logos/{league_normalize}/{team1_normalize}.png"
value={channelLogoUrl}
onChange={(e) => {
const value = e.target.value;
setChannelLogoUrl(value);
form.setFieldValue('custom_properties.channel_logo_url', value);
}}
/>
<TextInput
label="Program Poster URL (Optional)"
description="Build a URL for the program poster/icon using regex groups. Example: https://example.com/posters/{team1_normalize}-vs-{team2_normalize}.jpg. Use {groupname_normalize} for cleaner URLs (alphanumeric-only, lowercase). This will be used as the program <icon> in the EPG output."
placeholder="https://example.com/posters/{team1_normalize}-vs-{team2_normalize}.jpg"
value={programPosterUrl}
onChange={(e) => {
const value = e.target.value;
setProgramPosterUrl(value);
form.setFieldValue('custom_properties.program_poster_url', value);
}}
/>
<Checkbox
label="Include Date Tag"
description="Include the <date> tag in EPG output with the program's start date (YYYY-MM-DD format). Added to all programs."
{...form.getInputProps('custom_properties.include_date', {
type: 'checkbox',
})}
/>
<Checkbox
label="Include Live Tag"
description="Mark programs as live content with the <live /> tag in EPG output. Note: Only added to the main event, not upcoming/ended filler programs."
{...form.getInputProps('custom_properties.include_live', {
type: 'checkbox',
})}
/>
<Checkbox
label="Include New Tag"
description="Mark programs as new content with the <new /> tag in EPG output. Note: Only added to the main event, not upcoming/ended filler programs."
{...form.getInputProps('custom_properties.include_new', {
type: 'checkbox',
})}
/>
<Checkbox
label={
<LabelWithInfo
label="Include New Tag"
info="Mark programs as new content with the <new /> tag in EPG output. Note: Only added to the main event, not upcoming/ended filler programs."
/>
}
{...form.getInputProps('custom_properties.include_new', {
type: 'checkbox',
})}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
{/* Testing & Preview */}
<Divider label="Test Your Configuration" labelPosition="center" />
@ -1186,8 +1353,12 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
<TextInput
id="sample_title"
name="sample_title"
label={`Sample ${form.values.custom_properties?.name_source === 'stream' ? 'Stream' : 'Channel'} Name`}
description={`Enter a sample ${form.values.custom_properties?.name_source === 'stream' ? 'stream name' : 'channel name'} to test pattern matching and see the formatted output`}
label={
<LabelWithInfo
label={`Sample ${form.values.custom_properties?.name_source === 'stream' ? 'Stream' : 'Channel'} Name`}
info={`Enter a sample ${form.values.custom_properties?.name_source === 'stream' ? 'stream name' : 'channel name'} to test pattern matching and see the formatted output`}
/>
}
placeholder="League 01: Team 1 VS Team 2 @ Oct 17 8:00PM ET"
value={sampleTitle}
onChange={(e) => {