diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4f1856f1..9f680d3e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -16,6 +16,7 @@
"@mantine/hooks": "^7.17.2",
"@mantine/notifications": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
+ "@tanstack/react-table": "^8.21.2",
"allotment": "^1.20.3",
"axios": "^1.8.2",
"clsx": "^2.1.1",
@@ -31,6 +32,7 @@
"react-draggable": "^4.4.6",
"react-pro-sidebar": "^1.1.0",
"react-router-dom": "^7.3.0",
+ "react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.11",
"recharts": "^2.15.1",
"video.js": "^8.21.0",
@@ -1741,12 +1743,12 @@
}
},
"node_modules/@tanstack/react-table": {
- "version": "8.20.5",
- "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
- "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
+ "version": "8.21.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
+ "integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==",
"license": "MIT",
"dependencies": {
- "@tanstack/table-core": "8.20.5"
+ "@tanstack/table-core": "8.21.2"
},
"engines": {
"node": ">=12"
@@ -1778,9 +1780,9 @@
}
},
"node_modules/@tanstack/table-core": {
- "version": "8.20.5",
- "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
- "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
+ "version": "8.21.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
+ "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -3452,6 +3454,39 @@
"react-dom": ">=18.0"
}
},
+ "node_modules/mantine-react-table/node_modules/@tanstack/react-table": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
+ "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/table-core": "8.20.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/mantine-react-table/node_modules/@tanstack/table-core": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
+ "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4081,6 +4116,16 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/react-virtualized-auto-sizer": {
+ "version": "1.0.26",
+ "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
+ "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-window": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 5677558b..c0d6ced3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,6 +18,7 @@
"@mantine/hooks": "^7.17.2",
"@mantine/notifications": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
+ "@tanstack/react-table": "^8.21.2",
"allotment": "^1.20.3",
"axios": "^1.8.2",
"clsx": "^2.1.1",
@@ -33,6 +34,7 @@
"react-draggable": "^4.4.6",
"react-pro-sidebar": "^1.1.0",
"react-router-dom": "^7.3.0",
+ "react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.11",
"recharts": "^2.15.1",
"video.js": "^8.21.0",
diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx
index 1d33565f..f9ab1cc3 100644
--- a/frontend/src/WebSocket.jsx
+++ b/frontend/src/WebSocket.jsx
@@ -122,7 +122,7 @@ export const WebsocketProvider = ({ children }) => {
message: 'EPG match is complete!',
color: 'green.5',
});
- fetchChannels();
+ // fetchChannels();
fetchEPGData();
break;
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 92dc84e8..411a7f26 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -320,7 +320,7 @@ export default class API {
});
// Optionally refesh the channel list in Zustand
- await useChannelsStore.getState().fetchChannels();
+ // await useChannelsStore.getState().fetchChannels();
return response;
} catch (e) {
@@ -604,7 +604,7 @@ export default class API {
usePlaylistsStore.getState().removePlaylists([id]);
// @TODO: MIGHT need to optimize this later if someone has thousands of channels
// but I'm feeling laze right now
- useChannelsStore.getState().fetchChannels();
+ // useChannelsStore.getState().fetchChannels();
} catch (e) {
errorNotification(`Failed to delete playlist ${id}`, e);
}
diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx
index 4a6bc014..d3d4b868 100644
--- a/frontend/src/components/tables/ChannelsTable.jsx
+++ b/frontend/src/components/tables/ChannelsTable.jsx
@@ -5,7 +5,6 @@ import React, {
useState,
useCallback,
} from 'react';
-import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
import useChannelsStore from '../../store/channels';
import { notifications } from '@mantine/notifications';
import API from '../../api';
@@ -55,7 +54,21 @@ import {
MultiSelect,
Pagination,
NativeSelect,
+ Checkbox,
+ Table,
} from '@mantine/core';
+import AutoSizer from 'react-virtualized-auto-sizer'
+import { FixedSizeList as List } from 'react-window'
+import ChannelsTableRow from './ChannelsTable/ChannelsTableRow';
+import ChannelsTableBody from './ChannelsTable/ChannelsTableBody';
+import {
+ flexRender,
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ getPaginationRowModel,
+} from '@tanstack/react-table'
+import { notUndefined, useVirtualizer } from '@tanstack/react-virtual'
const ChannelStreams = React.memo(({ channel, isExpanded }) => {
const channelStreams = useChannelsStore(
@@ -71,7 +84,7 @@ const ChannelStreams = React.memo(({ channel, isExpanded }) => {
});
};
- const channelStreamsTable = useMantineReactTable({
+ const channelStreamsTable = useReactTable({
...TableHelper.defaultProperties,
data: channelStreams,
columns: useMemo(
@@ -250,6 +263,160 @@ const CreateProfilePopover = React.memo(({ }) => {
);
});
+const ChannelEnabledCell = ({ cell, row, toggleChannelEnabled, selectedProfileId }) => {
+ const handleSwitchChange = useCallback(() => {
+ toggleChannelEnabled([row.original.id], !cell.getValue());
+ }, [cell.getValue(), row.original.id, toggleChannelEnabled]);
+
+ return (
+
+
+
+ );
+}
+
+const ChannelLogoCell = React.memo(({ cell }) => {
+ return (
+
+
+
+ )
+})
+
+const ChannelSelectCell = React.memo(({
+ checked,
+ disabled,
+ indeterminate,
+ onChange,
+}) => {
+ return (
+
+
+
+
+
+ )
+})
+
+const RowActions = React.memo(({
+ row,
+ editChannel,
+ deleteChannel,
+ handleWatchStream,
+ createRecording,
+}) => {
+ const theme = useMantineTheme()
+ const { channelsPageSelection } = useChannelsStore()
+
+ const onEdit = useCallback(() => {
+ editChannel(row.original)
+ }, [row]);
+
+ const onDelete = useCallback(async () => {
+ deleteChannel(row.original)
+ }, [row]);
+
+ const onRecord = useCallback(() => {
+ createRecording(row.original)
+ }, [row]);
+
+ const onPreview = useCallback(() => {
+ handleWatchStream(row.original)
+ }, [row]);
+
+ return (
+
+
+
+
+
+
+ 0 &&
+ !channelsPageSelection.map((row) => row.id).includes(row.id)
+ }
+ >
+ {channelsPageSelection.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {/* {env_mode == 'dev' && (
+
+ )} */}
+
+
+ );
+});
+
const ChannelsTable = React.memo(({ }) => {
const {
channels,
@@ -277,17 +444,17 @@ const ChannelsTable = React.memo(({ }) => {
);
const [channelsEnabledHeaderSwitch, setChannelsEnabledHeaderSwitch] =
useState(false);
- const [initialDataCount, setInitialDataCount] = useState(null);
- const [data, setData] = useState([]);
+ const [data, setData] = useState([]); // Holds fetched data
+ const [selectedRowIds, setSelectedRowIds] = useState([]);
const [rowCount, setRowCount] = useState(0);
const [pageCount, setPageCount] = useState(0);
const [paginationString, setPaginationString] = useState('');
- const [selectedStreamIds, setSelectedStreamIds] = useState([]);
- // const [allRowsSelected, setAllRowsSelected] = useState(false);
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 250,
});
+ const [groupOptions, setGroupOptions] = useState([]);
+ const [initialDataCount, setInitialDataCount] = useState(null);
const [filters, setFilters] = useState({
name: '',
channel_group: '',
@@ -295,6 +462,7 @@ const ChannelsTable = React.memo(({ }) => {
});
const debouncedFilters = useDebounce(filters, 500);
const [isLoading, setIsLoading] = useState(true);
+ const [selectedChannelIds, setSelectedChannelIds] = useState([]);
const [sorting, setSorting] = useState([
{ id: 'channel_number', desc: false },
{ id: 'name', desc: false },
@@ -326,19 +494,23 @@ const ChannelsTable = React.memo(({ }) => {
useEffect(() => {
setChannelGroupOptions([
...new Set(
- Object.values(channels).map((channel) => channel.channel_group?.name)
+ Object.values(data).map((channel) => channel.channel_group?.name)
),
]);
- }, [channels]);
+ }, [data]);
- const handleFilterChange = (columnId, value) => {
- setFilterValues((prev) => ({
+ const handleFilterChange = (e) => {
+ const { name, value } = e.target;
+ setFilters((prev) => ({
...prev,
- [columnId]: Array.isArray(value)
- ? value
- : value
- ? value.toLowerCase()
- : '',
+ [name]: value,
+ }));
+ };
+
+ const handleGroupChange = (value) => {
+ setFilters((prev) => ({
+ ...prev,
+ channel_group: value ? value : '',
}));
};
@@ -387,7 +559,7 @@ const ChannelsTable = React.memo(({ }) => {
const newSelection = {};
result.results.forEach((item, index) => {
- if (selectedStreamIds.includes(item.id)) {
+ if (selectedChannelIds.includes(item.id)) {
newSelection[index] = true;
}
});
@@ -412,7 +584,7 @@ const ChannelsTable = React.memo(({ }) => {
const newRowSelection =
typeof updater === 'function' ? updater(prevRowSelection) : updater;
- const updatedSelected = new Set([...selectedStreamIds]);
+ const updatedSelected = new Set([...selectedChannelIds]);
table.getRowModel().rows.forEach((row) => {
if (newRowSelection[row.id] === undefined || !newRowSelection[row.id]) {
updatedSelected.delete(row.original.id);
@@ -420,7 +592,7 @@ const ChannelsTable = React.memo(({ }) => {
updatedSelected.add(row.original.id);
}
});
- setSelectedStreamIds([...updatedSelected]);
+ setSelectedChannelIds([...updatedSelected]);
return newRowSelection;
});
@@ -435,9 +607,9 @@ const ChannelsTable = React.memo(({ }) => {
if (value) params.append(key, value);
});
const ids = await API.getAllStreamIds(params);
- setSelectedStreamIds(ids);
+ setSelectedChannelIds(ids);
} else {
- setSelectedStreamIds([]);
+ setSelectedChannelIds([]);
}
const newSelection = {};
@@ -489,9 +661,13 @@ const ChannelsTable = React.memo(({ }) => {
/>
));
- const renderEnabledHeader = useCallback(() => {
+ const renderEnabledHeader = useCallback(({ header }) => {
if (Object.values(rowSelection).length === 0) {
- return ;
+ return (
+
+
+
+ );
}
const handleToggle = () => {
@@ -518,109 +694,82 @@ const ChannelsTable = React.memo(({ }) => {
// Configure columns
const columns = useMemo(
() => [
+ {
+ id: 'select',
+ size: 20,
+ meta: {
+ minWidth: 20,
+ maxWidth: 20,
+ },
+ header: ({ table }) => (
+
+
+
+ ),
+ cell: ({ row }) => (
+
+ ),
+ },
{
id: 'enabled',
- Header: renderEnabledHeader,
- enableSorting: false,
+ size: 32,
+ meta: {
+ minWidth: 32,
+ maxWidth: 32,
+ },
+ header: renderEnabledHeader,
accessorFn: (row) => {
return selectedProfileId == '0'
? true
: enabledChannelSet.has(row.id);
},
- mantineTableHeadCellProps: {
- align: 'right',
- style: {
- backgroundColor: '#3F3F46',
- width: '40px',
- minWidth: '40px',
- maxWidth: '40px',
- // // minWidth: '20px',
- // // width: '50px !important',
- // // justifyContent: 'center',
- padding: 0,
- // // paddingLeft: 8,
- // // paddingRight: 0,
- },
- },
- mantineTableBodyCellProps: {
- align: 'right',
- style: {
- width: '40px',
- minWidth: '40px',
- maxWidth: '40px',
- // // minWidth: '20px',
- // // justifyContent: 'center',
- // // paddingLeft: 0,
- // // paddingRight: 0,
- padding: 0,
- },
- },
- Cell: ({ row, cell }) => {
- const memoizedCellValue = useMemo(() => cell.getValue(), [cell]);
- const handleSwitchChange = useCallback(() => {
- toggleChannelEnabled([row.original.id], !memoizedCellValue);
- }, [memoizedCellValue, row.original.id, toggleChannelEnabled]);
-
- return (
-
- );
- },
+ cell: ({ row, cell }) => (
+
+ ),
},
{
- header: '#',
- size: 50,
- maxSize: 50,
+ size: 26,
+ maxSize: 26,
accessorKey: 'channel_number',
- sortingFn: (a, b, columnId) => {
- return (
- parseInt(a.original.channel_number) -
- parseInt(b.original.channel_number)
- );
- },
- mantineTableHeadCellProps: {
- align: 'right',
- // // style: {
- // // backgroundColor: '#3F3F46',
- // // // minWidth: '20px',
- // // // justifyContent: 'center',
- // // // paddingLeft: 15,
- // // paddingRight: 0,
- // // },
- },
- mantineTableBodyCellProps: {
- align: 'right',
- // // style: {
- // // minWidth: '20px',
- // // // justifyContent: 'center',
- // // paddingLeft: 0,
- // // paddingRight: 0,
- // // },
+ header: ({ header }) => (
+ #
+ ),
+ meta: {
+ align: 'right'
},
+ // cell: ({ cell }) => (
+ //
+ // {cell.getValue()}
+ //
+ // )
},
{
- header: 'Name',
accessorKey: 'name',
- Header: ({ column }) => (
+ header: ({ column }) => (
{
- e.stopPropagation();
- handleFilterChange(column.id, e.target.value);
- }}
+ onClick={(e) => e.stopPropagation()}
+ onChange={handleFilterChange}
size="xs"
variant="unstyled"
className="table-input-header"
- onClick={(e) => e.stopPropagation()}
/>
),
- Cell: ({ cell }) => (
+ cell: ({ cell }) => (
{
),
},
{
- header: 'Group',
accessorKey: 'channel_group.name',
accessorFn: (row) => row.channel_group?.name || '',
- Cell: ({ cell }) => (
+ cell: ({ cell }) => (
{
{cell.getValue()}
),
- Header: ({ column }) => (
+ header: ({ column }) => (
e.stopPropagation()}>
{
- handleFilterChange(column.id, value);
- }}
+ onChange={handleGroupChange}
data={channelGroupOptions}
variant="unstyled"
className="table-input-header custom-multiselect"
@@ -675,27 +821,23 @@ const ChannelsTable = React.memo(({ }) => {
maxWidth: '75px',
},
},
- Cell: ({ cell }) => (
-
-
-
+ cell: ({ cell }) => (
+
),
},
+ {
+ header: 'Actions',
+ size: 40,
+ cell: ({ row }) => (
+
+ )
+ }
],
[
channelGroupOptions,
@@ -703,8 +845,7 @@ const ChannelsTable = React.memo(({ }) => {
selectedProfile,
selectedProfileChannels,
rowSelection,
- channelsPageSelection,
- channelsEnabledHeaderSwitch,
+ // channelsEnabledHeaderSwitch,
]
);
@@ -716,6 +857,7 @@ const ChannelsTable = React.memo(({ }) => {
.rows.filter((row) => row.getIsSelected());
await API.deleteChannels(selected.map((row) => row.original.id));
+ fetchData();
setIsLoading(false);
};
@@ -739,7 +881,7 @@ const ChannelsTable = React.memo(({ }) => {
});
// Refresh the channel list
- await fetchChannels();
+ // await fetchChannels();
} catch (err) {
console.error(err);
notifications.show({
@@ -811,41 +953,26 @@ const ChannelsTable = React.memo(({ }) => {
handleCopy(hdhrUrl, hdhrUrlRef);
};
+ // useEffect(() => {
+ // const selectedRows = table
+ // .getSelectedRowModel()
+ // .rows.map((row) => row.original);
+ // setChannelsPageSelection(selectedRows);
+
+ // if (selectedProfileId != '0') {
+ // setChannelsEnabledHeaderSwitch(
+ // selectedRows.filter(
+ // (row) =>
+ // selectedProfileChannels.find((channel) => row.id == channel.id)
+ // .enabled
+ // ).length == selectedRows.length
+ // );
+ // }
+ // }, [rowSelection])
+
useEffect(() => {
- const selectedRows = table
- .getSelectedRowModel()
- .rows.map((row) => row.original);
- setChannelsPageSelection(selectedRows);
-
- if (selectedProfileId != '0') {
- setChannelsEnabledHeaderSwitch(
- selectedRows.filter(
- (row) =>
- selectedProfileChannels.find((channel) => row.id == channel.id)
- .enabled
- ).length == selectedRows.length
- );
- }
- }, [rowSelection]);
-
- const filteredData = Object.values(channels).filter((row) =>
- columns.every(({ accessorKey }) => {
- if (!accessorKey) {
- return true;
- }
-
- const filterValue = filterValues[accessorKey];
- const rowValue = getDescendantProp(row, accessorKey);
-
- if (Array.isArray(filterValue) && filterValue.length != 0) {
- return filterValue.includes(rowValue);
- } else if (filterValue) {
- return rowValue?.toLowerCase().includes(filterValues[accessorKey]);
- }
-
- return true;
- })
- );
+ fetchData();
+ }, [fetchData]);
const deleteProfile = async (id) => {
await API.deleteChannelProfile(id);
@@ -872,257 +999,71 @@ const ChannelsTable = React.memo(({ }) => {
);
};
- const RowActions = React.memo(({ row }) => {
- const editChannel = useCallback(() => {
- setChannel(row.original);
- setChannelModalOpen(true);
- }, []);
+ const editChannel = useCallback((row) => {
+ setChannel(row.original);
+ setChannelModalOpen(true);
+ }, []);
- const deleteChannel = useCallback(async () => {
- setRowSelection([]);
- // if (channelsPageSelection.length > 0) {
- // return deleteChannels();
- // }
- await API.deleteChannel(row.id);
- }, []);
+ const deleteChannel = useCallback(async (row) => {
+ console.log(row)
+ setRowSelection([]);
+ // if (channelsPageSelection.length > 0) {
+ // return deleteChannels();
+ // }
+ await API.deleteChannel(row.id);
+ }, []);
- const createRecording = useCallback(() => {
- setChannel(row);
- setRecordingModalOpen(true);
- }, []);
+ const createRecording = useCallback((row) => {
+ setChannel(row);
+ setRecordingModalOpen(true);
+ }, []);
- const handleWatchStream = useCallback(() => {
- let vidUrl = `/proxy/ts/stream/${row.uuid}`;
- if (env_mode == 'dev') {
- vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`;
- }
- showVideo(vidUrl);
- }, []);
+ const handleWatchStream = useCallback((row) => {
+ let vidUrl = `/proxy/ts/stream/${row.uuid}`;
+ if (env_mode == 'dev') {
+ vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`;
+ }
+ showVideo(vidUrl);
+ }, []);
- return (
-
-
-
-
-
-
- 0 &&
- !channelsPageSelection.map((row) => row.id).includes(row.id)
- }
- >
- {channelsPageSelection.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- {env_mode == 'dev' && (
-
- }
- >
- Record
-
-
-
- )}
-
-
- );
- });
-
- const table = useMantineReactTable({
- ...TableHelper.defaultProperties,
- columns,
+ const table = useReactTable({
data,
- enablePagination: true,
- manualPagination: true,
- enableColumnActions: false,
+ columns,
+ // filterFns: {},
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ // getPaginationRowModel: getPaginationRowModel(),
+ // manualPagination: true,
enableRowSelection: true,
- renderTopToolbar: false,
- onRowSelectionChange: onRowSelectionChange,
- onSortingChange: setSorting,
- state: {
- isLoading: isLoading || channelsLoading,
- sorting,
- rowSelection,
- },
- enableBottomToolbar: true,
- renderBottomToolbar: ({ table }) => (
-
- Page Size
-
-
- {paginationString}
-
- ),
- initialState: {
- density: 'compact',
- sorting: [
- {
- id: 'channel_number',
- desc: false,
- },
- ],
- },
- enableRowActions: true,
- enableExpandAll: false,
- displayColumnDefOptions: {
- 'mrt-row-select': {
- size: 10,
- maxSize: 10,
- mantineTableHeadCellProps: {
- align: 'right',
- style: {
- paddding: 0,
- // paddingLeft: 7,
- width: '20px',
- minWidth: '20px',
- backgroundColor: '#3F3F46',
- },
- },
- mantineTableBodyCellProps: {
- align: 'right',
- style: {
- paddingLeft: 0,
- width: '20px',
- minWidth: '20px',
- },
- },
- },
- 'mrt-row-expand': {
- size: 20,
- maxSize: 20,
- header: '',
- mantineTableHeadCellProps: {
- style: {
- padding: 0,
- paddingLeft: 2,
- width: '20px',
- minWidth: '20px',
- maxWidth: '20px',
- backgroundColor: '#3F3F46',
- },
- },
- mantineTableBodyCellProps: {
- style: {
- padding: 0,
- paddingLeft: 2,
- width: '20px',
- minWidth: '20px',
- maxWidth: '20px',
- },
- },
- },
- 'mrt-row-actions': {
- size: 85,
- maxWidth: 85,
- mantineTableHeadCellProps: {
- align: 'center',
- style: {
- minWidth: '85px',
- maxWidth: '85px',
- // paddingRight: 40,
- fontWeight: 'normal',
- color: 'rgb(207,207,207)',
- backgroundColor: '#3F3F46',
- },
- },
- mantineTableBodyCellProps: {
- style: {
- minWidth: '85px',
- maxWidth: '85px',
- paddingLeft: 0,
- // paddingRight: 10,
- },
- },
- },
- },
- mantineExpandButtonProps: ({ row, table }) => ({
- onClick: () => {
- setRowSelection({ [row.index]: true });
- table.setExpanded({ [row.id]: !row.getIsExpanded() });
- },
- size: 'xs',
- style: {
- transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)',
- transition: 'transform 0.2s',
- },
- }),
- renderDetailPanel: ({ row }) => (
-
- ),
- renderRowActions: ({ row }) => ,
- mantineTableContainerProps: {
- style: {
- height: 'calc(100vh - 150px)',
- overflowY: 'auto',
- // margin: 5,
- },
- },
- });
+ // debugTable: true,
+ // debugHeaders: true,
+ // debugColumns: false,
+ })
+
+ const { rows } = table.getRowModel()
+
+ const virtualizerRef = useRef(null)
+ const virtualizer = useVirtualizer({
+ count: rows.length,
+ getScrollElement: () => virtualizerRef.current,
+ estimateSize: () => 21,
+ overscan: 20,
+ })
+ const items = virtualizer.getVirtualItems()
+
+ const [before, after] =
+ items.length > 0
+ ? [
+ notUndefined(items[0]).start - virtualizer.options.scrollMargin,
+ virtualizer.getTotalSize() - notUndefined(items[items.length - 1]).end
+ ]
+ : [0, 0];
return (
{/* Header Row: outside the Paper */}
{
{/* Paper container: contains top toolbar and table (or ghost state) */}
-
- {/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
-
-
-
+ {/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
+
+
+
- {/* Table or ghost empty state inside Paper */}
-
- {Object.keys(channels).length === 0 && (
-
+
+ }
+ variant="default"
+ size="xs"
+ onClick={deleteChannels}
+ disabled={Object.values(rowSelection).length == 0}
+ >
+ Remove
+
+
+
+ }
+ variant="default"
+ size="xs"
+ onClick={assignChannels}
+ p={5}
+ >
+ Assign
+
+
+
+
+ }
+ variant="default"
+ size="xs"
+ onClick={matchEpg}
+ p={5}
+ >
+ Auto-Match
+
+
+
+ }
+ variant="light"
+ size="xs"
+ onClick={() => editChannel()}
+ p={5}
+ color={theme.tailwind.green[5]}
style={{
- paddingTop: 20,
- bgcolor: theme.palette.background.paper,
+ borderWidth: '1px',
+ borderColor: theme.tailwind.green[5],
+ color: 'white',
}}
>
-
-
-
- It’s recommended to create channels after adding your M3U or
- streams.
-
-
- You can still create channels without streams if you’d like,
- and map them later.
-
- }
- variant="light"
- size="xs"
- onClick={() => editChannel()}
- color="gray"
- style={{
- marginTop: 20,
- borderWidth: '1px',
- borderColor: 'gray',
- color: 'white',
- }}
- >
- Create Channel
-
-
-
-
-
-
-
-
- )}
+ Add
+
+
- {Object.keys(channels).length > 0 && (
-
+
+
+ {/* Table or ghost empty state inside Paper */}
+
+ {initialDataCount === 0 && (
+
)}
-
+
+
+
+ {initialDataCount > 0 && (
+
+
+
+ {table.getHeaderGroups().map(headerGroup => (
+
+ {headerGroup.headers.map(header => {
+ return (
+
+ {flexRender(header.column.columnDef.header, header.getContext())}
+
+ )
+ })}
+
+ ))}
+
+
+
+
+ )}
{
isOpen={recordingModalOpen}
onClose={closeRecordingForm}
/>
-
+
);
});
diff --git a/frontend/src/components/tables/ChannelsTable/ChannelsTableBody.jsx b/frontend/src/components/tables/ChannelsTable/ChannelsTableBody.jsx
new file mode 100644
index 00000000..2f50cb23
--- /dev/null
+++ b/frontend/src/components/tables/ChannelsTable/ChannelsTableBody.jsx
@@ -0,0 +1,86 @@
+// HeadlessChannelsTable.jsx
+import React, { useMemo, useState, useCallback, useRef } from 'react';
+import { FixedSizeList as List } from 'react-window';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import {
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ flexRender,
+ getExpandedRowModel,
+} from '@tanstack/react-table';
+import {
+ Table,
+ Box,
+ Checkbox,
+ ActionIcon,
+ ScrollArea,
+} from '@mantine/core';
+import { ChevronRight, ChevronDown } from 'lucide-react';
+import ChannelsTableRow from './ChannelsTableRow';
+import { useVirtualizer } from '@tanstack/react-virtual'
+
+const ChannelsTableBody = ({ rows, height, onEdit, onDelete, onPreview, onRecord, virtualizedItems }) => {
+ const rowHeight = 48;
+
+ // return (
+ //
+ //
+ // {({ height }) => (
+ //
+ // {({ index, style }) => {
+ // const row = rows[index];
+ // return (
+ //
+ //
+ // {row.getIsExpanded() && }
+ //
+ // );
+ // }}
+ //
+ // )}
+ //
+ //
+ // );
+
+ return (
+
+ {virtualizedItems.map((virtualRow, index) => {
+ const row = rows[virtualRow.index]
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default ChannelsTableBody;
diff --git a/frontend/src/components/tables/ChannelsTable/ChannelsTableRow.jsx b/frontend/src/components/tables/ChannelsTable/ChannelsTableRow.jsx
new file mode 100644
index 00000000..94466e13
--- /dev/null
+++ b/frontend/src/components/tables/ChannelsTable/ChannelsTableRow.jsx
@@ -0,0 +1,61 @@
+// HeadlessChannelsTable.jsx
+import React, { useMemo, useState, useCallback } from 'react';
+import { FixedSizeList as List } from 'react-window';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import {
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ flexRender,
+ getExpandedRowModel,
+} from '@tanstack/react-table';
+import {
+ Table,
+ Box,
+ Checkbox,
+ ActionIcon,
+ ScrollArea,
+ Center,
+ useMantineTheme,
+} from '@mantine/core';
+import { ChevronRight, ChevronDown } from 'lucide-react';
+import useSettingsStore from '../../../store/settings';
+import useChannelsStore from '../../../store/channels';
+
+const ExpandIcon = ({ row, toggle }) => (
+
+ {row.getIsExpanded() ? : }
+
+);
+
+const ChannelsTableRow = ({ row, virtualRow, index, style, onEdit, onDelete, onPreview, onRecord }) => {
+ return (
+
+ {row.getVisibleCells().map(cell => {
+ return (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ )
+ })}
+
+ )
+};
+
+export default ChannelsTableRow
diff --git a/frontend/src/components/tables/ChannelsTable/EmptyChannelsTableGuide.jsx b/frontend/src/components/tables/ChannelsTable/EmptyChannelsTableGuide.jsx
new file mode 100644
index 00000000..9f719cda
--- /dev/null
+++ b/frontend/src/components/tables/ChannelsTable/EmptyChannelsTableGuide.jsx
@@ -0,0 +1,78 @@
+export default () => {
+ return (
+
+
+
+
+ It’s recommended to create channels after adding your M3U or
+ streams.
+
+
+ You can still create channels without streams if you’d like,
+ and map them later.
+
+ }
+ variant="light"
+ size="xs"
+ onClick={() => editChannel()}
+ color="gray"
+ style={{
+ marginTop: 20,
+ borderWidth: '1px',
+ borderColor: 'gray',
+ color: 'white',
+ }}
+ >
+ Create Channel
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/store/auth.jsx b/frontend/src/store/auth.jsx
index d6eb8053..3e45d7d9 100644
--- a/frontend/src/store/auth.jsx
+++ b/frontend/src/store/auth.jsx
@@ -32,7 +32,7 @@ const useAuthStore = create((set, get) => ({
initData: async () => {
await Promise.all([
- useChannelsStore.getState().fetchChannels(),
+ // useChannelsStore.getState().fetchChannels(),
useChannelsStore.getState().fetchChannelGroups(),
useChannelsStore.getState().fetchLogos(),
useChannelsStore.getState().fetchChannelProfiles(),
diff --git a/frontend/src/store/channels.jsx b/frontend/src/store/channels.jsx
index 53cce7af..f0987f44 100644
--- a/frontend/src/store/channels.jsx
+++ b/frontend/src/store/channels.jsx
@@ -310,75 +310,75 @@ const useChannelsStore = create((set, get) => ({
})),
setChannelStats: (stats) => {
- return set((state) => {
- const {
- channels,
- stats: currentStats,
- activeChannels: oldChannels,
- activeClients: oldClients,
- channelsByUUID,
- } = state;
+ // return set((state) => {
+ // const {
+ // channels,
+ // stats: currentStats,
+ // activeChannels: oldChannels,
+ // activeClients: oldClients,
+ // channelsByUUID,
+ // } = state;
- const newClients = {};
- const newChannels = stats.channels.reduce((acc, ch) => {
- acc[ch.channel_id] = ch;
+ // const newClients = {};
+ // const newChannels = stats.channels.reduce((acc, ch) => {
+ // acc[ch.channel_id] = ch;
- if (currentStats.channels) {
- if (oldChannels[ch.channel_id] === undefined) {
- notifications.show({
- title: 'New channel streaming',
- message: channels[channelsByUUID[ch.channel_id]].name,
- color: 'blue.5',
- });
- }
- }
+ // if (currentStats.channels) {
+ // if (oldChannels[ch.channel_id] === undefined) {
+ // notifications.show({
+ // title: 'New channel streaming',
+ // message: channels[channelsByUUID[ch.channel_id]].name,
+ // color: 'blue.5',
+ // });
+ // }
+ // }
- ch.clients.map((client) => {
- newClients[client.client_id] = client;
- // This check prevents the notifications if streams are active on page load
- if (currentStats.channels) {
- if (oldClients[client.client_id] === undefined) {
- notifications.show({
- title: 'New client started streaming',
- message: `Client streaming from ${client.ip_address}`,
- color: 'blue.5',
- });
- }
- }
- });
+ // ch.clients.map((client) => {
+ // newClients[client.client_id] = client;
+ // // This check prevents the notifications if streams are active on page load
+ // if (currentStats.channels) {
+ // if (oldClients[client.client_id] === undefined) {
+ // notifications.show({
+ // title: 'New client started streaming',
+ // message: `Client streaming from ${client.ip_address}`,
+ // color: 'blue.5',
+ // });
+ // }
+ // }
+ // });
- return acc;
- }, {});
+ // return acc;
+ // }, {});
- // This check prevents the notifications if streams are active on page load
- if (currentStats.channels) {
- for (const uuid in oldChannels) {
- if (newChannels[uuid] === undefined) {
- notifications.show({
- title: 'Channel streaming stopped',
- message: channels[channelsByUUID[uuid]].name,
- color: 'blue.5',
- });
- }
- }
+ // // This check prevents the notifications if streams are active on page load
+ // if (currentStats.channels) {
+ // for (const uuid in oldChannels) {
+ // if (newChannels[uuid] === undefined) {
+ // notifications.show({
+ // title: 'Channel streaming stopped',
+ // message: channels[channelsByUUID[uuid]].name,
+ // color: 'blue.5',
+ // });
+ // }
+ // }
- for (const clientId in oldClients) {
- if (newClients[clientId] === undefined) {
- notifications.show({
- title: 'Client stopped streaming',
- message: `Client stopped streaming from ${oldClients[clientId].ip_address}`,
- color: 'blue.5',
- });
- }
- }
- }
+ // for (const clientId in oldClients) {
+ // if (newClients[clientId] === undefined) {
+ // notifications.show({
+ // title: 'Client stopped streaming',
+ // message: `Client stopped streaming from ${oldClients[clientId].ip_address}`,
+ // color: 'blue.5',
+ // });
+ // }
+ // }
+ // }
- return {
- stats,
- activeChannels: newChannels,
- activeClients: newClients,
- };
- });
+ // return {
+ // stats,
+ // activeChannels: newChannels,
+ // activeClients: newClients,
+ // };
+ // });
},
fetchRecordings: async () => {