Add pult frontend stuff

This commit is contained in:
2022-02-19 11:47:50 +01:00
parent 763aa23ca8
commit 62636fa2f9
15 changed files with 29069 additions and 0 deletions

107
pult/frontend/src/App.tsx Normal file
View 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;

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

View 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
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />