mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
more changes, table styling and compacting
This commit is contained in:
parent
febc908ad8
commit
5dc4e54585
19 changed files with 547 additions and 15648 deletions
31
frontend/package-lock.json
generated
31
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
0
frontend/src/pages/Guide2.js
Normal file
0
frontend/src/pages/Guide2.js
Normal 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++;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue