big push of UI updates

This commit is contained in:
dekzter 2025-03-07 16:29:39 -05:00
parent 36cc6da547
commit a1a25799dd
10 changed files with 399 additions and 81 deletions

View file

@ -112,6 +112,9 @@ REST_FRAMEWORK = {
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
SWAGGER_SETTINGS = {
@ -158,12 +161,6 @@ CSRF_TRUSTED_ORIGINS = [
]
APPEND_SLASH = True
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),

View file

@ -12,7 +12,7 @@ import Login from './pages/Login';
import Channels from './pages/Channels';
import M3U from './pages/M3U';
import { ThemeProvider } from '@mui/material/styles';
import { Box, CssBaseline } from '@mui/material';
import { Box, CssBaseline, GlobalStyles } from '@mui/material';
import theme from './theme';
import EPG from './pages/EPG';
import Guide from './pages/Guide';
@ -80,6 +80,14 @@ const App = () => {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<GlobalStyles
styles={{
'.Mui-TableHeadCell-Content': {
height: '100%',
alignItems: 'flex-end !important',
},
}}
/>
<WebsocketProvider>
<Router>
<Sidebar

View file

@ -1,7 +1,7 @@
// frontend/src/components/FloatingVideo.js
import React, { useEffect, useRef } from 'react';
import Draggable from 'react-draggable';
import useVideoStore from '../store/video';
import useVideoStore from '../store/useVideoStore';
import mpegts from 'mpegts.js';
export default function FloatingVideo() {

View file

@ -15,6 +15,8 @@ import {
Snackbar,
Popover,
TextField,
Autocomplete,
InputAdornment,
} from '@mui/material';
import useChannelsStore from '../../store/channels';
import {
@ -24,14 +26,15 @@ import {
SwapVert as SwapVertIcon,
LiveTv as LiveTvIcon,
ContentCopy,
Tv as TvIcon, // <-- ADD THIS IMPORT
Tv as TvIcon,
Clear as ClearIcon,
} from '@mui/icons-material';
import API from '../../api';
import ChannelForm from '../forms/Channel';
import { TableHelper } from '../../helpers';
import utils from '../../utils';
import logo from '../../images/logo.png';
import useVideoStore from '../../store/video';
import useVideoStore from '../../store/useVideoStore';
import useSettingsStore from '../../store/settings';
import useStreamsStore from '../../store/streams';
import usePlaylistsStore from '../../store/playlists';
@ -85,7 +88,6 @@ const ChannelStreams = ({ channel, isExpanded }) => {
[playlists]
),
enableKeyboardShortcuts: false,
enableColumnActions: false,
enableColumnFilters: false,
enableSorting: false,
enableBottomToolbar: false,
@ -150,23 +152,44 @@ const ChannelStreams = ({ channel, isExpanded }) => {
);
};
const ChannelsTable = ({ setSelectedChannels }) => {
const ChannelsTable = ({}) => {
const [channel, setChannel] = useState(null);
const [channelModalOpen, setChannelModalOpen] = useState(false);
const [rowSelection, setRowSelection] = useState([]);
const [channelGroupOptions, setChannelGroupOptions] = useState([]);
const [anchorEl, setAnchorEl] = useState(null);
const [textToCopy, setTextToCopy] = useState('');
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarOpen, setSnackbarOpen] = useState(false);
const { showVideo } = useVideoStore(); // or useVideoStore()
const [filterValues, setFilterValues] = useState({});
const { showVideo } = useVideoStore();
const {
channels,
isLoading: channelsLoading,
fetchChannels,
setChannelsPageSelection,
} = useChannelsStore();
useEffect(() => {
setChannelGroupOptions([
...new Set(
Object.values(channels).map((channel) => channel.channel_group?.name)
),
]);
}, [channels]);
const handleFilterChange = (columnId, value) => {
console.log(columnId);
console.log(value);
setFilterValues((prev) => ({
...prev,
[columnId]: value ? value.toLowerCase() : '',
}));
};
const outputUrlRef = useRef(null);
const {
@ -184,14 +207,81 @@ const ChannelsTable = ({ setSelectedChannels }) => {
{
header: 'Name',
accessorKey: 'channel_name',
muiTableHeadCellProps: {
sx: { textAlign: 'center' }, // Center-align the header
},
Header: ({ column }) => (
<TextField
variant="standard"
label="Name"
value={filterValues[column.id]}
onChange={(e) => handleFilterChange(column.id, e.target.value)}
size="small"
margin="none"
fullWidth
sx={
{
// '& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
// '& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
// width: '200px', // Optional: Adjust width
}
}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => handleFilterChange(column.id, '')} // Clear text on click
edge="end"
size="small"
>
<ClearIcon sx={{ fontSize: '1rem' }} />
</IconButton>
</InputAdornment>
),
},
}}
/>
),
meta: {
filterVariant: null,
},
},
{
header: 'Group',
accessorFn: (row) => row.channel_group?.name || '',
Header: ({ column }) => (
<Autocomplete
disablePortal
options={channelGroupOptions}
size="small"
sx={{ width: 300 }}
clearOnEscape
onChange={(event, newValue) =>
handleFilterChange(column.id, newValue)
}
renderInput={(params) => (
<TextField
{...params}
label="Group"
size="small"
variant="standard"
onClick={(e) => e.stopPropagation()}
sx={{
pb: 0.8,
// '& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
// '& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
// width: '200px', // Optional: Adjust width
}}
/>
)}
/>
),
},
{
header: 'Logo',
accessorKey: 'logo_url',
enableSorting: false,
size: 55,
Cell: ({ cell }) => (
<Grid2
@ -210,7 +300,7 @@ const ChannelsTable = ({ setSelectedChannels }) => {
},
},
],
[]
[channelGroupOptions]
);
// Access the row virtualizer instance (optional)
@ -370,22 +460,36 @@ const ChannelsTable = ({ setSelectedChannels }) => {
);
};
const onRowSelectionChange = (e, test) => {
console.log(e());
console.log(test);
setRowSelection(e);
};
useEffect(() => {
const selectedRows = table
.getSelectedRowModel()
.rows.map((row) => row.original);
setSelectedChannels(selectedRows);
setChannelsPageSelection(selectedRows);
}, [rowSelection]);
// Configure the MaterialReactTable
const filteredData = Object.values(channels).filter((row) =>
columns.every(({ accessorKey }) =>
filterValues[accessorKey]
? row[accessorKey]?.toLowerCase().includes(filterValues[accessorKey])
: true
)
);
const table = useMaterialReactTable({
...TableHelper.defaultProperties,
columns,
data: Object.values(channels),
data: filteredData,
enablePagination: false,
enableColumnActions: false,
enableRowVirtualization: true,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onRowSelectionChange: onRowSelectionChange,
onSortingChange: setSorting,
state: {
isLoading: isLoading || channelsLoading,
@ -400,18 +504,21 @@ const ChannelsTable = ({ setSelectedChannels }) => {
enableRowActions: true,
enableExpandAll: false,
displayColumnDefOptions: {
'mrt-row-select': {
size: 50, // Set custom width (default is ~40px)
},
'mrt-row-expand': {
size: 10, // Set custom width (default is ~40px)
header: '',
muiTableHeadCellProps: {
sx: { width: 30, minWidth: 30, maxWidth: 30 },
sx: { width: 38, minWidth: 38, maxWidth: 38, height: '100%' },
},
muiTableBodyCellProps: {
sx: { width: 30, minWidth: 30, maxWidth: 30 },
sx: { width: 38, minWidth: 38, maxWidth: 38 },
},
},
'mrt-row-actions': {
size: 50, // Set custom width (default is ~40px)
size: 68, // Set custom width (default is ~40px)
},
},
muiExpandButtonProps: ({ row, table }) => ({
@ -429,32 +536,40 @@ const ChannelsTable = ({ setSelectedChannels }) => {
),
renderRowActions: ({ row }) => (
<Box sx={{ justifyContent: 'right' }}>
<IconButton
size="small"
color="warning"
onClick={() => {
editChannel(row.original);
}}
sx={{ p: 0 }}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => deleteChannel(row.original.id)}
sx={{ p: 0 }}
>
<DeleteIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="info"
onClick={() => handleWatchStream(row.original.channel_number)}
sx={{ p: 0 }}
>
<LiveTvIcon fontSize="small" />
</IconButton>
<Tooltip title="Edit Channel">
<IconButton
size="small"
color="warning"
onClick={() => {
editChannel(row.original);
}}
sx={{ py: 0, px: 0.5 }}
>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Delete Channel">
<IconButton
size="small"
color="error"
onClick={() => deleteChannel(row.original.id)}
sx={{ py: 0, px: 0.5 }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Preview Channel">
<IconButton
size="small"
color="info"
onClick={() => handleWatchStream(row.original.channel_number)}
sx={{ py: 0, px: 0.5 }}
>
<LiveTvIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
),
muiTableContainerProps: {

View file

@ -10,6 +10,11 @@ import {
IconButton,
Tooltip,
Button,
Menu,
MenuItem,
TextField,
Autocomplete,
InputAdornment,
} from '@mui/material';
import useStreamsStore from '../../store/streams';
import API from '../../api';
@ -17,30 +22,158 @@ import {
Delete as DeleteIcon,
Edit as EditIcon,
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';
const StreamsTable = ({ selectedChannels }) => {
const StreamsTable = ({}) => {
const [rowSelection, setRowSelection] = useState([]);
const [stream, setStream] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
const [moreActionsAnchorEl, setMoreActionsAnchorEl] = useState(null);
const [filterValues, setFilterValues] = useState({});
const [groupOptions, setGroupOptions] = useState([]);
const [m3uOptions, setM3uOptions] = useState([]);
const [actionsOpenRow, setActionsOpenRow] = useState(null);
const { streams, isLoading: streamsLoading } = useStreamsStore();
const { playlists } = usePlaylistsStore();
const { channelsPageSelection } = useChannelsStore();
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]);
const columns = useMemo(
() => [
{ header: 'Name', accessorKey: 'name' },
{ header: 'Group', accessorKey: 'group_name' },
{
header: 'Name',
accessorKey: 'name',
muiTableHeadCellProps: {
sx: { textAlign: 'center' }, // Center-align the header
},
Header: ({ column }) => (
<TextField
variant="standard"
label="Name"
value={filterValues[column.id]}
onClick={(e) => e.stopPropagation()}
onChange={(e) => handleFilterChange(column.id, e.target.value)}
size="small"
margin="none"
fullWidth
sx={
{
// '& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
// '& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
// 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>
),
},
}}
/>
),
meta: {
filterVariant: null,
},
},
{
header: 'Group',
accessorKey: 'group_name',
Header: ({ column }) => (
<Autocomplete
disablePortal
options={groupOptions}
size="small"
sx={{ width: 300 }}
clearOnEscape
onChange={(event, newValue) =>
handleFilterChange(column.id, newValue)
}
renderInput={(params) => (
<TextField
{...params}
label="Group"
size="small"
variant="standard"
onClick={(e) => e.stopPropagation()}
sx={{
pb: 0.8,
'& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
'& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
width: '200px', // Optional: Adjust width
}}
/>
)}
/>
),
},
{
header: 'M3U',
size: 100,
accessorFn: (row) =>
playlists.find((playlist) => playlist.id === row.m3u_account)?.name,
Header: ({ column }) => (
<Autocomplete
disablePortal
options={m3uOptions}
size="small"
sx={{ width: 300 }}
clearOnEscape
onChange={(event, newValue) =>
handleFilterChange(column.id, newValue)
}
renderInput={(params) => (
<TextField
{...params}
label="M3U"
size="small"
variant="standard"
onClick={(e) => e.stopPropagation()}
sx={{
pb: 0.8,
'& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
'& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
width: '200px', // Optional: Adjust width
}}
/>
)}
/>
),
},
],
[playlists]
[playlists, groupOptions, m3uOptions]
);
const rowVirtualizerInstanceRef = useRef(null);
@ -110,8 +243,8 @@ const StreamsTable = ({ selectedChannels }) => {
}
}, [sorting]);
const addStreamsToChannel = async (stream) => {
const channel = selectedChannels[0];
const addStreamsToChannel = async () => {
const channel = channelsPageSelection[0];
const selectedRows = table.getSelectedRowModel().rows;
await API.updateChannel({
...channel,
@ -123,10 +256,36 @@ const StreamsTable = ({ selectedChannels }) => {
});
};
const addStreamToChannel = async (streamId) => {
const channel = channelsPageSelection[0];
await API.updateChannel({
...channel,
streams: [...new Set(channel.stream_ids.concat([streamId]))],
});
};
const handleMoreActionsClick = (event, rowId) => {
setMoreActionsAnchorEl(event.currentTarget);
setActionsOpenRow(rowId);
};
const handleMoreActionsClose = () => {
setMoreActionsAnchorEl(null);
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: streams,
data: filteredData,
enablePagination: false,
enableRowVirtualization: true,
enableRowSelection: true,
@ -140,33 +299,57 @@ const StreamsTable = ({ selectedChannels }) => {
rowVirtualizerInstanceRef,
rowVirtualizerOptions: { overscan: 5 },
enableRowActions: true,
positionActionsColumn: 'first',
renderRowActions: ({ row }) => (
<>
<Tooltip title="Add to Channel">
<IconButton
size="small"
color="info"
onClick={() => addStreamToChannel(row.original.id)}
sx={{ py: 0, px: 0.5 }}
disabled={
channelsPageSelection.length != 1 ||
channelsPageSelection[0]?.stream_ids.includes(row.original.id)
}
>
<PlaylistAddIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Create New Channel">
<IconButton
size="small"
color="success"
onClick={() => createChannelFromStream(row.original)}
sx={{ py: 0, px: 0.5 }}
>
<AddIcon fontSize="small" />
</IconButton>
</Tooltip>
<IconButton
onClick={(event) => handleMoreActionsClick(event, row.original.id)}
size="small"
color="warning"
onClick={() => editStream(row.original)}
disabled={row.original.m3u_account ? true : false}
sx={{ p: 0 }}
sx={{ py: 0, px: 0.5 }}
>
<EditIcon fontSize="small" />
<MoreVertIcon />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => deleteStream(row.original.id)}
sx={{ p: 0 }}
<Menu
anchorEl={moreActionsAnchorEl}
open={isMoreActionsOpen && actionsOpenRow == row.original.id}
onClose={handleMoreActionsClose}
>
<DeleteIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="success"
onClick={() => createChannelFromStream(row.original)}
sx={{ p: 0 }}
>
<AddIcon fontSize="small" />
</IconButton>
<MenuItem
onClick={() => editStream(row.original.id)}
disabled={row.original.m3u_account ? true : false}
>
Edit
</MenuItem>
<MenuItem onClick={() => deleteStream(row.original.id)}>
Delete Stream
</MenuItem>
</Menu>
</>
),
muiTableContainerProps: {
@ -175,6 +358,14 @@ const StreamsTable = ({ selectedChannels }) => {
overflowY: 'auto',
},
},
displayColumnDefOptions: {
'mrt-row-actions': {
size: 68,
},
'mrt-row-select': {
size: 50,
},
},
renderTopToolbarCustomActions: ({ table }) => {
const selectedRowCount = table.getSelectedRowModel().rows.length;
@ -216,7 +407,9 @@ const StreamsTable = ({ selectedChannels }) => {
onClick={addStreamsToChannel}
size="small"
sx={{ marginLeft: 1 }}
disabled={selectedChannels.length != 1 || selectedRowCount == 0}
disabled={
channelsPageSelection.length != 1 || selectedRowCount == 0
}
>
Add to Channel
</Button>

View file

@ -5,9 +5,12 @@ export default {
enableDensityToggle: false,
enableFullScreenToggle: false,
positionToolbarAlertBanner: 'none',
columnFilterDisplayMode: 'popover',
// columnFilterDisplayMode: 'popover',
enableRowNumbers: false,
positionActionsColumn: 'last',
enableColumnActions: false,
enableColumnFilters: false,
enableGlobalFilter: false,
initialState: {
density: 'compact',
},

View file

@ -4,8 +4,6 @@ import StreamsTable from '../components/tables/StreamsTable';
import { Grid2, Box } from '@mui/material';
const ChannelsPage = () => {
const [selectedChannels, setSelectedChannels] = useState([]);
return (
<Grid2 container>
<Grid2 size={6}>
@ -20,7 +18,7 @@ const ChannelsPage = () => {
overflow: 'hidden', // Prevent parent scrolling
}}
>
<ChannelsTable setSelectedChannels={setSelectedChannels} />
<ChannelsTable />
</Box>
</Grid2>
<Grid2 size={6}>
@ -35,7 +33,7 @@ const ChannelsPage = () => {
overflow: 'hidden', // Prevent parent scrolling
}}
>
<StreamsTable selectedChannels={selectedChannels} />
<StreamsTable />
</Box>
</Grid2>
</Grid2>

View file

@ -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/video'; // NEW import
import useVideoStore from '../store/useVideoStore'; // NEW import
import useAlertStore from '../store/alerts';
import useSettingsStore from '../store/settings';

View file

@ -4,6 +4,7 @@ import api from '../api';
const useChannelsStore = create((set) => ({
channels: [],
channelGroups: [],
channelsPageSelection: [],
isLoading: false,
error: null,
@ -80,6 +81,9 @@ const useChannelsStore = create((set) => ({
group.id === channelGroup.id ? channelGroup : group
),
})),
setChannelsPageSelection: (channelsPageSelection) =>
set((state) => ({ channelsPageSelection })),
}));
export default useChannelsStore;