Add pult frontend stuff
This commit is contained in:
107
pult/frontend/src/App.tsx
Normal file
107
pult/frontend/src/App.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState, useMemo, createContext, useContext } from "react";
|
||||
|
||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import blueGrey from "@mui/material/colors/blueGrey"
|
||||
import teal from "@mui/material/colors/teal";
|
||||
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||
import Container from "@mui/material/Container";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Sliders from "./Sliders";
|
||||
|
||||
// Light / Dark mode
|
||||
|
||||
type ThemeMode = 'system' | 'light' | 'dark';
|
||||
const ThemeModeContext = createContext((_: ThemeMode) => { })
|
||||
|
||||
const App: React.FC = () => {
|
||||
|
||||
const systemDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
|
||||
const [themeMode, setThemeMode] = useState<ThemeMode>('system');
|
||||
|
||||
const theme = useMemo(
|
||||
() => createTheme({
|
||||
palette: {
|
||||
mode: themeMode === 'system' ? (systemDarkMode ? 'dark' : 'light') : themeMode,
|
||||
primary: blueGrey,
|
||||
secondary: teal,
|
||||
}
|
||||
}),
|
||||
[themeMode, systemDarkMode],
|
||||
);
|
||||
|
||||
return <ThemeModeContext.Provider value={setThemeMode}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Layout />
|
||||
</ThemeProvider>
|
||||
</ThemeModeContext.Provider>
|
||||
}
|
||||
|
||||
// Layout
|
||||
|
||||
const Layout: React.FC = () => {
|
||||
return <>
|
||||
<TopBar />
|
||||
<main>
|
||||
<Box sx={{
|
||||
pt: 8,
|
||||
pb: 6,
|
||||
}}>
|
||||
<Container>
|
||||
<Sliders />
|
||||
</Container>
|
||||
</Box>
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
|
||||
// Top Bar
|
||||
|
||||
const TopBar: React.FC = () => {
|
||||
|
||||
const setThemeMode = useContext(ThemeModeContext);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return <AppBar position="relative">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||
DMX Controllinator
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) }}
|
||||
color="inherit"
|
||||
>
|
||||
<Brightness4Icon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => { setAnchorEl(null) }}
|
||||
>
|
||||
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('light'); }}>Light</MenuItem>
|
||||
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('system'); }}>System</MenuItem>
|
||||
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('dark'); }}>Dark</MenuItem>
|
||||
</Menu>
|
||||
<Button variant="contained" href="docs">
|
||||
API
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar >
|
||||
}
|
||||
|
||||
// Content
|
||||
|
||||
|
||||
export default App;
|
||||
120
pult/frontend/src/Sliders.tsx
Normal file
120
pult/frontend/src/Sliders.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import MuiSlider from "@mui/material/Slider";
|
||||
import React from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
type StateItem = {
|
||||
value: number,
|
||||
status: "open" | "owned" | "locked",
|
||||
};
|
||||
|
||||
type State = Array<StateItem>;
|
||||
|
||||
type ClientAction = {
|
||||
action_type: "grab" | "release",
|
||||
slider: number,
|
||||
} | {
|
||||
action_type: "move",
|
||||
slider: number,
|
||||
new_value: number,
|
||||
}
|
||||
|
||||
const Sliders: React.FC = () => {
|
||||
const ws = useRef<WebSocket>();
|
||||
const reconnectInterval = useRef<number>();
|
||||
|
||||
const [state, setState] = useState<State>();
|
||||
|
||||
const connect = () => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
||||
if (ws.current !== undefined && ws.current.readyState !== 3) return;
|
||||
|
||||
const wsURL = new URL("ws", window.location.href);
|
||||
wsURL.protocol = wsURL.protocol.replace("http", "ws");
|
||||
ws.current = new WebSocket(wsURL.href);
|
||||
|
||||
ws.current.onmessage = (ev) => {
|
||||
setState(JSON.parse(ev.data));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
reconnectInterval.current = window.setInterval(connect, 1000);
|
||||
|
||||
return () => {
|
||||
if (reconnectInterval.current !== undefined) window.clearInterval(reconnectInterval.current);
|
||||
if (ws.current !== undefined) ws.current.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const cb = (action: ClientAction) => {
|
||||
if (ws.current !== undefined && ws.current.readyState !== 3) {
|
||||
ws.current.send(JSON.stringify({
|
||||
action: action,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
{state?.map((item, index) => <Slider key={index} item={item} index={index} cb={cb} />)}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
const styleOverride = {
|
||||
"& .MuiSlider-track": { transition: "none" },
|
||||
"& .MuiSlider-thumb": { transition: "none" },
|
||||
};
|
||||
|
||||
const Slider: React.FC<{
|
||||
item: StateItem,
|
||||
index: number,
|
||||
cb: (action: ClientAction) => void,
|
||||
}> = ({ item, index, cb }) => {
|
||||
|
||||
const disabled = item.status === "locked";
|
||||
const [value, setValue] = useState(item.value);
|
||||
|
||||
useEffect(() => {
|
||||
if (item.status !== "owned") setValue(item.value);
|
||||
}, [item]);
|
||||
|
||||
const onChange = (n: number) => {
|
||||
setValue(n);
|
||||
cb({
|
||||
action_type: "move",
|
||||
slider: index,
|
||||
new_value: n,
|
||||
});
|
||||
};
|
||||
|
||||
const onGrab = () => {
|
||||
cb({
|
||||
action_type: "grab",
|
||||
slider: index,
|
||||
});
|
||||
};
|
||||
|
||||
const onRelease = () => {
|
||||
cb({
|
||||
action_type: "release",
|
||||
slider: index,
|
||||
});
|
||||
};
|
||||
|
||||
return <MuiSlider
|
||||
min={0}
|
||||
max={255}
|
||||
sx={disabled ? styleOverride : {}}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={(_, n) => { onChange(n as number) }}
|
||||
onMouseDown={onGrab}
|
||||
onTouchStart={onGrab}
|
||||
onMouseUp={onRelease}
|
||||
onTouchEnd={onRelease}
|
||||
/>
|
||||
};
|
||||
|
||||
export default Sliders;
|
||||
15
pult/frontend/src/index.tsx
Normal file
15
pult/frontend/src/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
1
pult/frontend/src/react-app-env.d.ts
vendored
Normal file
1
pult/frontend/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
Reference in New Issue
Block a user