diff --git a/frontend/src/App.js b/frontend/src/App.js
index 334246ec..5c758891 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -98,14 +98,14 @@ const App = () => {
toggleDrawer={toggleDrawer}
/>
- {/* Main content area, no AppBar, so no marginTop */}
+ {/* Main content area */}
, path: '/channels' },
{ label: 'M3U', icon: , path: '/m3u' },
{ label: 'EPG', icon: , path: '/epg' },
- {
- label: 'Stream Profiles',
- icon: ,
- path: '/stream-profiles',
- },
+ { label: 'Stream Profiles', icon: , path: '/stream-profiles' },
{ label: 'TV Guide', icon: , path: '/guide' },
{ label: 'Settings', icon: , path: '/settings' },
];
const Sidebar = ({ open, drawerWidth, miniDrawerWidth, toggleDrawer }) => {
const location = useLocation();
+ const theme = useTheme();
return (
{
width: open ? drawerWidth : miniDrawerWidth,
overflowX: 'hidden',
transition: 'width 0.3s',
- backgroundColor: '#18181b',
+ backgroundColor: theme.palette.background.default,
color: 'text.primary',
display: 'flex',
flexDirection: 'column',
@@ -118,16 +116,18 @@ const Sidebar = ({ open, drawerWidth, miniDrawerWidth, toggleDrawer }) => {
borderRadius: 1,
width: open ? '208px' : 'auto',
transition: 'all 0.2s ease',
- bgcolor: isActive ? 'rgba(21, 69, 62, 0.67)' : 'transparent',
+ bgcolor: isActive
+ ? theme.custom.sidebar.activeBackground
+ : 'transparent',
border: isActive
- ? '1px solid #14917e'
+ ? `1px solid ${theme.custom.sidebar.activeBorder}`
: '1px solid transparent',
color: 'text.primary',
px: 1,
py: 0.25,
'&:hover': {
- bgcolor: '#27272a',
- border: '1px solid #3f3f46',
+ bgcolor: theme.custom.sidebar.hoverBackground,
+ border: `1px solid ${theme.custom.sidebar.hoverBorder}`,
},
}}
>
@@ -148,9 +148,10 @@ const Sidebar = ({ open, drawerWidth, miniDrawerWidth, toggleDrawer }) => {
sx: {
fontSize: '14px',
fontWeight: 400,
- color: isActive ? '##d4d4d8' : '##d4d4d8',
- fontFamily: 'Inter, sans-serif',
+ fontFamily: theme.custom.sidebar.fontFamily,
letterSpacing: '-0.3px',
+ // Keeping the text color as it is in your original
+ color: isActive ? '#d4d4d8' : '#d4d4d8',
},
}}
/>
diff --git a/frontend/src/components/tables/ChannelsTable.js b/frontend/src/components/tables/ChannelsTable.js
index e65c7514..235177ca 100644
--- a/frontend/src/components/tables/ChannelsTable.js
+++ b/frontend/src/components/tables/ChannelsTable.js
@@ -17,6 +17,7 @@ import {
TextField,
Autocomplete,
InputAdornment,
+ Paper,
} from '@mui/material';
import useChannelsStore from '../../store/channels';
import {
@@ -28,6 +29,11 @@ import {
ContentCopy,
Tv as TvIcon,
Clear as ClearIcon,
+ IndeterminateCheckBox,
+ CompareArrows,
+ Code,
+ AddBox,
+ Hd as HdIcon,
} from '@mui/icons-material';
import API from '../../api';
import ChannelForm from '../forms/Channel';
@@ -37,6 +43,9 @@ import logo from '../../images/logo.png';
import useVideoStore from '../../store/useVideoStore';
import useSettingsStore from '../../store/settings';
import usePlaylistsStore from '../../store/playlists';
+import { Tv2, ScreenShare, Scroll } from 'lucide-react';
+import { styled, useTheme } from '@mui/material/styles';
+import ghostImage from '../../images/ghost.svg';
const ChannelStreams = ({ channel, isExpanded }) => {
const channelStreams = useChannelsStore(
@@ -45,12 +54,10 @@ const ChannelStreams = ({ channel, isExpanded }) => {
const { playlists } = usePlaylistsStore();
const removeStream = async (stream) => {
- let streamSet = new Set(channelStreams);
- streamSet.delete(stream);
- streamSet = Array.from(streamSet);
+ const newStreamList = channelStreams.filter((s) => s.id !== stream.id);
await API.updateChannel({
...channel,
- stream_ids: streamSet.map((stream) => stream.id),
+ stream_ids: newStreamList.map((s) => s.id),
});
};
@@ -109,11 +116,11 @@ const ChannelStreams = ({ channel, isExpanded }) => {
renderRowActions: ({ row }) => (
<>
removeStream(row.original)}
>
- {/* Small icon size */}
+
>
),
@@ -137,6 +144,63 @@ const ChannelStreams = ({ channel, isExpanded }) => {
);
};
+/* -----------------------------------------------------------
+ 2) Custom-styled "chip" buttons for HDHR, M3U, EPG
+------------------------------------------------------------ */
+const HDHRButton = styled(Button)(() => ({
+ border: '1px solid #a3d977',
+ color: '#a3d977',
+ backgroundColor: 'transparent',
+ textTransform: 'none',
+ fontSize: '0.85rem',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ padding: '2px 8px',
+ minWidth: 'auto',
+ '&:hover': {
+ borderColor: '#c2e583',
+ color: '#c2e583',
+ backgroundColor: 'rgba(163,217,119,0.1)',
+ },
+}));
+
+const M3UButton = styled(Button)(() => ({
+ border: '1px solid #5f6dc6',
+ color: '#5f6dc6',
+ backgroundColor: 'transparent',
+ textTransform: 'none',
+ fontSize: '0.85rem',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ padding: '2px 8px',
+ minWidth: 'auto',
+ '&:hover': {
+ borderColor: '#7f8de6',
+ color: '#7f8de6',
+ backgroundColor: 'rgba(95,109,198,0.1)',
+ },
+}));
+
+const EPGButton = styled(Button)(() => ({
+ border: '1px solid #707070',
+ color: '#a0a0a0',
+ backgroundColor: 'transparent',
+ textTransform: 'none',
+ fontSize: '0.85rem',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ padding: '2px 8px',
+ minWidth: 'auto',
+ '&:hover': {
+ borderColor: '#a0a0a0',
+ color: '#c0c0c0',
+ backgroundColor: 'rgba(112,112,112,0.1)',
+ },
+}));
+
const ChannelsTable = ({}) => {
const [channel, setChannel] = useState(null);
const [channelModalOpen, setChannelModalOpen] = useState(false);
@@ -150,6 +214,8 @@ const ChannelsTable = ({}) => {
const [filterValues, setFilterValues] = useState({});
+ const theme = useTheme();
+
const { showVideo } = useVideoStore();
const {
channels,
@@ -193,7 +259,7 @@ const ChannelsTable = ({}) => {
header: 'Name',
accessorKey: 'channel_name',
muiTableHeadCellProps: {
- sx: { textAlign: 'center' }, // Center-align the header
+ sx: { textAlign: 'center' },
},
Header: ({ column }) => (
{
fullWidth
sx={
{
- // '& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
- // '& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
+ // '& .MuiInputBase-root': { fontSize: '0.875rem' },
+ // '& .MuiInputLabel-root': { fontSize: '0.75rem' },
// width: '200px', // Optional: Adjust width
}
}
@@ -216,7 +282,7 @@ const ChannelsTable = ({}) => {
endAdornment: (
handleFilterChange(column.id, '')} // Clear text on click
+ onClick={() => handleFilterChange(column.id, '')}
edge="end"
size="small"
>
@@ -254,8 +320,8 @@ const ChannelsTable = ({}) => {
onClick={(e) => e.stopPropagation()}
sx={{
pb: 0.8,
- // '& .MuiInputBase-root': { fontSize: '0.875rem' }, // Text size
- // '& .MuiInputLabel-root': { fontSize: '0.75rem' }, // Label size
+ // '& .MuiInputBase-root': { fontSize: '0.875rem' },
+ // '& .MuiInputLabel-root': { fontSize: '0.75rem' },
// width: '200px', // Optional: Adjust width
}}
/>
@@ -475,7 +541,7 @@ const ChannelsTable = ({}) => {
sorting,
rowSelection,
},
- rowVirtualizerInstanceRef, // optional
+ rowVirtualizerInstanceRef,
rowVirtualizerOptions: { overscan: 5 },
initialState: {
density: 'compact',
@@ -484,10 +550,10 @@ const ChannelsTable = ({}) => {
enableExpandAll: false,
displayColumnDefOptions: {
'mrt-row-select': {
- size: 50, // Set custom width (default is ~40px)
+ size: 50,
},
'mrt-row-expand': {
- size: 10, // Set custom width (default is ~40px)
+ size: 10,
header: '',
muiTableHeadCellProps: {
sx: { width: 38, minWidth: 38, maxWidth: 38, height: '100%' },
@@ -497,13 +563,13 @@ const ChannelsTable = ({}) => {
},
},
'mrt-row-actions': {
- size: 68, // Set custom width (default is ~40px)
+ size: 68,
},
},
muiExpandButtonProps: ({ row, table }) => ({
onClick: () => {
setRowSelection({ [row.index]: true });
- table.setExpanded({ [row.id]: !row.getIsExpanded() }); //only 1 detail panel open at a time
+ table.setExpanded({ [row.id]: !row.getIsExpanded() });
},
sx: {
transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)',
@@ -629,42 +695,445 @@ const ChannelsTable = ({}) => {
return (
-
+ {/* Header Row: outside the Paper */}
+
+
+ Channels
+
+
+
+ Links:
+
+
+
+ {/* HDHR Button */}
+
+
+ {/* M3U Button */}
+
+
+ {/* EPG Button */}
+
+
+
+
+ {/* Paper container: contains top toolbar and table (or ghost state) */}
+
+ {/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
+
+
+
+
+ }
+ sx={{
+ borderColor: theme.palette.custom.borderDefault,
+ borderRadius: '4px',
+ borderWidth: '1px',
+ borderStyle: 'solid',
+ height: '25px',
+ opacity: 0.4,
+ color: theme.palette.text.secondary,
+ fontSize: '0.85rem',
+ px: 1,
+ py: 0.5,
+ '&:hover': {
+ borderColor: theme.palette.custom.borderHover,
+ },
+ }}
+ >
+ Remove
+
+
+
+
+ }
+ sx={{
+ borderColor: theme.palette.custom.borderDefault,
+ borderRadius: '4px',
+ borderWidth: '1px',
+ borderStyle: 'solid',
+ height: '25px',
+ opacity: 0.4,
+ color: theme.palette.text.secondary,
+ fontSize: '0.85rem',
+ px: 1,
+ py: 0.5,
+ '&:hover': {
+ borderColor: theme.palette.custom.borderHover,
+ },
+ }}
+ >
+ Assign
+
+
+
+
+ }
+ sx={{
+ minWidth: '106px',
+ borderColor: theme.palette.custom.borderDefault,
+ borderRadius: '4px',
+ borderWidth: '1px',
+ borderStyle: 'solid',
+ height: '25px',
+ opacity: 0.4,
+ color: theme.palette.text.secondary,
+ fontSize: '0.85rem',
+ px: 1,
+ py: 0.5,
+ '&:hover': {
+ borderColor: theme.palette.custom.borderHover,
+ },
+ }}
+ >
+ Auto-match
+
+
+
+
+
+
+
+
+ {/* Table or ghost empty state inside Paper */}
+
+ {filteredData.length === 0 ? (
+
+
+
+
+ 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.
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
- {/* Channel Form Modal */}
- {/* Popover for the "copy" URLs */}
-
+
-
+
- {/* Snackbar for feedback */}
+
+
diff --git a/frontend/src/pages/Channels.js b/frontend/src/pages/Channels.js
index b4c00028..73eccb9a 100644
--- a/frontend/src/pages/Channels.js
+++ b/frontend/src/pages/Channels.js
@@ -10,10 +10,10 @@ const ChannelsPage = () => {
{