mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
refactore channels store, added new UX for adding streams to a channel
This commit is contained in:
parent
1dcbf8875f
commit
c7347c568a
14 changed files with 360 additions and 132 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ node_modules/
|
|||
.history/
|
||||
staticfiles/
|
||||
static/
|
||||
data/
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ server {
|
|||
}
|
||||
|
||||
# Serve FFmpeg streams efficiently
|
||||
location /stream/ {
|
||||
location /output/stream/ {
|
||||
proxy_pass http://127.0.0.1:5656;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Connection keep-alive;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const WebsocketProvider = ({ children }) => {
|
|||
const [val, setVal] = useState(null);
|
||||
|
||||
const { showAlert } = useAlertStore();
|
||||
const { fetchStreams } = useStreamsStore();
|
||||
|
||||
const ws = useRef(null);
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ export const WebsocketProvider = ({ children }) => {
|
|||
switch (event.type) {
|
||||
case 'm3u_refresh':
|
||||
if (event.message?.success) {
|
||||
useStreamsStore.getState().fetchStreams();
|
||||
fetchStreams();
|
||||
showAlert(event.message.message, 'success');
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const AlertPopup = () => {
|
|||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
autoHideDuration={5000}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// frontend/src/components/FloatingVideo.js
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Draggable from 'react-draggable';
|
||||
import useVideoStore from '../store/useVideoStore';
|
||||
import useVideoStore from '../store/video';
|
||||
import mpegts from 'mpegts.js';
|
||||
|
||||
export default function FloatingVideo() {
|
||||
|
|
@ -63,7 +63,13 @@ export default function FloatingVideo() {
|
|||
}}
|
||||
>
|
||||
{/* Simple header row with a close button */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '4px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '4px',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={hideVideo}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -118,7 +118,9 @@ const Sidebar = ({ open, miniDrawerWidth, drawerWidth, toggleDrawer }) => {
|
|||
</ListItem>
|
||||
</List>
|
||||
{open && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}
|
||||
>
|
||||
{/* Public IP + optional flag */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TextField
|
||||
|
|
|
|||
|
|
@ -31,10 +31,126 @@ import ChannelForm from '../forms/Channel';
|
|||
import { TableHelper } from '../../helpers';
|
||||
import utils from '../../utils';
|
||||
import logo from '../../images/logo.png';
|
||||
import useVideoStore from '../../store/useVideoStore';
|
||||
import useVideoStore from '../../store/video';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
|
||||
const ChannelsTable = () => {
|
||||
const ChannelStreams = ({ channel, isExpanded }) => {
|
||||
const [channelStreams, setChannelStreams] = useState([]);
|
||||
const channelStreamIds = useChannelsStore(
|
||||
(state) => state.channels[channel.id]?.stream_ids
|
||||
);
|
||||
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);
|
||||
streamSet.delete(stream);
|
||||
streamSet = Array.from(streamSet);
|
||||
await API.updateChannel({
|
||||
...channel,
|
||||
streams: streamSet.map((stream) => stream.id),
|
||||
});
|
||||
};
|
||||
|
||||
const channelStreamsTable = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
data: channelStreams,
|
||||
columns: useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'M3U',
|
||||
accessorFn: (row) =>
|
||||
playlists.find((playlist) => playlist.id === row.m3u_account)?.name,
|
||||
},
|
||||
],
|
||||
[playlists]
|
||||
),
|
||||
enableKeyboardShortcuts: false,
|
||||
enableColumnActions: false,
|
||||
enableColumnFilters: false,
|
||||
enableSorting: false,
|
||||
enableBottomToolbar: false,
|
||||
enableTopToolbar: false,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableColumnHeaders: false,
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
},
|
||||
enableRowActions: true,
|
||||
enableRowOrdering: true,
|
||||
muiRowDragHandleProps: ({ table }) => ({
|
||||
onDragEnd: async () => {
|
||||
const { draggingRow, hoveredRow } = table.getState();
|
||||
|
||||
if (hoveredRow && draggingRow) {
|
||||
channelStreams.splice(
|
||||
hoveredRow.index,
|
||||
0,
|
||||
channelStreams.splice(draggingRow.index, 1)[0]
|
||||
);
|
||||
|
||||
// setChannelStreams([...channelStreams]);
|
||||
API.updateChannel({
|
||||
...channel,
|
||||
streams: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<IconButton
|
||||
size="small" // Makes the button smaller
|
||||
color="error" // Red color for delete actions
|
||||
onClick={() => removeStream(row.original)}
|
||||
>
|
||||
<DeleteIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
if (!isExpanded) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
pt: 1,
|
||||
pb: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<MaterialReactTable table={channelStreamsTable} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ChannelsTable = ({ setSelectedChannels }) => {
|
||||
const [channel, setChannel] = useState(null);
|
||||
const [channelModalOpen, setChannelModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
|
|
@ -43,9 +159,16 @@ const ChannelsTable = () => {
|
|||
const [textToCopy, setTextToCopy] = useState('');
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const { showVideo } = useVideoStore.getState(); // or useVideoStore()
|
||||
|
||||
const { channels, isLoading: channelsLoading } = useChannelsStore();
|
||||
const { showVideo } = useVideoStore(); // or useVideoStore()
|
||||
const {
|
||||
channels,
|
||||
isLoading: channelsLoading,
|
||||
fetchChannels,
|
||||
} = useChannelsStore();
|
||||
|
||||
const outputUrlRef = useRef(null);
|
||||
|
||||
const {
|
||||
environment: { env_mode },
|
||||
} = useSettingsStore();
|
||||
|
|
@ -104,9 +227,7 @@ const ChannelsTable = () => {
|
|||
};
|
||||
|
||||
const deleteChannel = async (id) => {
|
||||
setIsLoading(true);
|
||||
await API.deleteChannel(id);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
function handleWatchStream(channelNumber) {
|
||||
|
|
@ -150,7 +271,7 @@ const ChannelsTable = () => {
|
|||
setSnackbarOpen(true);
|
||||
|
||||
// Refresh the channel list
|
||||
await useChannelsStore.getState().fetchChannels();
|
||||
await fetchChannels();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSnackbarMessage('Failed to assign channels');
|
||||
|
|
@ -215,7 +336,16 @@ const ChannelsTable = () => {
|
|||
await navigator.clipboard.writeText(textToCopy);
|
||||
setSnackbarMessage('Copied!');
|
||||
} catch (err) {
|
||||
setSnackbarMessage('Failed to copy');
|
||||
const inputElement = outputUrlRef.current.querySelector('input'); // Get the actual input
|
||||
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
inputElement.select();
|
||||
|
||||
// For older browsers
|
||||
document.execCommand('copy');
|
||||
setSnackbarMessage('Copied!');
|
||||
}
|
||||
}
|
||||
setSnackbarOpen(true);
|
||||
};
|
||||
|
|
@ -240,11 +370,18 @@ const ChannelsTable = () => {
|
|||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const selectedRows = table
|
||||
.getSelectedRowModel()
|
||||
.rows.map((row) => row.original);
|
||||
setSelectedChannels(selectedRows);
|
||||
}, [rowSelection]);
|
||||
|
||||
// Configure the MaterialReactTable
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
data: channels,
|
||||
data: Object.values(channels),
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
|
|
@ -261,6 +398,34 @@ const ChannelsTable = () => {
|
|||
density: 'compact',
|
||||
},
|
||||
enableRowActions: true,
|
||||
enableExpandAll: false,
|
||||
displayColumnDefOptions: {
|
||||
'mrt-row-expand': {
|
||||
size: 10, // Set custom width (default is ~40px)
|
||||
header: '',
|
||||
muiTableHeadCellProps: {
|
||||
sx: { width: 30, minWidth: 30, maxWidth: 30 },
|
||||
},
|
||||
muiTableBodyCellProps: {
|
||||
sx: { width: 30, minWidth: 30, maxWidth: 30 },
|
||||
},
|
||||
},
|
||||
'mrt-row-actions': {
|
||||
size: 50, // Set custom width (default is ~40px)
|
||||
},
|
||||
},
|
||||
muiExpandButtonProps: ({ row, table }) => ({
|
||||
onClick: () => {
|
||||
table.setExpanded({ [row.id]: !row.getIsExpanded() }); //only 1 detail panel open at a time
|
||||
},
|
||||
sx: {
|
||||
transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)',
|
||||
transition: 'transform 0.2s',
|
||||
},
|
||||
}),
|
||||
renderDetailPanel: ({ row }) => (
|
||||
<ChannelStreams channel={row.original} isExpanded={row.getIsExpanded()} />
|
||||
),
|
||||
renderRowActions: ({ row }) => (
|
||||
<Box sx={{ justifyContent: 'right' }}>
|
||||
<IconButton
|
||||
|
|
@ -300,65 +465,71 @@ const ChannelsTable = () => {
|
|||
muiSearchTextFieldProps: {
|
||||
variant: 'standard',
|
||||
},
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack direction="row" sx={{ alignItems: 'center' }}>
|
||||
<Typography>Channels</Typography>
|
||||
<Tooltip title="Add New Channel">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={() => editChannel()}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Channels">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={deleteChannels}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Assign Channels">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="contained"
|
||||
onClick={assignChannels}
|
||||
>
|
||||
<SwapVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
renderTopToolbarCustomActions: ({ table }) => {
|
||||
const selectedRowCount = table.getSelectedRowModel().rows.length;
|
||||
|
||||
{/* Our brand-new button for EPG matching */}
|
||||
<Tooltip title="Auto-match EPG with fuzzy logic">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={matchEpg}
|
||||
>
|
||||
<TvIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
return (
|
||||
<Stack direction="row" sx={{ alignItems: 'center' }}>
|
||||
<Typography>Channels</Typography>
|
||||
<Tooltip title="Add New Channel">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={() => editChannel()}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Channels">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={deleteChannels}
|
||||
disabled={selectedRowCount == 0}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Assign Channels">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="contained"
|
||||
onClick={assignChannels}
|
||||
disabled={selectedRowCount == 0}
|
||||
>
|
||||
<SwapVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<ButtonGroup sx={{ marginLeft: 1 }}>
|
||||
<Button variant="contained" size="small" onClick={copyHDHRUrl}>
|
||||
HDHR URL
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={copyM3UUrl}>
|
||||
M3U URL
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={copyEPGUrl}>
|
||||
EPG
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
),
|
||||
{/* Our brand-new button for EPG matching */}
|
||||
<Tooltip title="Auto-match EPG with fuzzy logic">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={matchEpg}
|
||||
>
|
||||
<TvIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<ButtonGroup sx={{ marginLeft: 1 }}>
|
||||
<Button variant="contained" size="small" onClick={copyHDHRUrl}>
|
||||
HDHR URL
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={copyM3UUrl}>
|
||||
M3U URL
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={copyEPGUrl}>
|
||||
EPG
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -384,11 +555,13 @@ const ChannelsTable = () => {
|
|||
>
|
||||
<div style={{ padding: 16, display: 'flex', alignItems: 'center' }}>
|
||||
<TextField
|
||||
id="output-url"
|
||||
value={textToCopy}
|
||||
variant="standard"
|
||||
disabled
|
||||
// disabled
|
||||
size="small"
|
||||
sx={{ marginRight: 1 }}
|
||||
ref={outputUrlRef}
|
||||
/>
|
||||
<IconButton onClick={handleCopy} color="primary">
|
||||
<ContentCopy />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ 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);
|
||||
|
|
@ -33,6 +34,7 @@ const Example = () => {
|
|||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
|
||||
const playlists = usePlaylistsStore((state) => state.playlists);
|
||||
const { fetchStreams } = useStreamsStore();
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
|
|
@ -114,6 +116,7 @@ const Example = () => {
|
|||
|
||||
const deletePlaylist = async (id) => {
|
||||
await API.deletePlaylist(id);
|
||||
fetchStreams();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
|
|
@ -149,7 +152,7 @@ const Example = () => {
|
|||
data: playlists,
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
// enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
|
|
@ -197,6 +200,8 @@ const Example = () => {
|
|||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: 'calc(43vh - 0px)',
|
||||
pr: 1,
|
||||
pl: 1,
|
||||
},
|
||||
},
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
|
|
|
|||
|
|
@ -12,11 +12,7 @@ import {
|
|||
Button,
|
||||
} from '@mui/material';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import useChannelsStore from '../../store/channels'; // NEW: Import channels store
|
||||
import API from '../../api';
|
||||
// Make sure your api.js exports getAuthToken as a named export:
|
||||
// e.g. export const getAuthToken = async () => { ... }
|
||||
import { getAuthToken } from '../../api';
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
|
|
@ -26,7 +22,7 @@ import { TableHelper } from '../../helpers';
|
|||
import StreamForm from '../forms/Stream';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
|
||||
const StreamsTable = () => {
|
||||
const StreamsTable = ({ selectedChannels }) => {
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [stream, setStream] = useState(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
|
@ -53,13 +49,11 @@ const StreamsTable = () => {
|
|||
|
||||
// Fallback: Individual creation (optional)
|
||||
const createChannelFromStream = async (stream) => {
|
||||
setIsLoading(true);
|
||||
await API.createChannelFromStream({
|
||||
channel_name: stream.name,
|
||||
channel_number: null,
|
||||
stream_id: stream.id,
|
||||
});
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Bulk creation: create channels from selected streams in one API call
|
||||
|
|
@ -85,9 +79,7 @@ const StreamsTable = () => {
|
|||
};
|
||||
|
||||
const deleteStream = async (id) => {
|
||||
setIsLoading(true);
|
||||
await API.deleteStream(id);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const deleteStreams = async () => {
|
||||
|
|
@ -118,6 +110,19 @@ const StreamsTable = () => {
|
|||
}
|
||||
}, [sorting]);
|
||||
|
||||
const addStreamsToChannel = async (stream) => {
|
||||
const channel = selectedChannels[0];
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
await API.updateChannel({
|
||||
...channel,
|
||||
streams: [
|
||||
...new Set(
|
||||
channel.stream_ids.concat(selectedRows.map((row) => row.original.id))
|
||||
),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
|
|
@ -170,39 +175,54 @@ const StreamsTable = () => {
|
|||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack direction="row" sx={{ alignItems: 'center' }}>
|
||||
<Typography>Streams</Typography>
|
||||
<Tooltip title="Add New Stream">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
renderTopToolbarCustomActions: ({ table }) => {
|
||||
const selectedRowCount = table.getSelectedRowModel().rows.length;
|
||||
|
||||
return (
|
||||
<Stack direction="row" sx={{ alignItems: 'center' }}>
|
||||
<Typography>Streams</Typography>
|
||||
<Tooltip title="Add New Stream">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={() => editStream()}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Streams">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={deleteStreams}
|
||||
disabled={selectedRowCount == 0}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => editStream()}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Streams">
|
||||
<IconButton
|
||||
onClick={createChannelsFromStreams}
|
||||
size="small"
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={deleteStreams}
|
||||
sx={{ marginLeft: 1 }}
|
||||
disabled={selectedRowCount == 0}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={createChannelsFromStreams}
|
||||
size="small"
|
||||
sx={{ marginLeft: 1 }}
|
||||
>
|
||||
Create Channels
|
||||
</Button>
|
||||
</Stack>
|
||||
),
|
||||
Create Channels
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={addStreamsToChannel}
|
||||
size="small"
|
||||
sx={{ marginLeft: 1 }}
|
||||
disabled={selectedChannels.length != 1 || selectedRowCount == 0}
|
||||
>
|
||||
Add to Channel
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ export default {
|
|||
},
|
||||
muiTableBodyCellProps: {
|
||||
sx: {
|
||||
padding: 0,
|
||||
pt: 0,
|
||||
pb: 0,
|
||||
},
|
||||
},
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
padding: 0,
|
||||
pt: 0,
|
||||
pb: 0,
|
||||
},
|
||||
},
|
||||
muiTableBodyProps: {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import ChannelsTable from '../components/tables/ChannelsTable';
|
||||
import StreamsTable from '../components/tables/StreamsTable';
|
||||
import { Grid2, Box } from '@mui/material';
|
||||
|
||||
const ChannelsPage = () => {
|
||||
const [selectedChannels, setSelectedChannels] = useState([]);
|
||||
|
||||
return (
|
||||
<Grid2 container>
|
||||
<Grid2 size={6}>
|
||||
|
|
@ -18,7 +20,7 @@ const ChannelsPage = () => {
|
|||
overflow: 'hidden', // Prevent parent scrolling
|
||||
}}
|
||||
>
|
||||
<ChannelsTable />
|
||||
<ChannelsTable setSelectedChannels={setSelectedChannels} />
|
||||
</Box>
|
||||
</Grid2>
|
||||
<Grid2 size={6}>
|
||||
|
|
@ -33,7 +35,7 @@ const ChannelsPage = () => {
|
|||
overflow: 'hidden', // Prevent parent scrolling
|
||||
}}
|
||||
>
|
||||
<StreamsTable />
|
||||
<StreamsTable selectedChannels={selectedChannels} />
|
||||
</Box>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import dayjs from 'dayjs';
|
|||
import API from '../api';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import logo from '../images/logo.png';
|
||||
import useVideoStore from '../store/useVideoStore'; // NEW import
|
||||
import useVideoStore from '../store/video'; // NEW import
|
||||
import useAlertStore from '../store/alerts';
|
||||
import useSettingsStore from '../store/settings';
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
}
|
||||
|
||||
// The “Watch Now” click => show floating video
|
||||
const { showVideo } = useVideoStore.getState(); // or useVideoStore()
|
||||
const { showVideo } = useVideoStore(); // or useVideoStore()
|
||||
function handleWatchStream(program) {
|
||||
const matched = findChannelByTvgId(program.tvg_id);
|
||||
if (!matched) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ const useChannelsStore = create((set) => ({
|
|||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const channels = await api.getChannels();
|
||||
set({ channels: channels, isLoading: false });
|
||||
set({
|
||||
channels: channels.reduce((acc, channel) => {
|
||||
acc[channel.id] = channel;
|
||||
return acc;
|
||||
}, {}),
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch channels:', error);
|
||||
set({ error: 'Failed to load channels.', isLoading: false });
|
||||
|
|
@ -31,27 +37,37 @@ const useChannelsStore = create((set) => ({
|
|||
|
||||
addChannel: (newChannel) =>
|
||||
set((state) => ({
|
||||
channels: [...state.channels, newChannel],
|
||||
channels: {
|
||||
...state.channels,
|
||||
[newChannel.id]: newChannel,
|
||||
},
|
||||
})),
|
||||
|
||||
addChannels: (newChannels) =>
|
||||
set((state) => ({
|
||||
channels: state.channels.concat(newChannels),
|
||||
channels: {
|
||||
...state.channels,
|
||||
...newChannels,
|
||||
},
|
||||
})),
|
||||
|
||||
updateChannel: (userAgent) =>
|
||||
updateChannel: (channel) =>
|
||||
set((state) => ({
|
||||
channels: state.channels.map((chan) =>
|
||||
chan.id === userAgent.id ? userAgent : chan
|
||||
),
|
||||
channels: {
|
||||
...state.channels,
|
||||
[channel.id]: channel,
|
||||
},
|
||||
})),
|
||||
|
||||
removeChannels: (channelIds) =>
|
||||
set((state) => ({
|
||||
channels: state.channels.filter(
|
||||
(channel) => !channelIds.includes(channel.id)
|
||||
),
|
||||
})),
|
||||
set((state) => {
|
||||
const updatedChannels = { ...state.channels };
|
||||
for (const id of channelIds) {
|
||||
delete updatedChannels[id];
|
||||
}
|
||||
|
||||
return { channels: updatedChannels };
|
||||
}),
|
||||
|
||||
addChannelGroup: (newChannelGroup) =>
|
||||
set((state) => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue