From 066868087804824ac52fbbfcfd2a8ce484a5b685 Mon Sep 17 00:00:00 2001 From: Dispatcharr Date: Fri, 7 Mar 2025 17:39:34 -0600 Subject: [PATCH] UI Sidebar Redesign Redesigned sidebar --- frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/src/App.js | 25 +- frontend/src/components/Sidebar.js | 237 ++--- .../src/components/tables/ChannelsTable.js | 459 ++++----- frontend/src/helpers/table.js | 27 +- frontend/src/images/dispatcharr.svg | 23 + frontend/src/images/logo.png | Bin 4606 -> 44564 bytes frontend/src/index.css | 18 + frontend/src/pages/Channels.js | 937 +++++++++++++++++- frontend/src/theme.js | 50 +- 11 files changed, 1350 insertions(+), 437 deletions(-) create mode 100644 frontend/src/images/dispatcharr.svg diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b9b072dc..bf446dfe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "eslint": "^8.57.1", "formik": "^2.4.6", "hls.js": "^1.5.20", + "lucide-react": "^0.479.0", "material-react-table": "^3.2.0", "mpegts.js": "^1.4.2", "planby": "^1.1.7", @@ -11701,6 +11702,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.479.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.479.0.tgz", + "integrity": "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/m3u8-parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b35b8885..c64bf59f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "eslint": "^8.57.1", "formik": "^2.4.6", "hls.js": "^1.5.20", + "lucide-react": "^0.479.0", "material-react-table": "^3.2.0", "mpegts.js": "^1.4.2", "planby": "^1.1.7", diff --git a/frontend/src/App.js b/frontend/src/App.js index def311ff..ee0418a7 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -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 } from '@mui/material'; // removed AppBar/Toolbar import theme from './theme'; import EPG from './pages/EPG'; import Guide from './pages/Guide'; @@ -58,7 +58,7 @@ const App = () => { checkSuperuser(); }, []); - // Authentication check. + // Authentication check useEffect(() => { const checkAuth = async () => { const loggedIn = await initializeAuth(); @@ -72,7 +72,7 @@ const App = () => { checkAuth(); }, [initializeAuth, initData, setIsAuthenticated, logout]); - // If no superuser exists, show the initialization form. + // If no superuser exists, show the initialization form if (needsSuperuser) { return setNeedsSuperuser(false)} />; } @@ -82,6 +82,7 @@ const App = () => { + {/* Sidebar on the left */} { toggleDrawer={toggleDrawer} /> + {/* Main content area, no AppBar, so no marginTop */} - + {isAuthenticated ? ( <> @@ -136,6 +132,7 @@ const App = () => { + diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js index 5489311d..4ddaea1d 100644 --- a/frontend/src/components/Sidebar.js +++ b/frontend/src/components/Sidebar.js @@ -1,148 +1,151 @@ import React from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { - List, - ListItem, - ListItemButton, - ListItemText, - ListItemIcon, - Box, - Divider, Drawer, - TextField, + Toolbar, + Box, + Typography, + Avatar, + List, + ListItemButton, + ListItemIcon, + ListItemText, } from '@mui/material'; import { - Tv as TvIcon, - CalendarMonth as CalendarMonthIcon, - VideoFile as VideoFileIcon, - LiveTv as LiveTvIcon, - PlaylistPlay as PlaylistPlayIcon, - Settings as SettingsIcon, - Logout as LogoutIcon, -} from '@mui/icons-material'; + ListOrdered, + Play, + Database, + SlidersHorizontal, + LayoutGrid, + Settings as LucideSettings, +} from 'lucide-react'; import logo from '../images/logo.png'; -import useAuthStore from '../store/auth'; -import useSettingsStore from '../store/settings'; +import { ReactComponent as DispatcharrLogo } from '../images/dispatcharr.svg'; -const items = [ - { text: 'Channels', icon: , route: '/channels' }, - { text: 'M3U', icon: , route: '/m3u' }, - { text: 'EPG', icon: , route: '/epg' }, - { - text: 'Stream Profiles', - icon: , - route: '/stream-profiles', - }, - { text: 'TV Guide', icon: , route: '/guide' }, - { text: 'Settings', icon: , route: '/settings' }, +const navItems = [ + { label: 'Channels', icon: , path: '/channels' }, + { label: 'M3U', icon: , path: '/m3u' }, + { label: 'EPG', icon: , path: '/epg' }, + { label: 'Stream Profiles', icon: , path: '/stream-profiles' }, + { label: 'TV Guide', icon: , path: '/guide' }, + { label: 'Settings', icon: , path: '/settings' }, ]; -const Sidebar = ({ open, miniDrawerWidth, drawerWidth, toggleDrawer }) => { +const Sidebar = ({ open, drawerWidth, miniDrawerWidth, toggleDrawer }) => { const location = useLocation(); - const { isAuthenticated, logout } = useAuthStore(); - const { - environment: { public_ip, country_code, country_name }, - } = useSettingsStore(); - const navigate = useNavigate(); - - const onLogout = () => { - logout(); - navigate('/login'); - }; return ( - - - + + {open ? ( + + Dispatcharr Logo + + Dispatcharr + + + ) : ( + Dispatcharr Logo + )} + + + + {navItems.map((item) => { + const isActive = location.pathname.startsWith(item.path); + return ( - logo - {open && ( - - )} - - - - - - - {items.map((item) => ( - - - {item.icon} - {open && } - - - ))} - - - - {isAuthenticated && ( - - - - - - + + {item.icon} - - - - - {open && ( - - {/* Public IP + optional flag */} - - - {/* If we have a country code, show a small flag */} - {country_code && ( - {country_name )} - - )} - - )} + + ); + })} + + + + + + {open && ( + + John Doe + + )} + ); }; diff --git a/frontend/src/components/tables/ChannelsTable.js b/frontend/src/components/tables/ChannelsTable.js index 3fcfca7b..70fd3124 100644 --- a/frontend/src/components/tables/ChannelsTable.js +++ b/frontend/src/components/tables/ChannelsTable.js @@ -1,36 +1,33 @@ +// frontend/src/components/tables/ChannelsTable.js import { useEffect, useMemo, useRef, useState } from 'react'; -import { - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table'; +import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { Box, - Grid2, Stack, Typography, Tooltip, IconButton, - Button, ButtonGroup, + Button, Snackbar, Popover, TextField, } from '@mui/material'; -import useChannelsStore from '../../store/channels'; import { Delete as DeleteIcon, Edit as EditIcon, Add as AddIcon, SwapVert as SwapVertIcon, LiveTv as LiveTvIcon, + Tv as TvIcon, ContentCopy, - Tv as TvIcon, // <-- ADD THIS IMPORT } 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 useChannelsStore from '../../store/channels'; import useVideoStore from '../../store/useVideoStore'; import useSettingsStore from '../../store/settings'; @@ -43,16 +40,20 @@ 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 { environment: { env_mode }, } = useSettingsStore(); + const { showVideo } = useVideoStore.getState(); - // Configure columns - const columns = useMemo( - () => [ + const rowVirtualizerInstanceRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [sorting, setSorting] = useState([]); + + // Columns + const columns = useMemo(() => { + return [ { header: '#', size: 50, @@ -69,178 +70,21 @@ const ChannelsTable = () => { { header: 'Logo', accessorKey: 'logo_url', - size: 55, + size: 60, Cell: ({ cell }) => ( - - channel logo - + + channel logo + ), - meta: { - filterVariant: null, - }, }, - ], - [] - ); - - // Access the row virtualizer instance (optional) - const rowVirtualizerInstanceRef = useRef(null); - - const [isLoading, setIsLoading] = useState(true); - const [sorting, setSorting] = useState([]); - - const closeSnackbar = () => setSnackbarOpen(false); - - const editChannel = async (ch = null) => { - setChannel(ch); - setChannelModalOpen(true); - }; - - const deleteChannel = async (id) => { - setIsLoading(true); - await API.deleteChannel(id); - setIsLoading(false); - }; - - function handleWatchStream(channelNumber) { - let vidUrl = `/output/stream/${channelNumber}/`; - if (env_mode == 'dev') { - vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; - } - showVideo(vidUrl); - } - - // (Optional) bulk delete, but your endpoint is @TODO - const deleteChannels = async () => { - setIsLoading(true); - const selected = table - .getRowModel() - .rows.filter((row) => row.getIsSelected()); - await utils.Limiter( - 4, - selected.map((chan) => () => deleteChannel(chan.original.id)) - ); - // If you have a real bulk-delete endpoint, call it here: - // await API.deleteChannels(selected.map((sel) => sel.id)); - setIsLoading(false); - }; - - // ───────────────────────────────────────────────────────── - // The "Assign Channels" button logic - // ───────────────────────────────────────────────────────── - const assignChannels = async () => { - try { - // Get row order from the table - const rowOrder = table.getRowModel().rows.map((row) => row.original.id); - - // Call our custom API endpoint - setIsLoading(true); - const result = await API.assignChannelNumbers(rowOrder); - setIsLoading(false); - - // We might get { message: "Channels have been auto-assigned!" } - setSnackbarMessage(result.message || 'Channels assigned'); - setSnackbarOpen(true); - - // Refresh the channel list - await useChannelsStore.getState().fetchChannels(); - } catch (err) { - console.error(err); - setSnackbarMessage('Failed to assign channels'); - setSnackbarOpen(true); - } - }; - - // ───────────────────────────────────────────────────────── - // The new "Match EPG" button logic - // ───────────────────────────────────────────────────────── - const matchEpg = async () => { - try { - // Hit our new endpoint that triggers the fuzzy matching Celery task - const resp = await fetch('/api/channels/channels/match-epg/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${await API.getAuthToken()}`, - }, - }); - - if (resp.ok) { - setSnackbarMessage('EPG matching task started!'); - } else { - const text = await resp.text(); - setSnackbarMessage(`Failed to start EPG matching: ${text}`); - } - } catch (err) { - setSnackbarMessage(`Error: ${err.message}`); - } - setSnackbarOpen(true); - }; - - const closeChannelForm = () => { - setChannel(null); - setChannelModalOpen(false); - }; - - useEffect(() => { - if (typeof window !== 'undefined') { - setIsLoading(false); - } + ]; }, []); - useEffect(() => { - // Scroll to the top of the table when sorting changes - try { - rowVirtualizerInstanceRef.current?.scrollToIndex?.(0); - } catch (error) { - console.error(error); - } - }, [sorting]); - - const closePopover = () => { - setAnchorEl(null); - setSnackbarMessage(''); - }; - const openPopover = Boolean(anchorEl); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(textToCopy); - setSnackbarMessage('Copied!'); - } catch (err) { - setSnackbarMessage('Failed to copy'); - } - setSnackbarOpen(true); - }; - - // Example copy URLs - const copyM3UUrl = (event) => { - setAnchorEl(event.currentTarget); - setTextToCopy( - `${window.location.protocol}//${window.location.host}/output/m3u` - ); - }; - const copyEPGUrl = (event) => { - setAnchorEl(event.currentTarget); - setTextToCopy( - `${window.location.protocol}//${window.location.host}/output/epg` - ); - }; - const copyHDHRUrl = (event) => { - setAnchorEl(event.currentTarget); - setTextToCopy( - `${window.location.protocol}//${window.location.host}/output/hdhr` - ); - }; - - // Configure the MaterialReactTable + // Common table logic const table = useMaterialReactTable({ ...TableHelper.defaultProperties, columns, @@ -255,114 +99,204 @@ const ChannelsTable = () => { sorting, rowSelection, }, - rowVirtualizerInstanceRef, // optional + rowVirtualizerInstanceRef, rowVirtualizerOptions: { overscan: 5 }, - initialState: { - density: 'compact', - }, enableRowActions: true, renderRowActions: ({ row }) => ( - + + {/* Edit channel */} { - editChannel(row.original); - }} - sx={{ p: 0 }} + onClick={() => editChannel(row.original)} > + {/* Delete channel */} deleteChannel(row.original.id)} - sx={{ p: 0 }} > + {/* Watch now */} handleWatchStream(row.original.channel_number)} - sx={{ p: 0 }} > - + ), muiTableContainerProps: { sx: { - height: 'calc(100vh - 75px)', + height: 'calc(100% - 40px)', // fill parent minus a bit for your top row overflowY: 'auto', }, }, - muiSearchTextFieldProps: { - variant: 'standard', - }, - renderTopToolbarCustomActions: ({ table }) => ( - - Channels - - editChannel()} - > - - - - - - - - - - - - - - - {/* Our brand-new button for EPG matching */} - - - - - - - - - - + renderTopToolbarCustomActions: () => ( + + {/* “HDHR URL”, “M3U URL”, “EPG” ButtonGroup like your screenshot */} + + + + + + {/* Additional actions: auto-assign, auto-match, add, remove, etc. */} + + + + + + + + + + + + + + editChannel()}> + + + + + + + + + ), }); + // Lifecycle + useEffect(() => { + if (typeof window !== 'undefined') { + setIsLoading(false); + } + }, []); + + useEffect(() => { + try { + rowVirtualizerInstanceRef.current?.scrollToIndex?.(0); + } catch (error) { + console.error(error); + } + }, [sorting]); + + // Channel actions + function editChannel(channel = null) { + setChannel(channel); + setChannelModalOpen(true); + } + + async function deleteChannel(id) { + setIsLoading(true); + await API.deleteChannel(id); + setIsLoading(false); + } + + function handleWatchStream(channelNumber) { + let vidUrl = `/output/stream/${channelNumber}/`; + if (env_mode === 'dev') { + vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; + } + showVideo(vidUrl); + } + + async function deleteChannels() { + setIsLoading(true); + const selected = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()); + await utils.Limiter( + 4, + selected.map((chan) => () => deleteChannel(chan.original.id)) + ); + setIsLoading(false); + } + + async function assignChannels() { + try { + const rowOrder = table.getRowModel().rows.map((row) => row.original.id); + setIsLoading(true); + const result = await API.assignChannelNumbers(rowOrder); + setIsLoading(false); + setSnackbarMessage(result.message || 'Channels assigned'); + setSnackbarOpen(true); + await useChannelsStore.getState().fetchChannels(); + } catch (err) { + console.error(err); + setSnackbarMessage('Failed to assign channels'); + setSnackbarOpen(true); + } + } + + async function matchEpg() { + try { + const resp = await fetch('/api/channels/channels/match-epg/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${await API.getAuthToken()}`, + }, + }); + if (resp.ok) { + setSnackbarMessage('EPG matching task started!'); + } else { + const text = await resp.text(); + setSnackbarMessage(`Failed to start EPG matching: ${text}`); + } + } catch (err) { + setSnackbarMessage(`Error: ${err.message}`); + } + setSnackbarOpen(true); + } + + // Copy popover + const openPopover = Boolean(anchorEl); + function closePopover() { + setAnchorEl(null); + } + async function handleCopy() { + try { + await navigator.clipboard.writeText(textToCopy); + setSnackbarMessage('Copied!'); + } catch (err) { + setSnackbarMessage('Failed to copy'); + } + setSnackbarOpen(true); + } + function copyHDHRUrl(event) { + setAnchorEl(event.currentTarget); + setTextToCopy(`${window.location.protocol}//${window.location.host}/output/hdhr`); + } + function copyM3UUrl(event) { + setAnchorEl(event.currentTarget); + setTextToCopy(`${window.location.protocol}//${window.location.host}/output/m3u`); + } + function copyEPGUrl(event) { + setAnchorEl(event.currentTarget); + setTextToCopy(`${window.location.protocol}//${window.location.host}/output/epg`); + } + + // Channel form close + function closeChannelForm() { + setChannel(null); + setChannelModalOpen(false); + } + + // Snackbar + const handleSnackbarClose = () => { + setSnackbarOpen(false); + }; + return ( - + {/* Channel Form Modal */} @@ -372,36 +306,33 @@ const ChannelsTable = () => { onClose={closeChannelForm} /> - {/* Popover for the "copy" URLs */} + {/* Popover for "copy" URLs */} -
+ -
+
- {/* Snackbar for feedback */} + {/* Snackbar messages */}
diff --git a/frontend/src/helpers/table.js b/frontend/src/helpers/table.js index 84aeb328..320c0a87 100644 --- a/frontend/src/helpers/table.js +++ b/frontend/src/helpers/table.js @@ -1,3 +1,5 @@ +// frontend/src/helpers/table.js + export default { defaultProperties: { enableGlobalFilter: false, @@ -13,19 +15,34 @@ export default { }, muiTableBodyCellProps: { sx: { - padding: 0, + padding: '6px', + borderColor: '#444', + color: '#E0E0E0', + fontSize: '0.85rem', }, }, muiTableHeadCellProps: { sx: { - padding: 0, + padding: '6px', + color: '#CFCFCF', + backgroundColor: '#383A3F', + borderColor: '#444', + fontWeight: 600, + fontSize: '0.8rem', }, }, muiTableBodyProps: { sx: { - //stripe the rows, make odd rows a darker color - '& tr:nth-of-type(odd) > td': { - // backgroundColor: '#f5f5f5', + // Subtle row striping + '& tr:nth-of-type(odd)': { + backgroundColor: '#2F3034', + }, + '& tr:nth-of-type(even)': { + backgroundColor: '#333539', + }, + // Row hover effect + '& tr:hover td': { + backgroundColor: '#3B3D41', }, }, }, diff --git a/frontend/src/images/dispatcharr.svg b/frontend/src/images/dispatcharr.svg new file mode 100644 index 00000000..3ddd8e2b --- /dev/null +++ b/frontend/src/images/dispatcharr.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/images/logo.png b/frontend/src/images/logo.png index afadd08e3ff7a00fc15f0d3bc33b4d1db859a069..99c3c19f3d6ba0647c2883654126d5ea8b529020 100644 GIT binary patch literal 44564 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 { + // + // ----------------------------- + // 1) HOOKS & GLOBAL STORE DATA + // ----------------------------- + // + const { channels, fetchChannels } = useChannelsStore(); + const { streams, fetchStreams } = useStreamsStore(); + const { showVideo } = useVideoStore.getState(); + const { showAlert } = useAlertStore(); + + // We fetch channels/streams if needed + useEffect(() => { + // If not loaded yet, fetch them: + fetchChannels().catch((err) => console.error('Failed to fetch channels', err)); + fetchStreams().catch((err) => console.error('Failed to fetch streams', err)); + // eslint-disable-next-line + }, []); + + // + // ----------------------------- + // 2) LOCAL STATE FOR SELECTION + // ----------------------------- + // + const [selectedChannelIds, setSelectedChannelIds] = useState([]); + const [selectedStreamIds, setSelectedStreamIds] = useState([]); + + // For opening the Channel/Stream forms + const [channelFormOpen, setChannelFormOpen] = useState(false); + const [editingChannel, setEditingChannel] = useState(null); + + const [streamFormOpen, setStreamFormOpen] = useState(false); + const [editingStream, setEditingStream] = useState(null); + + // + // ----------------------------- + // 3) CHANNEL ACTIONS + // ----------------------------- + // + function handleToggleChannel(channelId) { + setSelectedChannelIds((prev) => { + if (prev.includes(channelId)) { + return prev.filter((id) => id !== channelId); + } else { + return [...prev, channelId]; + } + }); + } + + function handleSelectAllChannels() { + if (selectedChannelIds.length === channels.length) { + setSelectedChannelIds([]); + } else { + setSelectedChannelIds(channels.map((c) => c.id)); + } + } + + async function handleRemoveChannels() { + if (selectedChannelIds.length === 0) return; + // This calls your existing bulk delete method + try { + await API.deleteChannels(selectedChannelIds); + setSelectedChannelIds([]); + showAlert(`Deleted ${selectedChannelIds.length} channels`, 'success'); + } catch (err) { + console.error(err); + showAlert('Failed to remove channels', 'error'); + } + } + + async function handleAssignChannels() { + // The example calls a reorder method. If you have a different approach, adapt here + const channelIdsInCurrentOrder = channels.map((ch) => ch.id); + try { + await API.assignChannelNumbers(channelIdsInCurrentOrder); + showAlert('Channels assigned successfully!', 'success'); + } catch (err) { + console.error(err); + showAlert('Failed to assign channels', 'error'); + } + } + + async function handleAutoMatch() { + // Example "match-epg" call from your code: + try { + const resp = await fetch('/api/channels/channels/match-epg/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${await API.getAuthToken()}`, + }, + }); + if (resp.ok) { + showAlert('EPG matching task started!', 'success'); + } else { + const text = await resp.text(); + showAlert(`Failed to start EPG matching: ${text}`, 'error'); + } + } catch (err) { + showAlert(`Error: ${err.message}`, 'error'); + } + } + + function handleAddChannel() { + setEditingChannel(null); + setChannelFormOpen(true); + } + + function handleEditChannel(channel) { + setEditingChannel(channel); + setChannelFormOpen(true); + } + + async function handleDeleteChannel(channelId) { + try { + await API.deleteChannel(channelId); + showAlert('Channel deleted', 'success'); + } catch (err) { + console.error(err); + showAlert('Failed to delete channel', 'error'); + } + } + + function handlePlayChannel(channel) { + // For your environment logic, adapt as needed + const vidUrl = `/output/stream/${channel.channel_number}`; + showVideo(vidUrl); + } + + // + // ----------------------------- + // 4) STREAM ACTIONS + // ----------------------------- + // + function handleToggleStream(streamId) { + setSelectedStreamIds((prev) => { + if (prev.includes(streamId)) { + return prev.filter((id) => id !== streamId); + } else { + return [...prev, streamId]; + } + }); + } + + function handleSelectAllStreams() { + if (selectedStreamIds.length === streams.length) { + setSelectedStreamIds([]); + } else { + setSelectedStreamIds(streams.map((s) => s.id)); + } + } + + async function handleRemoveStreams() { + if (selectedStreamIds.length === 0) return; + try { + await API.deleteStreams(selectedStreamIds); + setSelectedStreamIds([]); + showAlert(`Deleted ${selectedStreamIds.length} streams`, 'success'); + } catch (err) { + console.error(err); + showAlert('Failed to remove streams', 'error'); + } + } + + // Bulk "create channels" from selected streams + async function handleCreateChannelsFromStreams() { + if (selectedStreamIds.length === 0) return; + // If your API is `createChannelsFromStreams()`, adapt below + const payload = selectedStreamIds.map((streamId) => { + const st = streams.find((s) => s.id === streamId); + return { + stream_id: st.id, + channel_name: st.name, + }; + }); + try { + await API.createChannelsFromStreams(payload); + showAlert(`Created channels from ${selectedStreamIds.length} streams`, 'success'); + } catch (err) { + console.error(err); + showAlert('Failed to create channels', 'error'); + } + } + + function handleAddStream() { + setEditingStream(null); + setStreamFormOpen(true); + } + + function handleEditStream(stream) { + setEditingStream(stream); + setStreamFormOpen(true); + } + + async function handleDeleteStream(streamId) { + try { + await API.deleteStream(streamId); + showAlert('Stream deleted', 'success'); + } catch (err) { + console.error(err); + showAlert('Failed to delete stream', 'error'); + } + } + + function handlePlayStream(stream) { + // If your environment logic differs, adapt as needed + const vidUrl = `/output/stream/${stream.id}`; + showVideo(vidUrl); + } + + // + // ----------------------------- + // 5) RENDER + // ----------------------------- + // return ( - - - + {/* We do NOT replicate the example's built-in sidebar here + because your App.js + is already handling that. + So we skip the sidebar portion from the Figma code. */} + + {/* Main content: 2 columns => Channels (left), Streams (right) */} + + {/* ------------------------ */} + {/* CHANNELS SECTION */} + {/* ------------------------ */} + + + Channels + + + + {/* Toolbar for Channels */} + + + + Links: + + {['HDHR', 'M3U', 'EPG'].map((link) => ( + { + // If you have a real link action, put it here + showAlert(`Clicked ${link}`, 'info'); + }} + /> + ))} + + + + + + + + + + + {/* Channels Table */} + + + + + + {/* "Select All" for Channels */} + + + + + + #{' '} + + + + Name{' '} + + + + Group{' '} + + + + Logo{' '} + + + + Actions + + + + + {channels.map((channel) => { + const isSelected = selectedChannelIds.includes(channel.id); + return ( + + + handleToggleChannel(channel.id)} + > + {isSelected ? ( + + ) : ( + + )} + + + + {channel.channel_number || channel.id} + + + {channel.channel_name} + + + {channel.channel_group + ? channel.channel_group.name + : ''} + + + {channel.logo_url ? ( + + ) : ( + + )} + + + + + handleEditChannel(channel)} + > + + + handlePlayChannel(channel)} + > + + + + handleDeleteChannel(channel.id)} + > + + + + + + ); + })} + +
+
+
+
+ + {/* ------------------------ */} + {/* STREAMS SECTION */} + {/* ------------------------ */} + + + Streams + + + + {/* Toolbar for Streams */} + + + + + + + + + {/* Streams Table */} + + + + + + {/* "Select All" for Streams */} + + + + + + Name{' '} + + + + Group{' '} + + + + M3U{' '} + + + + Actions + + + + + {streams.map((stream) => { + const isSelected = selectedStreamIds.includes(stream.id); + return ( + + + handleToggleStream(stream.id)} + > + {isSelected ? ( + + ) : ( + + )} + + + + {stream.name} + + + {stream.group_name || ''} + + + {/* If your store uses something else for "m3u" or "m3u_account", + adapt this line accordingly */} + {stream.m3u_account ? 'Yes' : 'No'} + + + + + handleEditStream(stream)} + > + + + handlePlayStream(stream)} + > + + + + handleDeleteStream(stream.id)} + > + + + + + + ); + })} + +
+
+
+
+
+ + {/* Channel Form Modal */} + {channelFormOpen && ( + { + setChannelFormOpen(false); + setEditingChannel(null); }} - > - -
-
- - + )} + + {/* Stream Form Modal */} + {streamFormOpen && ( + { + setStreamFormOpen(false); + setEditingStream(null); }} - > - - - -
+ /> + )} + ); }; diff --git a/frontend/src/theme.js b/frontend/src/theme.js index 258d415a..9c70a255 100644 --- a/frontend/src/theme.js +++ b/frontend/src/theme.js @@ -1,21 +1,65 @@ +// frontend/src/theme.js import { createTheme } from '@mui/material/styles'; const theme = createTheme({ palette: { mode: 'dark', + background: { + default: '#2B2C30', // Dark background + paper: '#333539', // Slightly lighter panel background + }, primary: { - main: '#495057', - contrastText: '#ffffff', // Ensure text is visible on primary color + // Adjust accent color if your Figma calls for a different highlight + main: '#4A90E2', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#F5A623', + contrastText: '#FFFFFF', + }, + text: { + primary: '#FFFFFF', + secondary: '#C3C3C3', + }, + }, + typography: { + fontFamily: ['Roboto', 'Helvetica', 'Arial', 'sans-serif'].join(','), + // Example typography tweaks + h6: { + fontWeight: 500, + fontSize: '0.95rem', + }, + body1: { + fontSize: '0.875rem', }, }, components: { MuiButton: { styleOverrides: { root: { - // textTransform: 'none', // Disable uppercase on buttons + borderRadius: 4, + textTransform: 'none', + fontWeight: 500, }, }, }, + MuiDrawer: { + styleOverrides: { + paper: { + backgroundColor: '#333539', + color: '#FFFFFF', + }, + }, + }, + // We remove the AppBar override since we won't be using it in App.js anymore + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: '#2B2C30', + }, + }, + }, + // Feel free to override more MUI components as needed... }, });