diff --git a/frontend/src/api.js b/frontend/src/api.js index aeedf0ae..5fe223ef 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -456,6 +456,23 @@ export default class API { } } + // Bulk update with per-channel payloads (e.g., regex renames) + static async bulkUpdateChannels(updates) { + try { + const response = await request( + `${host}/api/channels/channels/edit/bulk/`, + { + method: 'PATCH', + body: updates, + } + ); + + return response; + } catch (e) { + errorNotification('Failed to update channels', e); + } + } + static async setChannelEPG(channelId, epgDataId) { try { const response = await request( diff --git a/frontend/src/components/forms/ChannelBatch.jsx b/frontend/src/components/forms/ChannelBatch.jsx index 0e13a6b7..9973ea57 100644 --- a/frontend/src/components/forms/ChannelBatch.jsx +++ b/frontend/src/components/forms/ChannelBatch.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; import useChannelsStore from '../../store/channels'; import API from '../../api'; import useStreamProfilesStore from '../../store/streamProfiles'; @@ -20,6 +20,9 @@ import { Tooltip, UnstyledButton, Center, + Divider, + Checkbox, + Paper, } from '@mantine/core'; import { ListOrdered, SquarePlus, SquareX, X } from 'lucide-react'; import { FixedSizeList as List } from 'react-window'; @@ -39,6 +42,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false); const [selectedChannelGroup, setSelectedChannelGroup] = useState('-1'); const [isSubmitting, setIsSubmitting] = useState(false); + const [regexFind, setRegexFind] = useState(''); + const [regexReplace, setRegexReplace] = useState(''); const [groupPopoverOpened, setGroupPopoverOpened] = useState(false); const [groupFilter, setGroupFilter] = useState(''); @@ -83,7 +88,39 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { delete values.channel_group; try { - await API.updateChannels(channelIds, values); + const applyRegex = regexFind.trim().length > 0; + + if (applyRegex) { + // Build per-channel updates to apply unique names via regex + let flags = 'g'; + let re; + try { + re = new RegExp(regexFind, flags); + } catch (e) { + console.error('Invalid regex:', e); + setIsSubmitting(false); + return; + } + + const channelsMap = useChannelsStore.getState().channels; + const updates = channelIds.map((id) => { + const ch = channelsMap[id]; + const currentName = ch?.name ?? ''; + const newName = currentName.replace(re, regexReplace ?? ''); + const update = { id }; + if (newName !== currentName && newName.trim().length > 0) { + update.name = newName; + } + // Merge base values (group/profile/user_level) if present + Object.assign(update, values); + return update; + }); + + await API.bulkUpdateChannels(updates); + } else { + await API.updateChannels(channelIds, values); + } + // Refresh both the channels table data and the main channels store await Promise.all([ API.requeryChannels(), @@ -146,7 +183,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { @@ -158,6 +195,35 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
+ + + Channel Name + + + setRegexFind(e.currentTarget.value)} + style={{ flex: 1 }} + /> + setRegexReplace(e.currentTarget.value)} + style={{ flex: 1 }} + /> + + + + { }; export default ChannelBatchForm; + +// Lightweight inline preview component to visualize rename results for a subset +const RegexPreview = ({ channelIds, find, replace}) => { + const channelsMap = useChannelsStore((s) => s.channels); + const previewItems = useMemo(() => { + const items = []; + if (!find) return items; + let flags = 'g'; + let re; + try { + re = new RegExp(find, flags); + } catch (e) { + return [{ before: 'Invalid regex', after: '' }]; + } + for (let i = 0; i < Math.min(channelIds.length, 25); i++) { + const id = channelIds[i]; + const before = channelsMap[id]?.name ?? ''; + const after = before.replace(re, replace ?? ''); + if (before !== after) { + items.push({ before, after }); + } + } + return items; + }, [channelIds, channelsMap, find, replace]); + + if (!find) return null; + + return ( + + + Preview (first {Math.min(channelIds.length, 25)} of {channelIds.length} selected) + + + + {previewItems.length === 0 ? ( + No changes with current pattern. + ) : ( + previewItems.map((row, idx) => ( + + + {row.before} + + + + {row.after} + + + )) + )} + + + + ); +};