mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-20 16:51:10 +00:00
auto-assign numbers now configurable by selection, channel enable switch now applies to all selected
This commit is contained in:
parent
e63a66bf57
commit
4ebfde2797
6 changed files with 152 additions and 68 deletions
|
|
@ -185,6 +185,30 @@ class ChannelViewSet(viewsets.ModelViewSet):
|
|||
context['include_streams'] = include_streams
|
||||
return context
|
||||
|
||||
@action(detail=False, methods=['patch'], url_path='edit/bulk')
|
||||
def edit_bulk(self, request):
|
||||
data_list = request.data
|
||||
if not isinstance(data_list, list):
|
||||
return Response({"error": "Expected a list of channel objects objects"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
updated_channels = []
|
||||
try:
|
||||
with transaction.atomic():
|
||||
for item in data_list:
|
||||
channel = Channel.objects.id(id=item.pop('id'))
|
||||
for key, value in item.items():
|
||||
setattr(channel, key, value)
|
||||
|
||||
channel.save(update_fields=item.keys())
|
||||
updated_channels.append(channel)
|
||||
except Exception as e:
|
||||
logger.error("Error during bulk channel edit", e)
|
||||
return Response({"error": e}, status=500)
|
||||
|
||||
response_data = ChannelSerializer(updated_channels, many=True).data
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='ids')
|
||||
def get_ids(self, request, *args, **kwargs):
|
||||
# Get the filtered queryset
|
||||
|
|
@ -204,12 +228,13 @@ class ChannelViewSet(viewsets.ModelViewSet):
|
|||
operation_description="Auto-assign channel_number in bulk by an ordered list of channel IDs.",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=["channel_order"],
|
||||
required=["channel_ids"],
|
||||
properties={
|
||||
"channel_order": openapi.Schema(
|
||||
"starting_number": openapi.Schema(type=openapi.TYPE_STRING, description="Starting channel number to assign"),
|
||||
"channel_ids": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
||||
description="List of channel IDs in the new order"
|
||||
description="Channel IDs to assign"
|
||||
)
|
||||
}
|
||||
),
|
||||
|
|
@ -218,9 +243,11 @@ class ChannelViewSet(viewsets.ModelViewSet):
|
|||
@action(detail=False, methods=['post'], url_path='assign')
|
||||
def assign(self, request):
|
||||
with transaction.atomic():
|
||||
channel_order = request.data.get('channel_order', [])
|
||||
for order, channel_id in enumerate(channel_order, start=1):
|
||||
Channel.objects.filter(id=channel_id).update(channel_number=order)
|
||||
channel_ids = request.data.get('channel_ids', [])
|
||||
channel_num = request.data.get('starting_number', 1)
|
||||
for channel_id in channel_ids:
|
||||
Channel.objects.filter(id=channel_id).update(channel_number=channel_num)
|
||||
channel_num = channel_num + 1
|
||||
|
||||
return Response({"message": "Channels have been auto-assigned!"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
|
|
|||
|
|
@ -378,6 +378,31 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async updateChannels(ids, values) {
|
||||
const body = [];
|
||||
for (const id in ids) {
|
||||
body.push({
|
||||
id,
|
||||
...values,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(
|
||||
`${host}/api/channels/channels/edit/bulk/`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body,
|
||||
}
|
||||
);
|
||||
|
||||
useChannelsStore.getState().updateChannels(response);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to update channels', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async setChannelEPG(channelId, epgDataId) {
|
||||
try {
|
||||
const response = await request(
|
||||
|
|
@ -408,16 +433,13 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async assignChannelNumbers(channelIds) {
|
||||
static async assignChannelNumbers(channelIds, startingNum = 1) {
|
||||
try {
|
||||
const response = await request(`${host}/api/channels/channels/assign/`, {
|
||||
method: 'POST',
|
||||
body: { channel_order: channelIds },
|
||||
body: { channel_ids: channelIds, starting_number: startingNum },
|
||||
});
|
||||
|
||||
// Optionally refesh the channel list in Zustand
|
||||
// await useChannelsStore.getState().fetchChannels();
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to assign channel #s', e);
|
||||
|
|
@ -733,7 +755,11 @@ export default class API {
|
|||
|
||||
try {
|
||||
// If this is just toggling the active state, make a simpler request
|
||||
if (isToggle && 'is_active' in payload && Object.keys(payload).length === 1) {
|
||||
if (
|
||||
isToggle &&
|
||||
'is_active' in payload &&
|
||||
Object.keys(payload).length === 1
|
||||
) {
|
||||
const response = await request(`${host}/api/m3u/accounts/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: { is_active: payload.is_active },
|
||||
|
|
@ -830,7 +856,11 @@ export default class API {
|
|||
|
||||
try {
|
||||
// If this is just toggling the active state, make a simpler request
|
||||
if (isToggle && 'is_active' in payload && Object.keys(payload).length === 1) {
|
||||
if (
|
||||
isToggle &&
|
||||
'is_active' in payload &&
|
||||
Object.keys(payload).length === 1
|
||||
) {
|
||||
const response = await request(`${host}/api/epg/sources/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: { is_active: payload.is_active },
|
||||
|
|
@ -1344,7 +1374,9 @@ export default class API {
|
|||
|
||||
static async getChannel(id) {
|
||||
try {
|
||||
const response = await request(`${host}/api/channels/channels/${id}/?include_streams=true`);
|
||||
const response = await request(
|
||||
`${host}/api/channels/channels/${id}/?include_streams=true`
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to fetch channel details', e);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/
|
|||
const hdhrUrlBase = `${window.location.protocol}//${window.location.host}/hdhr`;
|
||||
|
||||
const ChannelEnabledSwitch = React.memo(
|
||||
({ rowId, selectedProfileId, toggleChannelEnabled }) => {
|
||||
({ rowId, selectedProfileId, selectedTableIds }) => {
|
||||
// Directly extract the channels set once to avoid re-renders on every change.
|
||||
const isEnabled = useChannelsStore(
|
||||
useCallback(
|
||||
|
|
@ -68,9 +68,17 @@ const ChannelEnabledSwitch = React.memo(
|
|||
)
|
||||
);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
toggleChannelEnabled([rowId], !isEnabled);
|
||||
}, [rowId, isEnabled, toggleChannelEnabled]);
|
||||
const handleToggle = () => {
|
||||
if (selectedTableIds.length > 1) {
|
||||
API.updateProfileChannels(
|
||||
selectedTableIds,
|
||||
selectedProfileId,
|
||||
!isEnabled
|
||||
);
|
||||
} else {
|
||||
API.updateProfileChannel(rowId, selectedProfileId, !isEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Center style={{ width: '100%' }}>
|
||||
|
|
@ -465,21 +473,6 @@ const ChannelsTable = ({}) => {
|
|||
});
|
||||
};
|
||||
|
||||
const toggleChannelEnabled = useCallback(
|
||||
async (channelIds, enabled) => {
|
||||
if (channelIds.length == 1) {
|
||||
await API.updateProfileChannel(
|
||||
channelIds[0],
|
||||
selectedProfileId,
|
||||
enabled
|
||||
);
|
||||
} else {
|
||||
await API.updateProfileChannels(channelIds, selectedProfileId, enabled);
|
||||
}
|
||||
},
|
||||
[selectedProfileId]
|
||||
);
|
||||
|
||||
const closeChannelForm = () => {
|
||||
setChannel(null);
|
||||
setChannelModalOpen(false);
|
||||
|
|
@ -547,22 +540,6 @@ const ChannelsTable = ({}) => {
|
|||
}
|
||||
};
|
||||
|
||||
const EnabledHeaderSwitch = useCallback(() => {
|
||||
let enabled = false;
|
||||
for (const id of selectedChannelIds) {
|
||||
if (selectedProfileChannelIds.has(id)) {
|
||||
enabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelected = () => {
|
||||
toggleChannelEnabled(selectedChannelIds, !enabled);
|
||||
};
|
||||
|
||||
return <Switch size="xs" checked={enabled} onChange={toggleSelected} />;
|
||||
}, [selectedChannelIds, selectedProfileChannelIds, data]);
|
||||
|
||||
/**
|
||||
* useEffect
|
||||
*/
|
||||
|
|
@ -602,12 +579,12 @@ const ChannelsTable = ({}) => {
|
|||
{
|
||||
id: 'enabled',
|
||||
size: 45,
|
||||
cell: ({ row }) => {
|
||||
cell: ({ row, table }) => {
|
||||
return (
|
||||
<ChannelEnabledSwitch
|
||||
rowId={row.original.id}
|
||||
selectedProfileId={selectedProfileId}
|
||||
toggleChannelEnabled={toggleChannelEnabled}
|
||||
selectedTableIds={table.getState().selectedTableIds}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
@ -716,9 +693,6 @@ const ChannelsTable = ({}) => {
|
|||
|
||||
switch (header.id) {
|
||||
case 'enabled':
|
||||
// if (selectedProfileId !== '0' && table.selectedTableIds.length > 0) {
|
||||
// return EnabledHeaderSwitch();
|
||||
// }
|
||||
return (
|
||||
<Center style={{ width: '100%' }}>
|
||||
<ScanEye size="16" />
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import {
|
|||
Button,
|
||||
Flex,
|
||||
Group,
|
||||
NumberInput,
|
||||
Popover,
|
||||
Select,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
useMantineTheme,
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
import {
|
||||
ArrowDown01,
|
||||
Binary,
|
||||
Check,
|
||||
CircleCheck,
|
||||
SquareMinus,
|
||||
SquarePlus,
|
||||
|
|
@ -87,6 +90,8 @@ const ChannelTableHeader = ({
|
|||
}) => {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const [channelNumAssignmentStart, setChannelNumAssignmentStart] = useState(1);
|
||||
|
||||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
|
||||
const setSelectedProfileId = useChannelsStore((s) => s.setSelectedProfileId);
|
||||
|
|
@ -110,11 +115,11 @@ const ChannelTableHeader = ({
|
|||
|
||||
const assignChannels = async () => {
|
||||
try {
|
||||
// Get row order from the table
|
||||
const rowOrder = rows.map((row) => row.original.id);
|
||||
|
||||
// Call our custom API endpoint
|
||||
const result = await API.assignChannelNumbers(rowOrder);
|
||||
const result = await API.assignChannelNumbers(
|
||||
selectedTableIds,
|
||||
channelNumAssignmentStart
|
||||
);
|
||||
|
||||
// We might get { message: "Channels have been auto-assigned!" }
|
||||
notifications.show({
|
||||
|
|
@ -194,15 +199,38 @@ const ChannelTableHeader = ({
|
|||
</Button>
|
||||
|
||||
<Tooltip label="Assign Channel #s">
|
||||
<Button
|
||||
leftSection={<ArrowDown01 size={18} />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
onClick={assignChannels}
|
||||
p={5}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
<Popover withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<Button
|
||||
leftSection={<ArrowDown01 size={18} />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
p={5}
|
||||
disabled={selectedTableIds.length == 0}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Group>
|
||||
<Text>Start #</Text>
|
||||
<NumberInput
|
||||
value={channelNumAssignmentStart}
|
||||
onChange={setChannelNumAssignmentStart}
|
||||
size="small"
|
||||
style={{ width: 50 }}
|
||||
/>
|
||||
<ActionIcon
|
||||
size="xs"
|
||||
color={theme.tailwind.green[5]}
|
||||
variant="transparent"
|
||||
onClick={assignChannels}
|
||||
>
|
||||
<Check />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Auto-Match EPG">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const useTable = ({
|
|||
expandedRowRenderer = () => <></>,
|
||||
onRowSelectionChange = null,
|
||||
getExpandedRowHeight = null,
|
||||
state = [],
|
||||
...options
|
||||
}) => {
|
||||
const [selectedTableIds, setSelectedTableIds] = useState([]);
|
||||
|
|
@ -155,7 +156,9 @@ const useTable = ({
|
|||
const rangeIds = allRowIds.slice(startIndex, endIndex + 1);
|
||||
|
||||
// Preserve existing selections outside the range
|
||||
const idsOutsideRange = selectedTableIds.filter(id => !rangeIds.includes(id));
|
||||
const idsOutsideRange = selectedTableIds.filter(
|
||||
(id) => !rangeIds.includes(id)
|
||||
);
|
||||
const newSelection = [...new Set([...rangeIds, ...idsOutsideRange])];
|
||||
updateSelectedTableIds(newSelection);
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,26 @@ const useChannelsStore = create((set, get) => ({
|
|||
},
|
||||
})),
|
||||
|
||||
updateChannels: (channels) => {
|
||||
const channelsByUUID = {};
|
||||
const updatedChannels = channels.reduce((acc, chan) => {
|
||||
channelsByUUID[chan.uuid] = chan;
|
||||
acc[chan.id] = chan;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return set((state) => ({
|
||||
channels: {
|
||||
...state.channels,
|
||||
...updatedChannels,
|
||||
},
|
||||
channelsByUUID: {
|
||||
...state.channelsByUUID,
|
||||
...channelsByUUID,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
removeChannels: (channelIds) => {
|
||||
set((state) => {
|
||||
const updatedChannels = { ...state.channels };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue