From 4886426ea02590e998a891e7a0df7edbcf01b5ba Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 14 Sep 2025 12:46:30 -0500 Subject: [PATCH] New feature: Allow specifying channel number during channel creation. Closes #377 Closes #169 [Feature]: Starting channel number/continue auto numbering from channel number X (user defined) --- apps/channels/tasks.py | 10 +- .../src/components/tables/StreamsTable.jsx | 141 +++++++++++++++++- 2 files changed, 144 insertions(+), 7 deletions(-) diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py index a4da0bdd..e0954210 100755 --- a/apps/channels/tasks.py +++ b/apps/channels/tasks.py @@ -1604,13 +1604,13 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None """ Asynchronously create channels from a list of stream IDs. Provides progress updates via WebSocket. - + Args: stream_ids: List of stream IDs to create channels from channel_profile_ids: Optional list of channel profile IDs to assign channels to starting_channel_number: Optional starting channel number behavior: - None: Use provider channel numbers, then auto-assign from 1 - - 0: Start with lowest available number and increment by 1 + - 0: Start with lowest available number and increment by 1 - Other number: Use as starting number for auto-assignment """ from apps.channels.models import Stream, Channel, ChannelGroup, ChannelProfile, ChannelProfileMembership, Logo @@ -1637,7 +1637,7 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None # Gather current used numbers once used_numbers = set(Channel.objects.all().values_list("channel_number", flat=True)) - + # Initialize next_number based on starting_channel_number mode if starting_channel_number is None: # Mode 1: Use provider numbers when available, auto-assign when not @@ -1688,7 +1688,7 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None # Determine channel number based on starting_channel_number mode channel_number = None - + if starting_channel_number is None: # Mode 1: Use provider numbers when available if "tvg-chno" in stream_custom_props: @@ -1697,7 +1697,7 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None channel_number = float(stream_custom_props["channel-number"]) elif "num" in stream_custom_props: channel_number = float(stream_custom_props["num"]) - + # For modes 2 and 3 (starting_channel_number == 0 or specific number), # ignore provider numbers and use sequential assignment diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index 87cf57bb..0214c7cd 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -39,11 +39,16 @@ import { UnstyledButton, LoadingOverlay, Skeleton, + Modal, + NumberInput, + Radio, + Checkbox, } from '@mantine/core'; import { useNavigate } from 'react-router-dom'; import useSettingsStore from '../../store/settings'; import useVideoStore from '../../store/useVideoStore'; import useChannelsTableStore from '../../store/channelsTable'; +import useWarningsStore from '../../store/warnings'; import { CustomTable, useTable } from './CustomTable'; import useLocalStorage from '../../hooks/useLocalStorage'; @@ -197,6 +202,13 @@ const StreamsTable = () => { const [sorting, setSorting] = useState([{ id: 'name', desc: '' }]); const [selectedStreamIds, setSelectedStreamIds] = useState([]); + // Channel numbering modal state + 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 [allRowsSelected, setAllRowsSelected] = useState(false); // Add local storage for page size @@ -249,6 +261,10 @@ const StreamsTable = () => { const showVideo = useVideoStore((s) => s.showVideo); const [tableSize, _] = useLocalStorage('table-size', 'default'); + // Warnings store for "remember choice" functionality + const suppressWarning = useWarningsStore((s) => s.suppressWarning); + const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); + const handleSelectClick = (e) => { e.stopPropagation(); e.preventDefault(); @@ -419,11 +435,35 @@ const StreamsTable = () => { fetchChannelGroups, ]); - // Bulk creation: create channels from selected streams in one API call // Bulk creation: create channels from selected streams asynchronously const createChannelsFromStreams = async () => { if (selectedStreamIds.length === 0) return; + // Check if user has suppressed the channel numbering dialog + const actionKey = 'channel-numbering-choice'; + if (isWarningSuppressed(actionKey)) { + // Use the remembered settings or default to 'provider' mode + const savedMode = + localStorage.getItem('channel-numbering-mode') || 'provider'; + const savedStartNumber = + localStorage.getItem('channel-numbering-start') || '1'; + + const startingChannelNumberValue = + savedMode === 'provider' + ? null + : savedMode === 'auto' + ? 0 + : Number(savedStartNumber); + + await executeChannelCreation(startingChannelNumberValue); + } else { + // Show the modal to let user choose + setChannelNumberingModalOpen(true); + } + }; + + // Separate function to actually execute the channel creation + const executeChannelCreation = async (startingChannelNumberValue) => { try { const selectedChannelProfileId = useChannelsStore.getState().selectedProfileId; @@ -431,7 +471,8 @@ const StreamsTable = () => { // Use the async API for all bulk operations const response = await API.createChannelsFromStreamsAsync( selectedStreamIds, - selectedChannelProfileId !== '0' ? [selectedChannelProfileId] : null + selectedChannelProfileId !== '0' ? [selectedChannelProfileId] : null, + startingChannelNumberValue ); console.log( @@ -446,6 +487,32 @@ const StreamsTable = () => { } }; + // Handle confirming the channel numbering modal + const handleChannelNumberingConfirm = async () => { + // Save the choice if user wants to remember it + if (rememberChoice) { + suppressWarning('channel-numbering-choice'); + localStorage.setItem('channel-numbering-mode', numberingMode); + if (numberingMode === 'custom') { + localStorage.setItem( + 'channel-numbering-start', + customStartNumber.toString() + ); + } + } + + // Convert mode to API value + const startingChannelNumberValue = + numberingMode === 'provider' + ? null + : numberingMode === 'auto' + ? 0 + : Number(customStartNumber); + + setChannelNumberingModalOpen(false); + await executeChannelCreation(startingChannelNumberValue); + }; + const editStream = async (stream = null) => { setStream(stream); setModalOpen(true); @@ -900,6 +967,76 @@ const StreamsTable = () => { isOpen={modalOpen} onClose={closeStreamForm} /> + + {/* Channel Numbering Modal */} + setChannelNumberingModalOpen(false)} + title="Channel Numbering Options" + size="md" + centered + > + + + Choose how to assign channel numbers to the{' '} + {selectedStreamIds.length} selected streams: + + + + + + + + + + + {numberingMode === 'custom' && ( + + )} + + setRememberChoice(event.currentTarget.checked)} + label="Remember this choice and don't ask again" + /> + + + + + + + ); };