frontend tweaks, better UX with loading, added in websockets

This commit is contained in:
dekzter 2025-03-05 17:03:53 -05:00
parent fe8426df2e
commit 993ab0828f
10 changed files with 164 additions and 67 deletions

View file

@ -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
View 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;
};

View file

@ -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>

View file

@ -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"

View file

@ -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) => (

View file

@ -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>

View file

@ -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" />

View file

@ -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)) {

View file

@ -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 });
}
},

View file

@ -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 });
}
},