mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
initial push for pagination with streams tables - still need to fix the channels form tables
This commit is contained in:
parent
f57e15bd00
commit
9ac73cf990
10 changed files with 228 additions and 124 deletions
|
|
@ -10,7 +10,27 @@ from django.shortcuts import get_object_or_404
|
|||
from .models import Stream, Channel, ChannelGroup
|
||||
from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer
|
||||
from .tasks import match_epg_channels
|
||||
import django_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
class StreamPagination(PageNumberPagination):
|
||||
page_size = 25 # Default page size
|
||||
page_size_query_param = 'page_size' # Allow clients to specify page size
|
||||
max_page_size = 1000 # Prevent excessive page sizes
|
||||
|
||||
class StreamFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
group_name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
m3u_account = django_filters.NumberFilter(field_name="m3u_account__id")
|
||||
m3u_account_name = django_filters.CharFilter(field_name="m3u_account__name", lookup_expr="icontains")
|
||||
m3u_account_is_active = django_filters.BooleanFilter(field_name="m3u_account__is_active")
|
||||
|
||||
class Meta:
|
||||
model = Stream
|
||||
fields = ['name', 'group_name', 'm3u_account', 'm3u_account_name', 'm3u_account_is_active']
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 1) Stream API (CRUD)
|
||||
|
|
@ -19,6 +39,13 @@ class StreamViewSet(viewsets.ModelViewSet):
|
|||
queryset = Stream.objects.all()
|
||||
serializer_class = StreamSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = StreamPagination
|
||||
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_class = StreamFilter
|
||||
search_fields = ['name', 'group_name']
|
||||
ordering_fields = ['name', 'group_name']
|
||||
ordering = ['-name']
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
|
|
|
|||
|
|
@ -74,11 +74,10 @@ class ChannelSerializer(serializers.ModelSerializer):
|
|||
required=False
|
||||
)
|
||||
|
||||
streams = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
write_only=True
|
||||
streams = serializers.SerializerMethodField()
|
||||
stream_ids = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Stream.objects.all(), many=True, write_only=True, required=False
|
||||
)
|
||||
stream_ids = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Channel
|
||||
|
|
@ -97,9 +96,17 @@ class ChannelSerializer(serializers.ModelSerializer):
|
|||
'stream_profile_id',
|
||||
]
|
||||
|
||||
def get_stream_ids(self, obj):
|
||||
"""Retrieve ordered stream IDs for GET requests."""
|
||||
return list(obj.streams.all().order_by('channelstream__order').values_list('id', flat=True))
|
||||
def get_streams(self, obj):
|
||||
"""Retrieve ordered stream objects for GET requests."""
|
||||
ordered_streams = obj.streams.all().order_by('channelstream__order')
|
||||
print(f'Retrieving streams in order')
|
||||
for index, stream in enumerate(ordered_streams):
|
||||
print(f'Stream {stream.id}, index {index}')
|
||||
return StreamSerializer(ordered_streams, many=True).data
|
||||
|
||||
# def get_stream_ids(self, obj):
|
||||
# """Retrieve ordered stream IDs for GET requests."""
|
||||
# return list(obj.streams.all().order_by('channelstream__order').values_list('id', flat=True))
|
||||
|
||||
def create(self, validated_data):
|
||||
stream_ids = validated_data.pop('streams', [])
|
||||
|
|
@ -113,8 +120,8 @@ class ChannelSerializer(serializers.ModelSerializer):
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
print("Validated Data:", validated_data)
|
||||
stream_ids = validated_data.pop('streams', None)
|
||||
print(f'stream ids: {stream_ids}')
|
||||
streams = validated_data.pop('stream_ids', None)
|
||||
print(f'stream ids: {streams}')
|
||||
|
||||
# Update the actual Channel fields
|
||||
instance.channel_number = validated_data.get('channel_number', instance.channel_number)
|
||||
|
|
@ -132,11 +139,12 @@ class ChannelSerializer(serializers.ModelSerializer):
|
|||
instance.save()
|
||||
|
||||
# Handle the many-to-many 'streams'
|
||||
if stream_ids is not None:
|
||||
if streams is not None:
|
||||
# Clear existing relationships
|
||||
instance.channelstream_set.all().delete()
|
||||
# Add new streams in order
|
||||
for index, stream_id in enumerate(stream_ids):
|
||||
ChannelStream.objects.create(channel=instance, stream_id=stream_id, order=index)
|
||||
for index, stream in enumerate(streams):
|
||||
print(f'Setting stream {stream.id} to index {index}')
|
||||
ChannelStream.objects.create(channel=instance, stream_id=stream.id, order=index)
|
||||
|
||||
return instance
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ INSTALLED_APPS = [
|
|||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ REST_FRAMEWORK = {
|
|||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
],
|
||||
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
|
|
|
|||
|
|
@ -266,6 +266,21 @@ export default class API {
|
|||
return retval;
|
||||
}
|
||||
|
||||
static async queryStreams(params) {
|
||||
const response = await fetch(
|
||||
`${host}/api/channels/streams/?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${await API.getAuthToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const retval = await response.json();
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async addStream(values) {
|
||||
const response = await fetch(`${host}/api/channels/streams/`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -36,29 +36,13 @@ import utils from '../../utils';
|
|||
import logo from '../../images/logo.png';
|
||||
import useVideoStore from '../../store/useVideoStore';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
|
||||
const ChannelStreams = ({ channel, isExpanded }) => {
|
||||
const [channelStreams, setChannelStreams] = useState([]);
|
||||
const channelStreamIds = useChannelsStore(
|
||||
(state) => state.channels[channel.id]?.stream_ids
|
||||
const channelStreams = useChannelsStore(
|
||||
(state) => state.channels[channel.id]?.streams
|
||||
);
|
||||
const { playlists } = usePlaylistsStore();
|
||||
const { streams } = useStreamsStore();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
setChannelStreams(
|
||||
streams
|
||||
.filter((stream) => channelStreamIds.includes(stream.id))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
channelStreamIds.indexOf(a.id) - channelStreamIds.indexOf(b.id)
|
||||
)
|
||||
),
|
||||
[streams, channelStreamIds]
|
||||
);
|
||||
|
||||
const removeStream = async (stream) => {
|
||||
let streamSet = new Set(channelStreams);
|
||||
|
|
@ -66,7 +50,7 @@ const ChannelStreams = ({ channel, isExpanded }) => {
|
|||
streamSet = Array.from(streamSet);
|
||||
await API.updateChannel({
|
||||
...channel,
|
||||
streams: streamSet.map((stream) => stream.id),
|
||||
stream_ids: streamSet.map((stream) => stream.id),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -113,10 +97,11 @@ const ChannelStreams = ({ channel, isExpanded }) => {
|
|||
channelStreams.splice(draggingRow.index, 1)[0]
|
||||
);
|
||||
|
||||
// setChannelStreams([...channelStreams]);
|
||||
const { streams: oldStreams, ...channelUpdate } = channel;
|
||||
|
||||
API.updateChannel({
|
||||
...channel,
|
||||
streams: channelStreams.map((stream) => stream.id),
|
||||
...channelUpdate,
|
||||
stream_ids: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -460,12 +445,6 @@ const ChannelsTable = ({}) => {
|
|||
);
|
||||
};
|
||||
|
||||
const onRowSelectionChange = (e, test) => {
|
||||
console.log(e());
|
||||
console.log(test);
|
||||
setRowSelection(e);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const selectedRows = table
|
||||
.getSelectedRowModel()
|
||||
|
|
@ -489,7 +468,7 @@ const ChannelsTable = ({}) => {
|
|||
enableColumnActions: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: onRowSelectionChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading: isLoading || channelsLoading,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import {
|
|||
import usePlaylistsStore from '../../store/playlists';
|
||||
import M3UForm from '../forms/M3U';
|
||||
import { TableHelper } from '../../helpers';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
|
||||
const Example = () => {
|
||||
const [playlist, setPlaylist] = useState(null);
|
||||
|
|
@ -34,7 +33,6 @@ const Example = () => {
|
|||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
|
||||
const playlists = usePlaylistsStore((state) => state.playlists);
|
||||
const { fetchStreams } = useStreamsStore();
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
|
|
@ -116,7 +114,6 @@ const Example = () => {
|
|||
|
||||
const deletePlaylist = async (id) => {
|
||||
await API.deletePlaylist(id);
|
||||
fetchStreams();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useCallback, useState } from 'react';
|
||||
import {
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
|
|
@ -16,7 +16,6 @@ import {
|
|||
Autocomplete,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import API from '../../api';
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
|
|
@ -24,14 +23,17 @@ import {
|
|||
Add as AddIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
PlaylistAdd as PlaylistAddIcon,
|
||||
Clear as ClearIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { TableHelper } from '../../helpers';
|
||||
import StreamForm from '../forms/Stream';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import { useDebounce } from '../../utils';
|
||||
|
||||
const StreamsTable = ({}) => {
|
||||
/**
|
||||
* useState
|
||||
*/
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [stream, setStream] = useState(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
|
@ -41,26 +43,39 @@ const StreamsTable = ({}) => {
|
|||
const [m3uOptions, setM3uOptions] = useState([]);
|
||||
const [actionsOpenRow, setActionsOpenRow] = useState(null);
|
||||
|
||||
const { streams, isLoading: streamsLoading } = useStreamsStore();
|
||||
const [data, setData] = useState([]); // Holds fetched data
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [selectedRows, setSelectedRows] = useState({});
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
});
|
||||
const [filters, setFilters] = useState({
|
||||
name: '',
|
||||
group_name: '',
|
||||
m3u_account: '',
|
||||
});
|
||||
const debouncedFilters = useDebounce(filters, 500);
|
||||
|
||||
/**
|
||||
* Stores
|
||||
*/
|
||||
const { playlists } = usePlaylistsStore();
|
||||
const { channelsPageSelection } = useChannelsStore();
|
||||
const channelSelectionStreams = useChannelsStore(
|
||||
(state) => state.channels[state.channelsPageSelection[0]?.id]?.streams
|
||||
);
|
||||
|
||||
const isMoreActionsOpen = Boolean(moreActionsAnchorEl);
|
||||
|
||||
const handleFilterChange = (columnId, value) => {
|
||||
setFilterValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[columnId]: value ? value.toLowerCase() : '',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setGroupOptions([...new Set(streams.map((stream) => stream.group_name))]);
|
||||
setM3uOptions([...new Set(playlists.map((playlist) => playlist.name))]);
|
||||
}, [streams, playlists]);
|
||||
|
||||
/**
|
||||
* useMemos
|
||||
*/
|
||||
/**
|
||||
* useMemo
|
||||
*/
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
|
@ -72,10 +87,11 @@ const StreamsTable = ({}) => {
|
|||
Header: ({ column }) => (
|
||||
<TextField
|
||||
variant="standard"
|
||||
name="name"
|
||||
label="Name"
|
||||
value={filterValues[column.id]}
|
||||
value={filters[column.id]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => handleFilterChange(column.id, e.target.value)}
|
||||
onChange={handleFilterChange}
|
||||
size="small"
|
||||
margin="none"
|
||||
fullWidth
|
||||
|
|
@ -86,27 +102,24 @@ const StreamsTable = ({}) => {
|
|||
// width: '200px', // Optional: Adjust width
|
||||
}
|
||||
}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => handleFilterChange(column.id, '')} // Clear text on click
|
||||
edge="end"
|
||||
size="small"
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<ClearIcon sx={{ fontSize: '1rem' }} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
// slotProps={{
|
||||
// input: {
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <IconButton
|
||||
// onClick={() => handleFilterChange(column.id, '')} // Clear text on click
|
||||
// edge="end"
|
||||
// size="small"
|
||||
// sx={{ p: 0 }}
|
||||
// >
|
||||
// <ClearIcon sx={{ fontSize: '1rem' }} />
|
||||
// </IconButton>
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
// },
|
||||
// }}
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
filterVariant: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Group',
|
||||
|
|
@ -173,12 +186,49 @@ const StreamsTable = ({}) => {
|
|||
),
|
||||
},
|
||||
],
|
||||
[playlists, groupOptions, m3uOptions]
|
||||
[playlists, groupOptions, m3uOptions, filters]
|
||||
);
|
||||
|
||||
const rowVirtualizerInstanceRef = useRef(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sorting, setSorting] = useState([]);
|
||||
/**
|
||||
* Functions
|
||||
*/
|
||||
const handleFilterChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', pagination.pageIndex + 1);
|
||||
params.append('page_size', pagination.pageSize);
|
||||
|
||||
// Apply sorting
|
||||
if (sorting.length > 0) {
|
||||
const sortField = sorting[0].id;
|
||||
const sortDirection = sorting[0].desc ? '-' : '';
|
||||
params.append('ordering', `${sortDirection}${sortField}`);
|
||||
}
|
||||
|
||||
// Apply debounced filters
|
||||
Object.entries(debouncedFilters).forEach(([key, value]) => {
|
||||
if (value) params.append(key, value);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await API.queryStreams(params);
|
||||
setData(result.results);
|
||||
setRowCount(result.count);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [pagination, sorting, debouncedFilters]);
|
||||
|
||||
// Fallback: Individual creation (optional)
|
||||
const createChannelFromStream = async (stream) => {
|
||||
|
|
@ -229,20 +279,6 @@ const StreamsTable = ({}) => {
|
|||
setModalOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
const addStreamsToChannel = async () => {
|
||||
const channel = channelsPageSelection[0];
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
|
@ -250,17 +286,23 @@ const StreamsTable = ({}) => {
|
|||
...channel,
|
||||
streams: [
|
||||
...new Set(
|
||||
channel.stream_ids.concat(selectedRows.map((row) => row.original.id))
|
||||
channel.streams
|
||||
.map((stream) => stream.id)
|
||||
.concat(selectedRows.map((row) => row.original.id))
|
||||
),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const addStreamToChannel = async (streamId) => {
|
||||
const channel = channelsPageSelection[0];
|
||||
const { streams, ...channel } = { ...channelsPageSelection[0] };
|
||||
await API.updateChannel({
|
||||
...channel,
|
||||
streams: [...new Set(channel.stream_ids.concat([streamId]))],
|
||||
stream_ids: [
|
||||
...new Set(
|
||||
channelSelectionStreams.map((stream) => stream.id).concat([streamId])
|
||||
),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -274,30 +316,27 @@ const StreamsTable = ({}) => {
|
|||
setActionsOpenRow(null);
|
||||
};
|
||||
|
||||
const filteredData = streams.filter((row) =>
|
||||
columns.every(({ accessorKey }) =>
|
||||
filterValues[accessorKey]
|
||||
? row[accessorKey]?.toLowerCase().includes(filterValues[accessorKey])
|
||||
: true
|
||||
)
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
data: filteredData,
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
data,
|
||||
enablePagination: true,
|
||||
manualPagination: true,
|
||||
manualSorting: true,
|
||||
enableBottomToolbar: true,
|
||||
enableStickyHeader: true,
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
rowCount: rowCount,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading: isLoading || streamsLoading,
|
||||
isLoading: isLoading,
|
||||
sorting,
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowVirtualizerInstanceRef,
|
||||
rowVirtualizerOptions: { overscan: 5 },
|
||||
enableRowActions: true,
|
||||
positionActionsColumn: 'first',
|
||||
renderRowActions: ({ row }) => (
|
||||
|
|
@ -310,7 +349,10 @@ const StreamsTable = ({}) => {
|
|||
sx={{ py: 0, px: 0.5 }}
|
||||
disabled={
|
||||
channelsPageSelection.length != 1 ||
|
||||
channelsPageSelection[0]?.stream_ids.includes(row.original.id)
|
||||
(channelSelectionStreams &&
|
||||
channelSelectionStreams
|
||||
.map((stream) => stream.id)
|
||||
.includes(row.original.id))
|
||||
}
|
||||
>
|
||||
<PlaylistAddIcon fontSize="small" />
|
||||
|
|
@ -352,9 +394,14 @@ const StreamsTable = ({}) => {
|
|||
</Menu>
|
||||
</>
|
||||
),
|
||||
muiPaginationProps: {
|
||||
size: 'small',
|
||||
rowsPerPageOptions: [25, 50, 100, 250, 500],
|
||||
labelRowsPerPage: 'Rows per page',
|
||||
},
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: 'calc(100vh - 75px)',
|
||||
height: 'calc(100vh - 145px)',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
|
|
@ -400,7 +447,7 @@ const StreamsTable = ({}) => {
|
|||
sx={{ marginLeft: 1 }}
|
||||
disabled={selectedRowCount == 0}
|
||||
>
|
||||
Create Channels
|
||||
CREATE CHANNELS
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
|
|
@ -411,13 +458,26 @@ const StreamsTable = ({}) => {
|
|||
channelsPageSelection.length != 1 || selectedRowCount == 0
|
||||
}
|
||||
>
|
||||
Add to Channel
|
||||
ADD TO CHANNEL
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* useEffects
|
||||
*/
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<MaterialReactTable table={table} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { create } from 'zustand';
|
||||
import API from '../api';
|
||||
import useChannelsStore from './channels';
|
||||
import useStreamsStore from './streams';
|
||||
import useUserAgentsStore from './userAgents';
|
||||
import usePlaylistsStore from './playlists';
|
||||
import useEPGsStore from './epgs';
|
||||
|
|
@ -32,7 +31,6 @@ const useAuthStore = create((set, get) => ({
|
|||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
useChannelsStore.getState().fetchChannelGroups(),
|
||||
useStreamsStore.getState().fetchStreams(),
|
||||
useUserAgentsStore.getState().fetchUserAgents(),
|
||||
usePlaylistsStore.getState().fetchPlaylists(),
|
||||
useEPGsStore.getState().fetchEPGs(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export default {
|
||||
Limiter: (n, list) => {
|
||||
if (!list || !list.length) {
|
||||
|
|
@ -34,3 +36,18 @@ export default {
|
|||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Custom debounce hook
|
||||
export function useDebounce(value, delay = 500) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(handler); // Cleanup timeout on unmount or value change
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,3 +25,4 @@ uwsgi
|
|||
channels
|
||||
channels-redis
|
||||
daphne
|
||||
django-filter
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue