mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
frontend tweaks, better UX with loading, added in websockets
This commit is contained in:
parent
fe8426df2e
commit
993ab0828f
10 changed files with 164 additions and 67 deletions
|
|
@ -22,6 +22,7 @@ import useAuthStore from './store/auth';
|
|||
import Alert from './components/Alert';
|
||||
import FloatingVideo from './components/FloatingVideo';
|
||||
import SuperuserForm from './components/forms/SuperuserForm';
|
||||
import { WebsocketProvider } from './WebSocket';
|
||||
|
||||
const drawerWidth = 240;
|
||||
const miniDrawerWidth = 60;
|
||||
|
|
@ -79,60 +80,65 @@ const App = () => {
|
|||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
<Sidebar
|
||||
open={open}
|
||||
miniDrawerWidth={miniDrawerWidth}
|
||||
drawerWidth={drawerWidth}
|
||||
toggleDrawer={toggleDrawer}
|
||||
/>
|
||||
<WebsocketProvider>
|
||||
<Router>
|
||||
<Sidebar
|
||||
open={open}
|
||||
miniDrawerWidth={miniDrawerWidth}
|
||||
drawerWidth={drawerWidth}
|
||||
toggleDrawer={toggleDrawer}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
backgroundColor: '#495057',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
backgroundColor: '#495057',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Route path="/channels" element={<Channels />} />
|
||||
<Route path="/m3u" element={<M3U />} />
|
||||
<Route path="/epg" element={<EPG />} />
|
||||
<Route path="/stream-profiles" element={<StreamProfiles />} />
|
||||
<Route path="/guide" element={<Guide />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</>
|
||||
) : (
|
||||
<Route path="/login" element={<Login />} />
|
||||
)}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Navigate
|
||||
to={isAuthenticated ? defaultRoute : '/login'}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Route path="/channels" element={<Channels />} />
|
||||
<Route path="/m3u" element={<M3U />} />
|
||||
<Route path="/epg" element={<EPG />} />
|
||||
<Route
|
||||
path="/stream-profiles"
|
||||
element={<StreamProfiles />}
|
||||
/>
|
||||
<Route path="/guide" element={<Guide />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</>
|
||||
) : (
|
||||
<Route path="/login" element={<Login />} />
|
||||
)}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Navigate
|
||||
to={isAuthenticated ? defaultRoute : '/login'}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Router>
|
||||
<Alert />
|
||||
<FloatingVideo />
|
||||
</Router>
|
||||
<Alert />
|
||||
<FloatingVideo />
|
||||
</WebsocketProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
78
frontend/src/WebSocket.js
Normal file
78
frontend/src/WebSocket.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
createContext,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import useStreamsStore from './store/streams';
|
||||
import useAlertStore from './store/alerts';
|
||||
|
||||
export const WebsocketContext = createContext(false, null, () => {});
|
||||
|
||||
export const WebsocketProvider = ({ children }) => {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [val, setVal] = useState(null);
|
||||
|
||||
const { showAlert } = useAlertStore();
|
||||
|
||||
const ws = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
let wsUrl = `ws://${window.location.host}/ws/`;
|
||||
if (process.env.REACT_APP_ENV_MODE == 'dev') {
|
||||
wsUrl = `ws://${window.location.hostname}:8001/ws/`;
|
||||
}
|
||||
|
||||
const socket = new WebSocket(wsUrl);
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('websocket connected');
|
||||
setIsReady(true);
|
||||
};
|
||||
|
||||
// Reconnection logic
|
||||
socket.onclose = () => {
|
||||
setIsReady(false);
|
||||
setTimeout(() => {
|
||||
const reconnectWs = new WebSocket(wsUrl);
|
||||
reconnectWs.onopen = () => setIsReady(true);
|
||||
}, 3000); // Attempt to reconnect every 3 seconds
|
||||
};
|
||||
|
||||
socket.onmessage = async (event) => {
|
||||
event = JSON.parse(event.data);
|
||||
switch (event.type) {
|
||||
case 'm3u_refresh':
|
||||
if (event.message?.success) {
|
||||
useStreamsStore.getState().fetchStreams();
|
||||
showAlert(event.message.message, 'success');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown websocket event type: ${event.type}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.current = socket;
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ret = [isReady, val, ws.current?.send.bind(ws.current)];
|
||||
|
||||
return (
|
||||
<WebsocketContext.Provider value={ret}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const socket = useContext(WebsocketContext);
|
||||
return socket;
|
||||
};
|
||||
|
|
@ -100,7 +100,7 @@ const Sidebar = ({ open, miniDrawerWidth, drawerWidth, toggleDrawer }) => {
|
|||
</Box>
|
||||
|
||||
{isAuthenticated && (
|
||||
<Box sx={{ p: 2, borderTop: '1px solid #ccc' }}>
|
||||
<Box sx={{ flexGrow: 1, borderTop: '1px solid #ccc' }}>
|
||||
<List>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const LoginForm = () => {
|
|||
justifyContent="center"
|
||||
direction="column"
|
||||
>
|
||||
<Grid2 item xs={12}>
|
||||
<Grid2 xs={12}>
|
||||
<TextField
|
||||
label="Username"
|
||||
variant="standard"
|
||||
|
|
@ -78,7 +78,7 @@ const LoginForm = () => {
|
|||
size="small"
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12}>
|
||||
<Grid2 xs={12}>
|
||||
<TextField
|
||||
label="Password"
|
||||
variant="standard"
|
||||
|
|
@ -91,7 +91,7 @@ const LoginForm = () => {
|
|||
size="small"
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12}>
|
||||
<Grid2 xs={12}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
|
|||
error={
|
||||
formik.touched.user_agent && Boolean(formik.errors.user_agent)
|
||||
}
|
||||
helperText={formik.touched.user_agent && formik.errors.user_agent}
|
||||
// helperText={formik.touched.user_agent && formik.errors.user_agent}
|
||||
variant="standard"
|
||||
>
|
||||
{userAgents.map((option, index) => (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
// frontend/src/components/forms/SuperuserForm.js
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Box, Paper, Typography, Grid as Grid2, TextField, Button } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
Grid2,
|
||||
TextField,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
|
||||
function SuperuserForm({ onSuccess }) {
|
||||
const [formData, setFormData] = useState({
|
||||
|
|
@ -58,8 +65,13 @@ function SuperuserForm({ onSuccess }) {
|
|||
</Typography>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Grid2 container spacing={2} justifyContent="center" direction="column">
|
||||
<Grid2 item xs={12}>
|
||||
<Grid2
|
||||
container
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
direction="column"
|
||||
>
|
||||
<Grid2 xs={12}>
|
||||
<TextField
|
||||
label="Username"
|
||||
variant="standard"
|
||||
|
|
@ -71,7 +83,7 @@ function SuperuserForm({ onSuccess }) {
|
|||
size="small"
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12}>
|
||||
<Grid2 xs={12}>
|
||||
<TextField
|
||||
label="Password"
|
||||
variant="standard"
|
||||
|
|
@ -84,7 +96,7 @@ function SuperuserForm({ onSuccess }) {
|
|||
size="small"
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12}>
|
||||
<Grid2 xs={12}>
|
||||
<TextField
|
||||
label="Email (optional)"
|
||||
variant="standard"
|
||||
|
|
@ -96,8 +108,13 @@ function SuperuserForm({ onSuccess }) {
|
|||
size="small"
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12}>
|
||||
<Button type="submit" variant="contained" color="primary" fullWidth>
|
||||
<Grid2 xs={12}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
>
|
||||
Create Superuser
|
||||
</Button>
|
||||
</Grid2>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ const StreamsTable = () => {
|
|||
size="small"
|
||||
color="warning"
|
||||
onClick={() => editStream(row.original)}
|
||||
disabled={row.original.m3u_account}
|
||||
disabled={row.original.m3u_account ? true : false}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ const useAuthStore = create((set, get) => ({
|
|||
setIsAuthenticated: (isAuthenticated) => set({ isAuthenticated }),
|
||||
|
||||
initData: async () => {
|
||||
console.log('fetching data');
|
||||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
useChannelsStore.getState().fetchChannelGroups(),
|
||||
|
|
@ -43,7 +42,6 @@ const useAuthStore = create((set, get) => ({
|
|||
},
|
||||
|
||||
getToken: async () => {
|
||||
const expiration = localStorage.getItem('tokenExpiration');
|
||||
const tokenExpiration = localStorage.getItem('tokenExpiration');
|
||||
let accessToken = null;
|
||||
if (isTokenExpired(tokenExpiration)) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ const useSettingsStore = create((set) => ({
|
|||
try {
|
||||
const settings = await api.getSettings();
|
||||
const env = await api.getEnvironmentSettings();
|
||||
console.log(env);
|
||||
set({
|
||||
settings: settings.reduce((acc, setting) => {
|
||||
acc[setting.key] = setting;
|
||||
|
|
@ -22,7 +21,6 @@ const useSettingsStore = create((set) => ({
|
|||
environment: env,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
set({ error: 'Failed to load settings.', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from "zustand";
|
||||
import api from "../api";
|
||||
import { create } from 'zustand';
|
||||
import api from '../api';
|
||||
|
||||
const useStreamsStore = create((set) => ({
|
||||
streams: [],
|
||||
|
|
@ -12,8 +12,8 @@ const useStreamsStore = create((set) => ({
|
|||
const streams = await api.getStreams();
|
||||
set({ streams: streams, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch streams:", error);
|
||||
set({ error: "Failed to load streams.", isLoading: false });
|
||||
console.error('Failed to fetch streams:', error);
|
||||
set({ error: 'Failed to load streams.', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue