more changes, table styling and compacting

This commit is contained in:
kappa118 2025-02-26 20:01:11 -05:00
parent febc908ad8
commit 5dc4e54585
19 changed files with 547 additions and 15648 deletions

View file

@ -14,6 +14,7 @@
"@mui/icons-material": "^6.4.5",
"@mui/material": "^6.4.5",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"eslint": "^8.57.1",
"formik": "^2.4.6",
"material-react-table": "^3.2.0",
@ -23,6 +24,7 @@
"react-dom": "^19.0.0",
"react-router-dom": "^7.2.0",
"react-scripts": "5.0.1",
"react-window": "^1.8.11",
"web-vitals": "^2.1.4",
"yup": "^1.6.1",
"zustand": "^5.0.3"
@ -6511,6 +6513,12 @@
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -11183,6 +11191,12 @@
"node": ">= 4.0.0"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@ -13783,6 +13797,23 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/react-window": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
"integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
},
"engines": {
"node": ">8.0.0"
},
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View file

@ -9,6 +9,7 @@
"@mui/icons-material": "^6.4.5",
"@mui/material": "^6.4.5",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"eslint": "^8.57.1",
"formik": "^2.4.6",
"material-react-table": "^3.2.0",
@ -18,6 +19,7 @@
"react-dom": "^19.0.0",
"react-router-dom": "^7.2.0",
"react-scripts": "5.0.1",
"react-window": "^1.8.11",
"web-vitals": "^2.1.4",
"yup": "^1.6.1",
"zustand": "^5.0.3"

File diff suppressed because it is too large Load diff

View file

@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

View file

@ -11,9 +11,6 @@ import Channels from './pages/Channels';
import M3U from './pages/M3U';
import { ThemeProvider } from '@mui/material/styles'; // Import theme tools
import {
AppBar,
Toolbar,
Typography,
Box,
CssBaseline,
Drawer,
@ -25,10 +22,10 @@ import {
} from '@mui/material';
import theme from './theme';
import EPG from './pages/EPG';
import Guide from './pages/Guide';
// import Guide from './pages/Guide';
import StreamProfiles from './pages/StreamProfiles';
import useAuthStore from './store/auth';
import API from './api';
import logo from './images/logo.png';
const drawerWidth = 240;
const miniDrawerWidth = 60;
@ -104,7 +101,7 @@ const App = () => {
pb: 0,
}}
>
<img src="/images/logo.png" width="33x" />
<img src={logo} width="33x" />
{open && (
<ListItemText primary="Dispatcharr" sx={{ paddingLeft: 3 }} />
)}
@ -153,7 +150,7 @@ const App = () => {
path="/stream-profiles"
element={<StreamProfiles />}
/>
<Route exact path="/guide" element={<Guide />} />
{/* <Route exact path="/guide" element={<Guide />} /> */}
</>
) : (
<Route path="/login" element={<Login />} />

View file

@ -1,4 +1,3 @@
import Axios from 'axios';
import useAuthStore from './store/auth';
import useChannelsStore from './store/channels';
import useUserAgentsStore from './store/userAgents';
@ -11,7 +10,8 @@ import useStreamProfilesStore from './store/streamProfiles';
// withCredentials: true,
// });
const host = 'http://192.168.1.151:9191';
const host = 'http://127.0.0.1:9191';
// const host = '';
const getAuthToken = async () => {
const token = await useAuthStore.getState().getToken(); // Assuming token is stored in Zustand store
@ -602,7 +602,6 @@ export default class API {
});
const retval = await response.json();
console.log(retval);
return retval;
return retval.data;
}
}

View file

@ -31,6 +31,7 @@ import {
} from 'material-react-table';
import ChannelGroupForm from './ChannelGroup';
import usePlaylistsStore from '../../store/playlists';
import logo from '../../images/logo.png';
const Channel = ({ channel = null, isOpen, onClose }) => {
const channelGroups = useChannelsStore((state) => state.channelGroups);
@ -38,8 +39,8 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
const { profiles: streamProfiles } = useStreamProfilesStore();
const { playlists } = usePlaylistsStore();
const [logo, setLogo] = useState(null);
const [logoPreview, setLogoPreview] = useState('/images/logo.png');
const [logoFile, setLogoFile] = useState(null);
const [logoPreview, setLogoPreview] = useState(logo);
const [channelStreams, setChannelStreams] = useState([]);
const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false);
@ -58,7 +59,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
const handleLogoChange = (e) => {
const file = e.target.files[0];
if (file) {
setLogo(file);
setLogoFile(file);
setLogoPreview(URL.createObjectURL(file));
}
};
@ -83,20 +84,20 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
await API.updateChannel({
id: channel.id,
...values,
logo_file: logo,
logo_file: logoFile,
streams: channelStreams.map((stream) => stream.id),
});
} else {
await API.addChannel({
...values,
logo_file: logo,
logo_file: logoFile,
streams: channelStreams.map((stream) => stream.id),
});
}
resetForm();
setLogo(null);
setLogoPreview('/images/logo.png');
setLogoFile(null);
setLogoPreview(logo);
setSubmitting(false);
onClose();
},

View file

@ -31,7 +31,7 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
validationSchema: Yup.object({
name: Yup.string().required('Name is required'),
url: Yup.string().required('URL is required').min(0),
stream_profile_id: Yup.string().required('Stream profile is required'),
// stream_profile_id: Yup.string().required('Stream profile is required'),
}),
onSubmit: async (values, { setSubmitting, resetForm }) => {
if (stream?.id) {

View file

@ -28,6 +28,7 @@ import ChannelForm from '../forms/Channel';
import { TableHelper } from '../../helpers';
import utils from '../../utils';
import { ContentCopy } from '@mui/icons-material';
import logo from '../../images/logo.png';
const Example = () => {
const [channel, setChannel] = useState(null);
@ -71,7 +72,7 @@ const Example = () => {
alignItems: 'center',
}}
>
<img src={info.getValue() || '/images/logo.png'} width="20" />
<img src={info.getValue() || logo} width="20" />
</Grid2>
),
meta: {
@ -175,7 +176,6 @@ const Example = () => {
...TableHelper.defaultProperties,
columns,
data: channels,
// enableGlobalFilterModes: true,
enablePagination: false,
// enableRowNumbers: true,
enableRowVirtualization: true,
@ -194,13 +194,14 @@ const Example = () => {
},
enableRowActions: true,
renderRowActions: ({ row }) => (
<>
<Box sx={{ justifyContent: 'right' }}>
<IconButton
size="small" // Makes the button smaller
color="warning" // Red color for delete actions
onClick={() => {
editChannel(row.original);
}}
sx={{ p: 0 }}
>
<EditIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -208,10 +209,11 @@ const Example = () => {
size="small" // Makes the button smaller
color="error" // Red color for delete actions
onClick={() => deleteChannel(row.original.id)}
sx={{ p: 0 }}
>
<DeleteIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
</>
</Box>
),
muiTableContainerProps: {
sx: {
@ -266,13 +268,13 @@ const Example = () => {
marginLeft: 1,
}}
>
<Button variant="contained" onClick={copyHDHRUrl}>
<Button variant="contained" size="small" onClick={copyHDHRUrl}>
HDHR URL
</Button>
<Button variant="contained" onClick={copyM3UUrl}>
<Button variant="contained" size="small" onClick={copyM3UUrl}>
M3U URL
</Button>
<Button variant="contained" onClick={copyEPGUrl}>
<Button variant="contained" size="small" onClick={copyEPGUrl}>
EPG
</Button>
</ButtonGroup>

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from 'react';
import {
MaterialReactTable,
MRT_ShowHideColumnsButton,
MRT_ToggleFullScreenButton,
useMaterialReactTable,
} from "material-react-table";
} from 'material-react-table';
import {
Box,
Grid2,
@ -16,8 +16,8 @@ import {
Select,
MenuItem,
Snackbar,
} from "@mui/material";
import API from "../../api";
} from '@mui/material';
import API from '../../api';
import {
Delete as DeleteIcon,
Edit as EditIcon,
@ -26,16 +26,16 @@ import {
Check as CheckIcon,
Close as CloseIcon,
Refresh as RefreshIcon,
} from "@mui/icons-material";
import useEPGsStore from "../../store/epgs";
import EPGForm from "../forms/EPG";
import { TableHelper } from "../../helpers";
} from '@mui/icons-material';
import useEPGsStore from '../../store/epgs';
import EPGForm from '../forms/EPG';
import { TableHelper } from '../../helpers';
const EPGsTable = () => {
const [epg, setEPG] = useState(null);
const [epgModalOpen, setEPGModalOpen] = useState(false);
const [rowSelection, setRowSelection] = useState([]);
const [snackbarMessage, setSnackbarMessage] = useState("");
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarOpen, setSnackbarOpen] = useState(false);
const epgs = useEPGsStore((state) => state.epgs);
@ -44,21 +44,19 @@ const EPGsTable = () => {
//column definitions...
() => [
{
header: "Name",
size: 10,
accessorKey: "name",
header: 'Name',
accessorKey: 'name',
},
{
header: "Source Type",
accessorKey: "source_type",
size: 50,
header: 'Source Type',
accessorKey: 'source_type',
},
{
header: "URL / API Key",
accessorKey: "max_streams",
header: 'URL / API Key',
accessorKey: 'max_streams',
},
],
[],
[]
);
//optionally access the underlying virtualizer instance
@ -82,12 +80,12 @@ const EPGsTable = () => {
const refreshEPG = async (id) => {
await API.refreshEPG(id);
setSnackbarMessage("EPG refresh initiated");
setSnackbarMessage('EPG refresh initiated');
setSnackbarOpen(true);
};
useEffect(() => {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
setIsLoading(false);
}
}, []);
@ -118,7 +116,7 @@ const EPGsTable = () => {
rowVirtualizerInstanceRef, //optional
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
initialState: {
density: "compact",
density: 'compact',
},
enableRowActions: true,
renderRowActions: ({ row }) => (
@ -127,6 +125,7 @@ const EPGsTable = () => {
size="small" // Makes the button smaller
color="info" // Red color for delete actions
onClick={() => editEPG(row.original)}
sx={{ pt: 0, pb: 0 }}
>
<EditIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -134,6 +133,7 @@ const EPGsTable = () => {
size="small" // Makes the button smaller
color="error" // Red color for delete actions
onClick={() => deleteEPG(row.original.id)}
sx={{ pt: 0, pb: 0 }}
>
<DeleteIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -141,6 +141,7 @@ const EPGsTable = () => {
size="small" // Makes the button smaller
color="info" // Red color for delete actions
onClick={() => refreshEPG(row.original.id)}
sx={{ pt: 0, pb: 0 }}
>
<RefreshIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -148,14 +149,14 @@ const EPGsTable = () => {
),
muiTableContainerProps: {
sx: {
height: "calc(42vh - 0px)",
height: 'calc(42vh - 0px)',
},
},
renderTopToolbarCustomActions: ({ table }) => (
<Stack
direction="row"
sx={{
alignItems: "center",
alignItems: 'center',
}}
>
<Typography>EPGs</Typography>
@ -188,7 +189,7 @@ const EPGsTable = () => {
/>
<Snackbar
anchorOrigin={{ vertical: "top", horizontal: "right" }}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
open={snackbarOpen}
autoHideDuration={5000}
onClose={closeSnackbar}

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from 'react';
import {
MaterialReactTable,
MRT_ShowHideColumnsButton,
MRT_ToggleFullScreenButton,
useMaterialReactTable,
} from "material-react-table";
} from 'material-react-table';
import {
Box,
Grid2,
@ -15,8 +15,8 @@ import {
Checkbox,
Select,
MenuItem,
} from "@mui/material";
import API from "../../api";
} from '@mui/material';
import API from '../../api';
import {
Delete as DeleteIcon,
Edit as EditIcon,
@ -25,16 +25,16 @@ import {
Check as CheckIcon,
Close as CloseIcon,
Refresh as RefreshIcon,
} from "@mui/icons-material";
import usePlaylistsStore from "../../store/playlists";
import M3UForm from "../forms/M3U";
import { TableHelper } from "../../helpers";
} from '@mui/icons-material';
import usePlaylistsStore from '../../store/playlists';
import M3UForm from '../forms/M3U';
import { TableHelper } from '../../helpers';
const Example = () => {
const [playlist, setPlaylist] = useState(null);
const [playlistModalOpen, setPlaylistModalOpen] = useState(false);
const [rowSelection, setRowSelection] = useState([]);
const [activeFilterValue, setActiveFilterValue] = useState("all");
const [activeFilterValue, setActiveFilterValue] = useState('all');
const playlists = usePlaylistsStore((state) => state.playlists);
@ -42,28 +42,28 @@ const Example = () => {
//column definitions...
() => [
{
header: "Name",
accessorKey: "name",
header: 'Name',
accessorKey: 'name',
},
{
header: "URL / File",
accessorKey: "server_url",
header: 'URL / File',
accessorKey: 'server_url',
},
{
header: "Max Streams",
accessorKey: "max_streams",
header: 'Max Streams',
accessorKey: 'max_streams',
size: 200,
},
{
header: "Active",
accessorKey: "is_active",
header: 'Active',
accessorKey: 'is_active',
size: 100,
sortingFn: "basic",
sortingFn: 'basic',
muiTableBodyCellProps: {
align: "left",
align: 'left',
},
Cell: ({ cell }) => (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{cell.getValue() ? (
<CheckIcon color="success" />
) : (
@ -92,11 +92,11 @@ const Example = () => {
),
filterFn: (row, _columnId, activeFilterValue) => {
if (!activeFilterValue) return true; // Show all if no filter
return String(row.getValue("is_active")) === activeFilterValue;
return String(row.getValue('is_active')) === activeFilterValue;
},
},
],
[],
[]
);
//optionally access the underlying virtualizer instance
@ -126,7 +126,7 @@ const Example = () => {
};
useEffect(() => {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
setIsLoading(false);
}
}, []);
@ -157,7 +157,7 @@ const Example = () => {
rowVirtualizerInstanceRef, //optional
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
initialState: {
density: "compact",
density: 'compact',
},
enableRowActions: true,
renderRowActions: ({ row }) => (
@ -168,6 +168,7 @@ const Example = () => {
onClick={() => {
editPlaylist(row.original);
}}
sx={{ pt: 0, pb: 0 }}
>
<EditIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -175,6 +176,7 @@ const Example = () => {
size="small" // Makes the button smaller
color="error" // Red color for delete actions
onClick={() => deletePlaylist(row.original.id)}
sx={{ pt: 0, pb: 0 }}
>
<DeleteIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -183,6 +185,7 @@ const Example = () => {
color="info" // Red color for delete actions
variant="contained"
onClick={() => refreshPlaylist(row.original.id)}
sx={{ pt: 0, pb: 0 }}
>
<RefreshIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -190,14 +193,14 @@ const Example = () => {
),
muiTableContainerProps: {
sx: {
height: "calc(42vh - 0px)",
height: 'calc(42vh - 0px)',
},
},
renderTopToolbarCustomActions: ({ table }) => (
<Stack
direction="row"
sx={{
alignItems: "center",
alignItems: 'center',
}}
>
<Typography>M3U Accounts</Typography>

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from 'react';
import {
MaterialReactTable,
MRT_ShowHideColumnsButton,
MRT_ToggleFullScreenButton,
useMaterialReactTable,
} from "material-react-table";
} from 'material-react-table';
import {
Box,
Grid2,
@ -15,8 +15,8 @@ import {
Checkbox,
Select,
MenuItem,
} from "@mui/material";
import API from "../../api";
} from '@mui/material';
import API from '../../api';
import {
Delete as DeleteIcon,
Edit as EditIcon,
@ -25,19 +25,19 @@ import {
Check as CheckIcon,
Close as CloseIcon,
Refresh as RefreshIcon,
} from "@mui/icons-material";
import useEPGsStore from "../../store/epgs";
import StreamProfileForm from "../forms/StreamProfile";
import useStreamProfilesStore from "../../store/streamProfiles";
import { TableHelper } from "../../helpers";
} from '@mui/icons-material';
import useEPGsStore from '../../store/epgs';
import StreamProfileForm from '../forms/StreamProfile';
import useStreamProfilesStore from '../../store/streamProfiles';
import { TableHelper } from '../../helpers';
const StreamProfiles = () => {
const [profile, setProfile] = useState(null);
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [rowSelection, setRowSelection] = useState([]);
const [snackbarMessage, setSnackbarMessage] = useState("");
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [activeFilterValue, setActiveFilterValue] = useState("all");
const [activeFilterValue, setActiveFilterValue] = useState('all');
const streamProfiles = useStreamProfilesStore((state) => state.profiles);
@ -45,27 +45,27 @@ const StreamProfiles = () => {
//column definitions...
() => [
{
header: "Name",
accessorKey: "profile_name",
header: 'Name',
accessorKey: 'profile_name',
},
{
header: "Command",
accessorKey: "command",
header: 'Command',
accessorKey: 'command',
},
{
header: "Parameters",
accessorKey: "parameters",
header: 'Parameters',
accessorKey: 'parameters',
},
{
header: "Active",
accessorKey: "is_active",
header: 'Active',
accessorKey: 'is_active',
size: 100,
sortingFn: "basic",
sortingFn: 'basic',
muiTableBodyCellProps: {
align: "left",
align: 'left',
},
Cell: ({ cell }) => (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{cell.getValue() ? (
<CheckIcon color="success" />
) : (
@ -93,12 +93,12 @@ const StreamProfiles = () => {
</Box>
),
filterFn: (row, _columnId, filterValue) => {
if (filterValue == "all") return true; // Show all if no filter
return String(row.getValue("is_active")) === filterValue;
if (filterValue == 'all') return true; // Show all if no filter
return String(row.getValue('is_active')) === filterValue;
},
},
],
[],
[]
);
//optionally access the underlying virtualizer instance
@ -117,7 +117,7 @@ const StreamProfiles = () => {
};
useEffect(() => {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
setIsLoading(false);
}
}, []);
@ -148,7 +148,7 @@ const StreamProfiles = () => {
rowVirtualizerInstanceRef, //optional
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
initialState: {
density: "compact",
density: 'compact',
},
enableRowActions: true,
renderRowActions: ({ row }) => (
@ -157,6 +157,7 @@ const StreamProfiles = () => {
size="small" // Makes the button smaller
color="warning" // Red color for delete actions
onClick={() => editStreamProfile(row.original)}
sx={{ pt: 0, pb: 0 }}
>
<EditIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -164,6 +165,7 @@ const StreamProfiles = () => {
size="small" // Makes the button smaller
color="error" // Red color for delete actions
onClick={() => deleteStreamProfile(row.original.id)}
sx={{ pt: 0, pb: 0 }}
>
<DeleteIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -171,15 +173,15 @@ const StreamProfiles = () => {
),
muiTableContainerProps: {
sx: {
height: "calc(100vh - 100px)", // Subtract padding to avoid cutoff
overflowY: "auto", // Internal scrolling for the table
height: 'calc(100vh - 100px)', // Subtract padding to avoid cutoff
overflowY: 'auto', // Internal scrolling for the table
},
},
renderTopToolbarCustomActions: ({ table }) => (
<Stack
direction="row"
sx={{
alignItems: "center",
alignItems: 'center',
}}
>
<Typography>Stream Profiles</Typography>

View file

@ -116,7 +116,6 @@ const Example = () => {
columns,
data: streams,
// enableGlobalFilterModes: true,
enablePagination: false,
enableRowVirtualization: true,
enableRowSelection: true,
@ -132,33 +131,30 @@ const Example = () => {
enableRowActions: true,
renderRowActions: ({ row }) => (
<>
<Tooltip
title={
row.original.m3u_account ? 'M3U streams locked' : 'Edit Stream'
}
<IconButton
size="small" // Makes the button smaller
color="warning" // Red color for delete actions
onClick={() => editStream(row.original)}
disabled={row.original.m3u_account}
sx={{ p: 0 }}
>
<IconButton
size="small" // Makes the button smaller
color="warning" // Red color for delete actions
onClick={() => editStream(row.original)}
disabled={row.original.m3u_account}
>
<EditIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
</Tooltip>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small" // Makes the button smaller
color="error" // Red color for delete actions
onClick={() => deleteStream(row.original.id)}
sx={{ p: 0 }}
>
<DeleteIcon fontSize="small" /> {/* Small icon size */}
<DeleteIcon fontSize="small" />
</IconButton>
<IconButton
size="small" // Makes the button smaller
color="success" // Red color for delete actions
onClick={() => createChannelFromStream(row.original)}
sx={{ p: 0 }}
>
<AddIcon fontSize="small" /> {/* Small icon size */}
<AddIcon fontSize="small" />
</IconButton>
</>
),
@ -199,6 +195,7 @@ const Example = () => {
<Button
variant="contained"
onClick={createChannelsFromStreams}
size="small"
// disabled={rowSelection.length === 0}
sx={{
marginLeft: 1,

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from 'react';
import {
MaterialReactTable,
MRT_ShowHideColumnsButton,
MRT_ToggleFullScreenButton,
useMaterialReactTable,
} from "material-react-table";
} from 'material-react-table';
import {
Box,
Grid2,
@ -14,24 +14,24 @@ import {
Tooltip,
Select,
MenuItem,
} from "@mui/material";
import API from "../../api";
} from '@mui/material';
import API from '../../api';
import {
Delete as DeleteIcon,
Edit as EditIcon,
Add as AddIcon,
Check as CheckIcon,
Close as CloseIcon,
} from "@mui/icons-material";
import useUserAgentsStore from "../../store/userAgents";
import UserAgentForm from "../forms/UserAgent";
import { TableHelper } from "../../helpers";
} from '@mui/icons-material';
import useUserAgentsStore from '../../store/userAgents';
import UserAgentForm from '../forms/UserAgent';
import { TableHelper } from '../../helpers';
const UserAgentsTable = () => {
const [userAgent, setUserAgent] = useState(null);
const [userAgentModalOpen, setUserAgentModalOpen] = useState(false);
const [rowSelection, setRowSelection] = useState([]);
const [activeFilterValue, setActiveFilterValue] = useState("all");
const [activeFilterValue, setActiveFilterValue] = useState('all');
const userAgents = useUserAgentsStore((state) => state.userAgents);
@ -39,29 +39,27 @@ const UserAgentsTable = () => {
//column definitions...
() => [
{
header: "Name",
size: 10,
accessorKey: "user_agent_name",
header: 'Name',
accessorKey: 'user_agent_name',
},
{
header: "User-Agent",
accessorKey: "user_agent",
size: 50,
header: 'User-Agent',
accessorKey: 'user_agent',
},
{
header: "Desecription",
accessorKey: "description",
header: 'Desecription',
accessorKey: 'description',
},
{
header: "Active",
accessorKey: "is_active",
header: 'Active',
accessorKey: 'is_active',
size: 100,
sortingFn: "basic",
sortingFn: 'basic',
muiTableBodyCellProps: {
align: "left",
align: 'left',
},
Cell: ({ cell }) => (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{cell.getValue() ? (
<CheckIcon color="success" />
) : (
@ -89,12 +87,12 @@ const UserAgentsTable = () => {
</Box>
),
filterFn: (row, _columnId, activeFilterValue) => {
if (activeFilterValue == "all") return true; // Show all if no filter
return String(row.getValue("is_active")) === activeFilterValue;
if (activeFilterValue == 'all') return true; // Show all if no filter
return String(row.getValue('is_active')) === activeFilterValue;
},
},
],
[],
[]
);
//optionally access the underlying virtualizer instance
@ -117,7 +115,7 @@ const UserAgentsTable = () => {
};
useEffect(() => {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
setIsLoading(false);
}
}, []);
@ -148,7 +146,7 @@ const UserAgentsTable = () => {
rowVirtualizerInstanceRef, //optional
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
initialState: {
density: "compact",
density: 'compact',
},
enableRowActions: true,
renderRowActions: ({ row }) => (
@ -159,6 +157,7 @@ const UserAgentsTable = () => {
onClick={() => {
editUserAgent(row.original);
}}
sx={{ pt: 0, pb: 0 }}
>
<EditIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -166,6 +165,7 @@ const UserAgentsTable = () => {
size="small" // Makes the button smaller
color="error" // Red color for delete actions
onClick={() => deleteUserAgent(row.original.id)}
sx={{ pt: 0, pb: 0 }}
>
<DeleteIcon fontSize="small" /> {/* Small icon size */}
</IconButton>
@ -173,14 +173,14 @@ const UserAgentsTable = () => {
),
muiTableContainerProps: {
sx: {
height: "calc(42vh - 10px)",
height: 'calc(42vh - 10px)',
},
},
renderTopToolbarCustomActions: ({ table }) => (
<Stack
direction="row"
sx={{
alignItems: "center",
alignItems: 'center',
}}
>
<Typography>User-Agents</Typography>

View file

@ -1,14 +1,33 @@
export default {
defaultProperties: {
enableGlobalFilter: false,
enableBottomToolbar: false,
enableDensityToggle: false,
enableFullScreenToggle: false,
positionToolbarAlertBanner: "none",
columnFilterDisplayMode: "popover",
positionToolbarAlertBanner: 'none',
columnFilterDisplayMode: 'popover',
enableRowNumbers: false,
positionActionsColumn: "last",
positionActionsColumn: 'last',
initialState: {
density: "compact",
density: 'compact',
},
muiTableBodyCellProps: {
sx: {
padding: 0,
},
},
muiTableHeadCellProps: {
sx: {
padding: 0,
},
},
muiTableBodyProps: {
sx: {
//stripe the rows, make odd rows a darker color
'& tr:nth-of-type(odd) > td': {
backgroundColor: '#f5f5f5',
},
},
},
},
};

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

View file

@ -1,91 +1,317 @@
import React from 'react';
import { useEpg, Epg, Layout } from 'planby';
import React, { useMemo, useState, useEffect, useRef } from 'react';
import { Box, Typography, Avatar, Paper, Tooltip, Stack } from '@mui/material';
import dayjs from 'dayjs';
import API from '../api';
import useChannelsStore from '../store/channels';
function App() {
const [channels, setChannels] = React.useState([]);
const [epg, setEpg] = React.useState([]);
const CHANNEL_WIDTH = 100;
const PROGRAM_HEIGHT = 80;
const HOUR_WIDTH = 300;
const fetchChannels = async () => {
const channels = await API.getChannels();
const retval = [];
for (const channel of channels) {
if (!channel.tvg_id) {
continue;
}
console.log(channel);
retval.push({
uuid: channel.tvg_id,
type: 'channel',
title: channel.channel_name,
country: 'USA',
provider: channel.channel_group?.name || 'Default',
logo: channel.logo_url || '/images/logo.png',
year: 2025,
});
}
const TVChannelGuide = ({ startDate, endDate }) => {
const { channels } = useChannelsStore();
setChannels(retval);
return retval;
};
const [programs, setPrograms] = useState([]);
const [guideChannels, setGuideChannels] = useState([]);
const fetchEpg = async () => {
const programs = await API.getGrid();
const retval = [];
console.log(programs);
for (const program of programs.data) {
retval.push({
id: program.id,
channelUuid: 'Nickelodeon (East).us',
description: program.description,
title: program.title,
since: program.start_time,
till: program.end_time,
});
}
const guideRef = useRef(null);
setEpg(retval);
return retval;
};
const fetchData = async () => {
const channels = await fetchChannels();
const epg = await fetchEpg();
setChannels(channels);
setEpg(epg);
};
if (channels.length === 0) {
fetchData();
if (!channels || channels.length === 0) {
console.warn('No channels provided or empty channels array');
}
const formatDate = (date) => date.toISOString().split('T')[0] + 'T00:00:00';
useEffect(() => {
const fetchPrograms = async () => {
const programs = await API.getGrid();
const programIds = [...new Set(programs.map((prog) => prog.tvg_id))];
console.log(programIds);
const filteredChannels = channels.filter((ch) =>
programIds.includes(ch.tvg_id)
);
console.log(filteredChannels);
setGuideChannels(filteredChannels);
setPrograms(programs);
};
const today = new Date();
const tomorrow = new Date(today);
fetchPrograms();
}, [channels]);
const {
getEpgProps,
getLayoutProps,
onScrollToNow,
onScrollLeft,
onScrollRight,
} = useEpg({
epg,
channels,
startDate: '2025-02-25T11:00:00', // or 2022-02-02T00:00:00
width: '100%',
height: 600,
});
const now = dayjs();
const latestHalfHour = new Date();
// Round down the minutes to the nearest half hour
const minutes = latestHalfHour.getMinutes();
const roundedMinutes = minutes < 30 ? 0 : 30;
latestHalfHour.setMinutes(roundedMinutes);
latestHalfHour.setSeconds(0);
latestHalfHour.setMilliseconds(0);
const todayMidnight = dayjs().startOf('day');
const start = dayjs(startDate || todayMidnight);
const end = endDate ? dayjs(endDate) : start.add(24, 'hour');
const timeline = useMemo(() => {
// console.log('Generating timeline...');
const hours = [];
let current = start;
while (current.isBefore(end)) {
hours.push(current);
current = current.add(1, 'hour');
}
// console.log('Timeline generated:', hours);
return hours;
}, [start, end]);
useEffect(() => {
if (guideRef.current) {
const nowOffset = dayjs().diff(start, 'minute');
const scrollPosition = (nowOffset / 60) * HOUR_WIDTH - HOUR_WIDTH;
guideRef.current.scrollLeft = Math.max(scrollPosition, 0);
}
}, [programs, start]);
const renderProgram = (program, channelStart) => {
const programStart = dayjs(program.start_time);
const programEnd = dayjs(program.end_time);
const startOffset = programStart.diff(channelStart, 'minute');
const duration = programEnd.diff(programStart, 'minute');
const now = dayjs();
const isLive =
dayjs(program.start_time).isBefore(now) &&
dayjs(program.end_time).isAfter(now);
return (
// <Tooltip title={`${program.title} - ${program.description}`} arrow>
<Box
sx={{
position: 'absolute',
left: (startOffset / 60) * HOUR_WIDTH + 2,
width: (duration / 60) * HOUR_WIDTH - 4,
top: 2,
height: PROGRAM_HEIGHT - 4,
padding: 1,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
// borderLeft: '1px solid black',
borderRight: '1px solid black',
borderRadius: '8px',
color: 'primary.contrastText',
background: isLive
? 'linear-gradient(to right, #1a202c, #1a202c, #002eb3)'
: 'linear-gradient(to right, #1a202c, #1a202c)',
'&:hover': {
background: 'linear-gradient(to right, #051937, #002360)',
},
}}
>
<Typography
variant="body2"
noWrap
sx={{
fontWeight: 'bold',
}}
>
{program.title}
</Typography>
<Typography variant="overline" noWrap>
{programStart.format('h:mma')} - {programEnd.format('h:mma')}
</Typography>
</Box>
// </Tooltip>
);
};
const nowPosition = useMemo(() => {
if (now.isBefore(start) || now.isAfter(end)) return -1;
const totalMinutes = end.diff(start, 'minute');
const minutesSinceStart = now.diff(start, 'minute');
return (minutesSinceStart / totalMinutes) * (timeline.length * HOUR_WIDTH);
}, [now, start, end, timeline.length]);
return (
<div>
<Epg {...getEpgProps()}>
<Layout {...getLayoutProps()} />
</Epg>
</div>
);
}
<Box
sx={{
overflow: 'hidden',
width: '100%',
height: '100%',
backgroundColor: '#171923',
}}
>
<Typography variant="h6">
Channels length: {guideChannels?.length ?? 0}
</Typography>
export default App;
<Stack direction="row">
<Box>
{/* Channel Column */}
<Box
sx={{
width: CHANNEL_WIDTH,
height: '40px',
}}
/>
{guideChannels.map((channel, index) => {
return (
<Box
key={index}
sx={{
display: 'flex',
// borderTop: '1px solid #ccc',
height: PROGRAM_HEIGHT + 1,
alignItems: 'center',
}}
>
<Box
sx={{
width: CHANNEL_WIDTH,
display: 'flex',
padding: 1,
justifyContent: 'center',
}}
>
<Avatar
src={channel.logo_url || '/static/images/logo.png'}
alt={channel.channel_name}
/>
{/* <Typography variant="body2" sx={{ marginLeft: 1 }}>
{channel.channel_name}
</Typography> */}
</Box>
</Box>
);
})}
</Box>
{/* Timeline and Lineup */}
<Box sx={{ overflowY: 'auto', height: '100%' }}>
<Box
sx={{
display: 'flex',
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
}}
>
<Box sx={{ flex: 1, display: 'flex' }}>
{timeline.map((time, index) => (
<Box
key={time.format()}
sx={{
width: HOUR_WIDTH,
// borderLeft: '1px solid #ddd',
padding: 1,
backgroundColor: '#171923',
color: 'primary.contrastText',
height: '40px',
alignItems: 'center',
position: 'relative',
padding: 0,
}}
>
<Typography
component="span"
variant="body2"
sx={{
color: '#a0aec0',
position: 'absolute',
left: index == 0 ? 0 : '-18px',
}}
>
{time.format('h:mma')}
</Typography>
<Box
sx={{
height: '100%',
width: '100%',
display: 'grid',
alignItems: 'end',
'grid-template-columns': 'repeat(4, 1fr)',
}}
>
<Box
sx={{
width: '1px',
height: '10px',
marginRight: HOUR_WIDTH / 4 + 'px',
background: '#718096',
}}
></Box>
<Box
sx={{
width: '1px',
height: '10px',
marginRight: HOUR_WIDTH / 4 + 'px',
background: '#718096',
}}
></Box>
<Box
sx={{
width: '1px',
height: '10px',
marginRight: HOUR_WIDTH / 4 + 'px',
background: '#718096',
}}
></Box>
<Box
sx={{
width: '1px',
height: '10px',
marginRight: HOUR_WIDTH / 4 + 'px',
background: '#718096',
}}
></Box>
</Box>
</Box>
))}
</Box>
</Box>
<Box sx={{ position: 'relative' }}>
{nowPosition > 0 && (
<Box
className="now-position"
sx={{
position: 'absolute',
left: nowPosition,
top: 0,
bottom: 0,
width: '3px',
backgroundColor: 'rgb(44, 122, 123)',
zIndex: 15,
}}
/>
)}
{guideChannels.map((channel, index) => {
const channelPrograms = programs.filter(
(p) => p.tvg_id === channel.tvg_id
);
return (
<Box key={index} sx={{ display: 'flex' }}>
<Box
sx={{
flex: 1,
position: 'relative',
minHeight: PROGRAM_HEIGHT,
}}
>
{channelPrograms.map((program) =>
renderProgram(program, start)
)}
</Box>
</Box>
);
})}
</Box>
</Box>
</Stack>
</Box>
);
};
export default TVChannelGuide;

View file

View file

@ -1,38 +1,36 @@
export default {
Limiter: (concurrency, promiseList) => {
if (!promiseList || promiseList.length === 0) {
return Promise.resolve([]); // Return a resolved empty array if no promises
Limiter: (n, list) => {
if (!list || !list.length) {
return;
}
let index = 0; // Keeps track of the current promise to be processed
const results = []; // Stores the results of all promises
const totalPromises = promiseList.length;
var tail = list.splice(n);
var head = list;
var resolved = [];
var processed = 0;
// Helper function to process promises one by one, respecting concurrency
const processNext = () => {
// If we've processed all promises, resolve with the results
if (index >= totalPromises) {
return Promise.all(results);
}
// Execute the current promise and store the result
const currentPromise = promiseList[index]();
results.push(currentPromise);
// Once the current promise resolves, move on to the next one
return currentPromise.then(() => {
index++; // Move to the next promise
return processNext(); // Process the next promise
return new Promise(function (resolve) {
head.forEach(function (x) {
var res = x();
resolved.push(res);
res.then(function (y) {
runNext();
return y;
});
});
};
// Start processing promises up to the given concurrency
const concurrencyPromises = [];
for (let i = 0; i < concurrency && i < totalPromises; i++) {
concurrencyPromises.push(processNext());
}
// Wait for all promises to resolve
return Promise.all(concurrencyPromises).then(() => results);
}
}
function runNext() {
if (processed == tail.length) {
resolve(Promise.all(resolved));
} else {
resolved.push(
tail[processed]().then(function (x) {
runNext();
return x;
})
);
processed++;
}
}
});
},
};