mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-22 18:28:00 +00:00
Refactor/Enhancement: Refactored channel numbering dialogs into a unified CreateChannelModal component that now includes channel profile selection alongside channel number assignment for both single and bulk channel creation. Users can choose to add channels to all profiles, no profiles, or specific profiles with mutual exclusivity between special options ("All Profiles", "None") and specific profile selections. Profile selection defaults to the current table filter for intuitive workflow.
This commit is contained in:
parent
6b873be3cf
commit
b8374fcc68
4 changed files with 409 additions and 151 deletions
|
|
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Sentinel value `[0]`: Channels are added to ALL profiles (explicit)
|
||||
- Specific IDs `[1, 2, ...]`: Channels are added only to the specified profiles
|
||||
This allows API consumers to control profile membership across all channel creation methods without requiring all channels to be added to every profile by default.
|
||||
- Channel profile selection in creation modal: Users can now choose which profiles to add channels to when creating channels from streams (both single and bulk operations). Options include adding to all profiles, no profiles, or specific profiles with mutual exclusivity between special options ("All Profiles", "None") and specific profile selections. Profile selection defaults to the current table filter for intuitive workflow.
|
||||
- Group retention policy for M3U accounts: Groups now follow the same stale retention logic as streams, using the account's `stale_stream_days` setting. Groups that temporarily disappear from an M3U source are retained for the configured retention period instead of being immediately deleted, preserving user settings and preventing data loss when providers temporarily remove/re-add groups. (Closes #809)
|
||||
- Visual stale indicators for streams and groups: Added `is_stale` field to Stream and both `is_stale` and `last_seen` fields to ChannelGroupM3UAccount models to track items in their retention grace period. Stale groups display with orange buttons and a warning tooltip, while stale streams show with a red background color matching the visual treatment of empty channels.
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- VOD upstream read timeout reduced from 30 seconds to 10 seconds to minimize lock hold time when clients disconnect during connection phase
|
||||
- Form management refactored across application: Migrated Channel, Stream, M3U Profile, Stream Profile, Logo, and User Agent forms from Formik to React Hook Form (RHF) with Yup validation for improved form handling, better validation feedback, and enhanced code maintainability
|
||||
- Stats and VOD pages refactored for clearer separation of concerns: extracted Stream/VOD connection cards (StreamConnectionCard, VodConnectionCard, VODCard, SeriesCard), moved page logic into dedicated utils, and lazy-loaded heavy components with ErrorBoundary fallbacks to improve readability and maintainability - Thanks [@nick4810](https://github.com/nick4810)
|
||||
- Channel creation modal refactored: Extracted and unified channel numbering dialogs from StreamsTable into a dedicated CreateChannelModal component that handles both single and bulk channel creation with cleaner, more maintainable implementation and integrated profile selection controls.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
122
frontend/src/components/modals/ChannelNumberingModal.jsx
Normal file
122
frontend/src/components/modals/ChannelNumberingModal.jsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
Radio,
|
||||
NumberInput,
|
||||
Checkbox,
|
||||
Group,
|
||||
Button,
|
||||
} from '@mantine/core';
|
||||
|
||||
const ChannelNumberingModal = ({
|
||||
opened,
|
||||
onClose,
|
||||
mode,
|
||||
onModeChange,
|
||||
numberValue,
|
||||
onNumberValueChange,
|
||||
rememberChoice,
|
||||
onRememberChoiceChange,
|
||||
onConfirm,
|
||||
// Props for customizing the modal behavior
|
||||
isBulk = false,
|
||||
streamCount = 1,
|
||||
streamName = '',
|
||||
}) => {
|
||||
const title = isBulk
|
||||
? 'Channel Numbering Options'
|
||||
: 'Channel Number Assignment';
|
||||
const confirmLabel = isBulk ? 'Create Channels' : 'Create Channel';
|
||||
const numberingLabel = isBulk ? 'Numbering Mode' : 'Number Assignment';
|
||||
|
||||
// For bulk: use 'custom' mode, for single: use 'specific' mode
|
||||
const customModeValue = isBulk ? 'custom' : 'specific';
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={title} size="md" centered>
|
||||
<Stack spacing="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{isBulk
|
||||
? `Choose how to assign channel numbers to the ${streamCount} selected streams:`
|
||||
: `Choose how to assign the channel number for "${streamName}":`}
|
||||
</Text>
|
||||
|
||||
<Radio.Group
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
label={numberingLabel}
|
||||
>
|
||||
<Stack mt="xs" spacing="xs">
|
||||
<Radio
|
||||
value="provider"
|
||||
label={isBulk ? 'Use Provider Numbers' : 'Use Provider Number'}
|
||||
description={
|
||||
isBulk
|
||||
? 'Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts'
|
||||
: 'Use tvg-chno or channel-number from stream metadata, auto-assign if not available'
|
||||
}
|
||||
/>
|
||||
<Radio
|
||||
value="auto"
|
||||
label={
|
||||
isBulk ? 'Auto-Assign Sequential' : 'Auto-Assign Next Available'
|
||||
}
|
||||
description={
|
||||
isBulk
|
||||
? 'Start from the lowest available channel number and increment by 1'
|
||||
: 'Automatically assign the next available channel number'
|
||||
}
|
||||
/>
|
||||
<Radio
|
||||
value={customModeValue}
|
||||
label={
|
||||
isBulk ? 'Start from Custom Number' : 'Use Specific Number'
|
||||
}
|
||||
description={
|
||||
isBulk
|
||||
? 'Start sequential numbering from a specific channel number'
|
||||
: 'Use a specific channel number'
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
{mode === customModeValue && (
|
||||
<NumberInput
|
||||
label={isBulk ? 'Starting Channel Number' : 'Channel Number'}
|
||||
description={
|
||||
isBulk
|
||||
? 'Channel numbers will be assigned starting from this number'
|
||||
: 'The specific channel number to assign'
|
||||
}
|
||||
value={numberValue}
|
||||
onChange={onNumberValueChange}
|
||||
min={1}
|
||||
placeholder={
|
||||
isBulk ? 'Enter starting number...' : 'Enter channel number...'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
checked={rememberChoice}
|
||||
onChange={(event) =>
|
||||
onRememberChoiceChange(event.currentTarget.checked)
|
||||
}
|
||||
label="Remember this choice and don't ask again"
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{confirmLabel}</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelNumberingModal;
|
||||
180
frontend/src/components/modals/CreateChannelModal.jsx
Normal file
180
frontend/src/components/modals/CreateChannelModal.jsx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
Radio,
|
||||
NumberInput,
|
||||
Checkbox,
|
||||
Group,
|
||||
Button,
|
||||
MultiSelect,
|
||||
Divider,
|
||||
} from '@mantine/core';
|
||||
|
||||
const CreateChannelModal = ({
|
||||
opened,
|
||||
onClose,
|
||||
mode,
|
||||
onModeChange,
|
||||
numberValue,
|
||||
onNumberValueChange,
|
||||
rememberChoice,
|
||||
onRememberChoiceChange,
|
||||
onConfirm,
|
||||
// Props for customizing the modal behavior
|
||||
isBulk = false,
|
||||
streamCount = 1,
|
||||
streamName = '',
|
||||
// Channel profile props
|
||||
selectedProfileIds,
|
||||
onProfileIdsChange,
|
||||
channelProfiles = [],
|
||||
}) => {
|
||||
const title = isBulk ? 'Create Channels Options' : 'Create Channel';
|
||||
const confirmLabel = isBulk ? 'Create Channels' : 'Create Channel';
|
||||
const numberingLabel = isBulk ? 'Numbering Mode' : 'Number Assignment';
|
||||
|
||||
// For bulk: use 'custom' mode, for single: use 'specific' mode
|
||||
const customModeValue = isBulk ? 'custom' : 'specific';
|
||||
|
||||
// Convert channel profiles to MultiSelect data format with groups
|
||||
// Filter out the "All" profile (id '0') and add our own special options
|
||||
const profileOptions = [
|
||||
{
|
||||
group: 'Special',
|
||||
items: [
|
||||
{ value: 'all', label: 'All Profiles' },
|
||||
{ value: 'none', label: 'No Profiles' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Profiles',
|
||||
items: channelProfiles
|
||||
.filter((profile) => profile.id.toString() !== '0')
|
||||
.map((profile) => ({
|
||||
value: profile.id.toString(),
|
||||
label: profile.name,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
// Handle profile selection with mutual exclusivity
|
||||
const handleProfileChange = (newValue) => {
|
||||
const lastSelected = newValue[newValue.length - 1];
|
||||
|
||||
// If 'all' or 'none' was just selected, clear everything else and keep only that
|
||||
if (lastSelected === 'all' || lastSelected === 'none') {
|
||||
onProfileIdsChange([lastSelected]);
|
||||
}
|
||||
// If a specific profile was selected, remove 'all' and 'none'
|
||||
else if (newValue.includes('all') || newValue.includes('none')) {
|
||||
onProfileIdsChange(newValue.filter((v) => v !== 'all' && v !== 'none'));
|
||||
}
|
||||
// Otherwise just update normally
|
||||
else {
|
||||
onProfileIdsChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={title} size="md" centered>
|
||||
<Stack spacing="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{isBulk
|
||||
? `Configure options for creating ${streamCount} channels from selected streams:`
|
||||
: `Configure options for creating a channel from "${streamName}":`}
|
||||
</Text>
|
||||
|
||||
<Divider label="Channel Profiles" labelPosition="left" />
|
||||
|
||||
<MultiSelect
|
||||
label="Channel Profiles"
|
||||
description="Select 'All Profiles' to add to all profiles, 'No Profiles' to not add to any profile, or choose specific profiles"
|
||||
placeholder="Select profiles..."
|
||||
data={profileOptions}
|
||||
value={selectedProfileIds}
|
||||
onChange={handleProfileChange}
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
|
||||
<Divider label="Channel Number" labelPosition="left" />
|
||||
|
||||
<Radio.Group
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
label={numberingLabel}
|
||||
>
|
||||
<Stack mt="xs" spacing="xs">
|
||||
<Radio
|
||||
value="provider"
|
||||
label={isBulk ? 'Use Provider Numbers' : 'Use Provider Number'}
|
||||
description={
|
||||
isBulk
|
||||
? 'Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts'
|
||||
: 'Use tvg-chno or channel-number from stream metadata, auto-assign if not available'
|
||||
}
|
||||
/>
|
||||
<Radio
|
||||
value="auto"
|
||||
label={
|
||||
isBulk ? 'Auto-Assign Sequential' : 'Auto-Assign Next Available'
|
||||
}
|
||||
description={
|
||||
isBulk
|
||||
? 'Start from the lowest available channel number and increment by 1'
|
||||
: 'Automatically assign the next available channel number'
|
||||
}
|
||||
/>
|
||||
<Radio
|
||||
value={customModeValue}
|
||||
label={
|
||||
isBulk ? 'Start from Custom Number' : 'Use Specific Number'
|
||||
}
|
||||
description={
|
||||
isBulk
|
||||
? 'Start sequential numbering from a specific channel number'
|
||||
: 'Use a specific channel number'
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
{mode === customModeValue && (
|
||||
<NumberInput
|
||||
label={isBulk ? 'Starting Channel Number' : 'Channel Number'}
|
||||
description={
|
||||
isBulk
|
||||
? 'Channel numbers will be assigned starting from this number'
|
||||
: 'The specific channel number to assign'
|
||||
}
|
||||
value={numberValue}
|
||||
onChange={onNumberValueChange}
|
||||
min={1}
|
||||
placeholder={
|
||||
isBulk ? 'Enter starting number...' : 'Enter channel number...'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
checked={rememberChoice}
|
||||
onChange={(event) =>
|
||||
onRememberChoiceChange(event.currentTarget.checked)
|
||||
}
|
||||
label="Remember this choice and don't ask again"
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{confirmLabel}</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateChannelModal;
|
||||
|
|
@ -58,6 +58,7 @@ import useWarningsStore from '../../store/warnings';
|
|||
import { CustomTable, useTable } from './CustomTable';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import ConfirmationDialog from '../ConfirmationDialog';
|
||||
import CreateChannelModal from '../modals/CreateChannelModal';
|
||||
|
||||
const StreamRowActions = ({
|
||||
theme,
|
||||
|
|
@ -193,19 +194,21 @@ const StreamsTable = ({ onReady }) => {
|
|||
const [sorting, setSorting] = useState([{ id: 'name', desc: false }]);
|
||||
const [selectedStreamIds, setSelectedStreamIds] = useState([]);
|
||||
|
||||
// Channel numbering modal state
|
||||
// Channel creation modal state (bulk)
|
||||
const [channelNumberingModalOpen, setChannelNumberingModalOpen] =
|
||||
useState(false);
|
||||
const [numberingMode, setNumberingMode] = useState('provider'); // 'provider', 'auto', or 'custom'
|
||||
const [customStartNumber, setCustomStartNumber] = useState(1);
|
||||
const [rememberChoice, setRememberChoice] = useState(false);
|
||||
const [bulkSelectedProfileIds, setBulkSelectedProfileIds] = useState([]);
|
||||
|
||||
// Single channel numbering modal state
|
||||
// Channel creation modal state (single)
|
||||
const [singleChannelModalOpen, setSingleChannelModalOpen] = useState(false);
|
||||
const [singleChannelMode, setSingleChannelMode] = useState('provider'); // 'provider', 'auto', or 'specific'
|
||||
const [specificChannelNumber, setSpecificChannelNumber] = useState(1);
|
||||
const [rememberSingleChoice, setRememberSingleChoice] = useState(false);
|
||||
const [currentStreamForChannel, setCurrentStreamForChannel] = useState(null);
|
||||
const [singleSelectedProfileIds, setSingleSelectedProfileIds] = useState([]);
|
||||
|
||||
// Confirmation dialog state
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
|
|
@ -260,6 +263,8 @@ const StreamsTable = ({ onReady }) => {
|
|||
(state) =>
|
||||
state.channels.find((chan) => chan.id === selectedChannelIds[0])?.streams
|
||||
);
|
||||
const channelProfiles = useChannelsStore((s) => s.profiles);
|
||||
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
|
||||
const env_mode = useSettingsStore((s) => s.environment.env_mode);
|
||||
const showVideo = useVideoStore((s) => s.showVideo);
|
||||
const [tableSize, _] = useLocalStorage('table-size', 'default');
|
||||
|
|
@ -462,6 +467,11 @@ const StreamsTable = ({ onReady }) => {
|
|||
const createChannelsFromStreams = async () => {
|
||||
if (selectedStreamIds.length === 0) return;
|
||||
|
||||
// Set default profile selection based on current profile filter
|
||||
const defaultProfileIds =
|
||||
selectedProfileId === '0' ? ['all'] : [selectedProfileId];
|
||||
setBulkSelectedProfileIds(defaultProfileIds);
|
||||
|
||||
// Check if user has suppressed the channel numbering dialog
|
||||
const actionKey = 'channel-numbering-choice';
|
||||
if (isWarningSuppressed(actionKey)) {
|
||||
|
|
@ -478,7 +488,10 @@ const StreamsTable = ({ onReady }) => {
|
|||
? 0
|
||||
: Number(savedStartNumber);
|
||||
|
||||
await executeChannelCreation(startingChannelNumberValue);
|
||||
await executeChannelCreation(
|
||||
startingChannelNumberValue,
|
||||
defaultProfileIds
|
||||
);
|
||||
} else {
|
||||
// Show the modal to let user choose
|
||||
setChannelNumberingModalOpen(true);
|
||||
|
|
@ -486,15 +499,32 @@ const StreamsTable = ({ onReady }) => {
|
|||
};
|
||||
|
||||
// Separate function to actually execute the channel creation
|
||||
const executeChannelCreation = async (startingChannelNumberValue) => {
|
||||
const executeChannelCreation = async (
|
||||
startingChannelNumberValue,
|
||||
profileIds = null
|
||||
) => {
|
||||
try {
|
||||
const selectedChannelProfileId =
|
||||
useChannelsStore.getState().selectedProfileId;
|
||||
// Convert profile selection: 'all' means all profiles (null), 'none' means no profiles ([]), specific IDs otherwise
|
||||
let channelProfileIds;
|
||||
if (profileIds) {
|
||||
if (profileIds.includes('none')) {
|
||||
channelProfileIds = [];
|
||||
} else if (profileIds.includes('all')) {
|
||||
channelProfileIds = null;
|
||||
} else {
|
||||
channelProfileIds = profileIds
|
||||
.filter((id) => id !== 'all' && id !== 'none')
|
||||
.map((id) => parseInt(id));
|
||||
}
|
||||
} else {
|
||||
channelProfileIds =
|
||||
selectedProfileId !== '0' ? [parseInt(selectedProfileId)] : null;
|
||||
}
|
||||
|
||||
// Use the async API for all bulk operations
|
||||
const response = await API.createChannelsFromStreamsAsync(
|
||||
selectedStreamIds,
|
||||
selectedChannelProfileId !== '0' ? [selectedChannelProfileId] : null,
|
||||
channelProfileIds,
|
||||
startingChannelNumberValue
|
||||
);
|
||||
|
||||
|
|
@ -533,7 +563,10 @@ const StreamsTable = ({ onReady }) => {
|
|||
: Number(customStartNumber);
|
||||
|
||||
setChannelNumberingModalOpen(false);
|
||||
await executeChannelCreation(startingChannelNumberValue);
|
||||
await executeChannelCreation(
|
||||
startingChannelNumberValue,
|
||||
bulkSelectedProfileIds
|
||||
);
|
||||
};
|
||||
|
||||
const editStream = async (stream = null) => {
|
||||
|
|
@ -595,6 +628,11 @@ const StreamsTable = ({ onReady }) => {
|
|||
|
||||
// Single channel creation functions
|
||||
const createChannelFromStream = async (stream) => {
|
||||
// Set default profile selection based on current profile filter
|
||||
const defaultProfileIds =
|
||||
selectedProfileId === '0' ? ['all'] : [selectedProfileId];
|
||||
setSingleSelectedProfileIds(defaultProfileIds);
|
||||
|
||||
// Check if user has suppressed the single channel numbering dialog
|
||||
const actionKey = 'single-channel-numbering-choice';
|
||||
if (isWarningSuppressed(actionKey)) {
|
||||
|
|
@ -611,7 +649,11 @@ const StreamsTable = ({ onReady }) => {
|
|||
? 0
|
||||
: Number(savedChannelNumber);
|
||||
|
||||
await executeSingleChannelCreation(stream, channelNumberValue);
|
||||
await executeSingleChannelCreation(
|
||||
stream,
|
||||
channelNumberValue,
|
||||
defaultProfileIds
|
||||
);
|
||||
} else {
|
||||
// Show the modal to let user choose
|
||||
setCurrentStreamForChannel(stream);
|
||||
|
|
@ -620,18 +662,33 @@ const StreamsTable = ({ onReady }) => {
|
|||
};
|
||||
|
||||
// Separate function to actually execute single channel creation
|
||||
const executeSingleChannelCreation = async (stream, channelNumber = null) => {
|
||||
const selectedChannelProfileId =
|
||||
useChannelsStore.getState().selectedProfileId;
|
||||
const executeSingleChannelCreation = async (
|
||||
stream,
|
||||
channelNumber = null,
|
||||
profileIds = null
|
||||
) => {
|
||||
// Convert profile selection: 'all' means all profiles (null), 'none' means no profiles ([]), specific IDs otherwise
|
||||
let channelProfileIds;
|
||||
if (profileIds) {
|
||||
if (profileIds.includes('none')) {
|
||||
channelProfileIds = [];
|
||||
} else if (profileIds.includes('all')) {
|
||||
channelProfileIds = null;
|
||||
} else {
|
||||
channelProfileIds = profileIds
|
||||
.filter((id) => id !== 'all' && id !== 'none')
|
||||
.map((id) => parseInt(id));
|
||||
}
|
||||
} else {
|
||||
channelProfileIds =
|
||||
selectedProfileId !== '0' ? [parseInt(selectedProfileId)] : null;
|
||||
}
|
||||
|
||||
await API.createChannelFromStream({
|
||||
name: stream.name,
|
||||
channel_number: channelNumber,
|
||||
stream_id: stream.id,
|
||||
// Only pass channel_profile_ids if a specific profile is selected (not "All")
|
||||
...(selectedChannelProfileId !== '0' && {
|
||||
channel_profile_ids: selectedChannelProfileId,
|
||||
}),
|
||||
channel_profile_ids: channelProfileIds,
|
||||
});
|
||||
await API.requeryChannels();
|
||||
const fetchLogos = useChannelsStore.getState().fetchLogos;
|
||||
|
|
@ -663,7 +720,8 @@ const StreamsTable = ({ onReady }) => {
|
|||
setSingleChannelModalOpen(false);
|
||||
await executeSingleChannelCreation(
|
||||
currentStreamForChannel,
|
||||
channelNumberValue
|
||||
channelNumberValue,
|
||||
singleSelectedProfileIds
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -1134,145 +1192,41 @@ const StreamsTable = ({ onReady }) => {
|
|||
onClose={closeStreamForm}
|
||||
/>
|
||||
|
||||
{/* Channel Numbering Modal */}
|
||||
<Modal
|
||||
{/* Bulk Channel Creation Modal */}
|
||||
<CreateChannelModal
|
||||
opened={channelNumberingModalOpen}
|
||||
onClose={() => setChannelNumberingModalOpen(false)}
|
||||
title="Channel Numbering Options"
|
||||
size="md"
|
||||
centered
|
||||
>
|
||||
<Stack spacing="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Choose how to assign channel numbers to the{' '}
|
||||
{selectedStreamIds.length} selected streams:
|
||||
</Text>
|
||||
mode={numberingMode}
|
||||
onModeChange={setNumberingMode}
|
||||
numberValue={customStartNumber}
|
||||
onNumberValueChange={setCustomStartNumber}
|
||||
rememberChoice={rememberChoice}
|
||||
onRememberChoiceChange={setRememberChoice}
|
||||
onConfirm={handleChannelNumberingConfirm}
|
||||
isBulk={true}
|
||||
streamCount={selectedStreamIds.length}
|
||||
selectedProfileIds={bulkSelectedProfileIds}
|
||||
onProfileIdsChange={setBulkSelectedProfileIds}
|
||||
channelProfiles={channelProfiles ? Object.values(channelProfiles) : []}
|
||||
/>
|
||||
|
||||
<Radio.Group
|
||||
value={numberingMode}
|
||||
onChange={setNumberingMode}
|
||||
label="Numbering Mode"
|
||||
>
|
||||
<Stack mt="xs" spacing="xs">
|
||||
<Radio
|
||||
value="provider"
|
||||
label="Use Provider Numbers"
|
||||
description="Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts"
|
||||
/>
|
||||
<Radio
|
||||
value="auto"
|
||||
label="Auto-Assign Sequential"
|
||||
description="Start from the lowest available channel number and increment by 1"
|
||||
/>
|
||||
<Radio
|
||||
value="custom"
|
||||
label="Start from Custom Number"
|
||||
description="Start sequential numbering from a specific channel number"
|
||||
/>
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
{numberingMode === 'custom' && (
|
||||
<NumberInput
|
||||
label="Starting Channel Number"
|
||||
description="Channel numbers will be assigned starting from this number"
|
||||
value={customStartNumber}
|
||||
onChange={setCustomStartNumber}
|
||||
min={1}
|
||||
placeholder="Enter starting number..."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
checked={rememberChoice}
|
||||
onChange={(event) => setRememberChoice(event.currentTarget.checked)}
|
||||
label="Remember this choice and don't ask again"
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setChannelNumberingModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleChannelNumberingConfirm}>
|
||||
Create Channels
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Single Channel Numbering Modal */}
|
||||
<Modal
|
||||
{/* Single Channel Creation Modal */}
|
||||
<CreateChannelModal
|
||||
opened={singleChannelModalOpen}
|
||||
onClose={() => setSingleChannelModalOpen(false)}
|
||||
title="Channel Number Assignment"
|
||||
size="md"
|
||||
centered
|
||||
>
|
||||
<Stack spacing="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Choose how to assign the channel number for "
|
||||
{currentStreamForChannel?.name}":
|
||||
</Text>
|
||||
|
||||
<Radio.Group
|
||||
value={singleChannelMode}
|
||||
onChange={setSingleChannelMode}
|
||||
label="Number Assignment"
|
||||
>
|
||||
<Stack mt="xs" spacing="xs">
|
||||
<Radio
|
||||
value="provider"
|
||||
label="Use Provider Number"
|
||||
description="Use tvg-chno or channel-number from stream metadata, auto-assign if not available"
|
||||
/>
|
||||
<Radio
|
||||
value="auto"
|
||||
label="Auto-Assign Next Available"
|
||||
description="Automatically assign the next available channel number"
|
||||
/>
|
||||
<Radio
|
||||
value="specific"
|
||||
label="Use Specific Number"
|
||||
description="Use a specific channel number"
|
||||
/>
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
{singleChannelMode === 'specific' && (
|
||||
<NumberInput
|
||||
label="Channel Number"
|
||||
description="The specific channel number to assign"
|
||||
value={specificChannelNumber}
|
||||
onChange={setSpecificChannelNumber}
|
||||
min={1}
|
||||
placeholder="Enter channel number..."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
checked={rememberSingleChoice}
|
||||
onChange={(event) =>
|
||||
setRememberSingleChoice(event.currentTarget.checked)
|
||||
}
|
||||
label="Remember this choice and don't ask again"
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setSingleChannelModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSingleChannelNumberingConfirm}>
|
||||
Create Channel
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
mode={singleChannelMode}
|
||||
onModeChange={setSingleChannelMode}
|
||||
numberValue={specificChannelNumber}
|
||||
onNumberValueChange={setSpecificChannelNumber}
|
||||
rememberChoice={rememberSingleChoice}
|
||||
onRememberChoiceChange={setRememberSingleChoice}
|
||||
onConfirm={handleSingleChannelNumberingConfirm}
|
||||
isBulk={false}
|
||||
streamName={currentStreamForChannel?.name}
|
||||
selectedProfileIds={singleSelectedProfileIds}
|
||||
onProfileIdsChange={setSingleSelectedProfileIds}
|
||||
channelProfiles={channelProfiles ? Object.values(channelProfiles) : []}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={confirmDeleteOpen}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue