Add store in url for layout and all cameras

Signed-off-by: Damian Krysta <damian@krysta.dev>
This commit is contained in:
Damian Krysta 2022-01-31 22:55:14 +01:00 committed by Scott Lamb
parent 552f6bf19d
commit 56918bf5c2
3 changed files with 41 additions and 30 deletions

View File

@ -11,7 +11,7 @@ import { useSnackbars } from "./snackbars";
import { Camera, Session } from "./types"; import { Camera, Session } from "./types";
import ListActivity from "./List"; import ListActivity from "./List";
import AppBar from "@mui/material/AppBar"; import AppBar from "@mui/material/AppBar";
import {BrowserRouter, Routes, Route, Link} from "react-router-dom"; import {Routes, Route, Link, useSearchParams, useResolvedPath, useMatch} from "react-router-dom";
import LiveActivity, { MultiviewChooser } from "./Live"; import LiveActivity, { MultiviewChooser } from "./Live";
import Drawer from "@mui/material/Drawer"; import Drawer from "@mui/material/Drawer";
import List from "@mui/material/List"; import List from "@mui/material/List";
@ -34,12 +34,16 @@ type Activity = "list" | "live";
function App() { function App() {
const [showMenu, toggleShowMenu] = useReducer((m: boolean) => !m, false); const [showMenu, toggleShowMenu] = useReducer((m: boolean) => !m, false);
const [searchParams, setSearchParams] = useSearchParams();
const [showListSelectors, toggleShowListSelectors] = useReducer( const [showListSelectors, toggleShowListSelectors] = useReducer(
(m: boolean) => !m, (m: boolean) => !m,
true true
); );
const [activity, setActivity] = useState<Activity>("list"); let resolved = useResolvedPath('live');
const [multiviewLayoutIndex, setMultiviewLayoutIndex] = useState(0); let match = useMatch({ path: resolved.pathname, end: true });
const [activity, setActivity] = useState<Activity>(match ? "live" : "list");
const [multiviewLayoutIndex, setMultiviewLayoutIndex] = useState(Number.parseInt(searchParams.get('layout') || '0', 10));
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Session | null>(null);
const [cameras, setCameras] = useState<Camera[] | null>(null); const [cameras, setCameras] = useState<Camera[] | null>(null);
const [timeZoneName, setTimeZoneName] = useState<string | null>(null); const [timeZoneName, setTimeZoneName] = useState<string | null>(null);
@ -145,14 +149,17 @@ function App() {
activityMenu = ( activityMenu = (
<MultiviewChooser <MultiviewChooser
layoutIndex={multiviewLayoutIndex} layoutIndex={multiviewLayoutIndex}
onChoice={setMultiviewLayoutIndex} onChoice={(value) => {
setMultiviewLayoutIndex(value)
setSearchParams({layout: value.toString()}, )
}}
/> />
); );
break; break;
} }
} }
return ( return (
<BrowserRouter> <>
<AppBar position="static"> <AppBar position="static">
<MoonfireMenu <MoonfireMenu
loginState={loginState} loginState={loginState}
@ -174,18 +181,17 @@ function App() {
}} }}
> >
<List> <List>
<ListItem button key="list" onClick={() => clickActivity("list")}> <ListItem button key="list" onClick={() => clickActivity("list")} component={Link} to="/">
<ListItemIcon> <ListItemIcon>
<ListIcon /> <ListIcon />
</ListItemIcon> </ListItemIcon>
<Link to="/"><ListItemText primary="List view" /></Link> <ListItemText primary="List view" />
</ListItem> </ListItem>
<ListItem button key="live" onClick={() => clickActivity("live")}> <ListItem button key="live" onClick={() => clickActivity("live")} component={Link} to="/live">
<ListItemIcon> <ListItemIcon>
<Videocam /> <Videocam />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Live view (experimental)" />
<Link to="/live"><ListItemText primary="Live view (experimental)" /></Link>
</ListItem> </ListItem>
</List> </List>
</Drawer> </Drawer>
@ -211,10 +217,10 @@ function App() {
</p> </p>
</Container> </Container>
)} )}
<Routes > <Routes>
{fetchedCameras(cameras)} {fetchedCameras(cameras)}
</Routes> </Routes>
</BrowserRouter> </>
); );
} }

View File

@ -4,7 +4,7 @@
import Select, { SelectChangeEvent } from "@mui/material/Select"; import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import React, { useReducer } from "react"; import React, {useReducer} from "react";
import { Camera } from "../types"; import { Camera } from "../types";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
@ -91,13 +91,13 @@ export const MultiviewChooser = (props: MultiviewChooserProps) => {
<Select <Select
id="layout" id="layout"
value={props.layoutIndex} value={props.layoutIndex}
onChange={(e) => onChange={(e) => {
props.onChoice( props.onChoice(
typeof e.target.value === "string" typeof e.target.value === "string"
? parseInt(e.target.value) ? parseInt(e.target.value)
: e.target.value : e.target.value
) )
} }}
size="small" size="small"
sx={{ sx={{
// Hacky attempt to style for the app menu. // Hacky attempt to style for the app menu.
@ -130,6 +130,7 @@ interface SelectOp {
cameraIndex: number | null; cameraIndex: number | null;
} }
function selectedReducer(old: SelectedCameras, op: SelectOp): SelectedCameras { function selectedReducer(old: SelectedCameras, op: SelectOp): SelectedCameras {
let selected = [...old]; // shallow clone. let selected = [...old]; // shallow clone.
if (op.cameraIndex !== null) { if (op.cameraIndex !== null) {
@ -151,15 +152,17 @@ function selectedReducer(old: SelectedCameras, op: SelectOp): SelectedCameras {
* as possible. * as possible.
*/ */
const Multiview = (props: MultiviewProps) => { const Multiview = (props: MultiviewProps) => {
const [searchParams, setSearchParams] = useSearchParams();
const [selected, updateSelected] = useReducer( const [selected, updateSelected] = useReducer(
selectedReducer, selectedReducer,
Array(MAX_CAMERAS).fill(null) searchParams.has('cams') ? JSON.parse(searchParams.get('cams') || "") : Array(MAX_CAMERAS).fill(null)
); );
const outerRef = React.useRef<HTMLDivElement>(null);
const outerRef = React.useRef<HTMLDivElement>(null);
const classes = useStyles(); const classes = useStyles();
const layout = LAYOUTS[props.layoutIndex]; const layout = LAYOUTS[props.layoutIndex];
const monoviews = selected.slice(0, layout.cameras).map((e, i) => { const monoviews = selected.slice(0, layout.cameras).map((e, i) => {
// When a camera is selected, use the camera's index as the key. // When a camera is selected, use the camera's index as the key.
// This allows swapping cameras' positions without tearing down their // This allows swapping cameras' positions without tearing down their
@ -168,18 +171,24 @@ const Multiview = (props: MultiviewProps) => {
// When no camera is selected, use the index within selected. (Actually, // When no camera is selected, use the index within selected. (Actually,
// -1 minus the index, to disambiguate between the two cases.) // -1 minus the index, to disambiguate between the two cases.)
const key = e ?? -1 - i; const key = e ?? -1 - i;
return ( return (
<Monoview <Monoview
key={key} key={key}
cameras={props.cameras} cameras={props.cameras}
cameraIndex={e} cameraIndex={e}
renderCamera={props.renderCamera} renderCamera={props.renderCamera}
onSelect={(cameraIndex) => onSelect={(cameraIndex) => {
updateSelected({ selectedIndex: i, cameraIndex }) updateSelected({selectedIndex: i, cameraIndex})
} searchParams.set('cams', JSON.stringify(selectedReducer(selected, {selectedIndex: i, cameraIndex})))
setSearchParams(searchParams)
}}
/> />
); );
}); });
return ( return (
<div className={classes.root} ref={outerRef}> <div className={classes.root} ref={outerRef}>
<div className="mid"> <div className="mid">
@ -200,24 +209,17 @@ interface MonoviewProps {
/** A single pane of a Multiview, including its camera chooser. */ /** A single pane of a Multiview, including its camera chooser. */
const Monoview = (props: MonoviewProps) => { const Monoview = (props: MonoviewProps) => {
const [searchParams, setSearchParams] = useSearchParams();
const handleChange = (event: SelectChangeEvent<number | null>) => { const handleChange = (event: SelectChangeEvent<number | null>) => {
const { const {
target: { value }, target: { value },
} = event; } = event;
if (value !== null && value !== undefined) {
setSearchParams(new URLSearchParams({cam: value.toString()}));
} else {
setSearchParams(new URLSearchParams({ }))
}
props.onSelect(typeof value === "string" ? parseInt(value) : value); props.onSelect(typeof value === "string" ? parseInt(value) : value);
}; };
const fromQueryIndexOrNull = searchParams.has('cam') ? Number.parseInt(searchParams.get('cam') as string, 10) : null;
const chooser = ( const chooser = (
<Select <Select
value={props.cameraIndex == null ? fromQueryIndexOrNull: props.cameraIndex} value={props.cameraIndex == null ? null: props.cameraIndex}
onChange={handleChange} onChange={handleChange}
displayEmpty displayEmpty
size="small" size="small"
@ -238,7 +240,7 @@ const Monoview = (props: MonoviewProps) => {
</Select> </Select>
); );
return props.renderCamera( return props.renderCamera(
props.cameraIndex === null ? fromQueryIndexOrNull === null ? null : props.cameras[fromQueryIndexOrNull] : props.cameras[props.cameraIndex], props.cameraIndex === null ? null : props.cameras[props.cameraIndex],
chooser chooser
); );
}; };

View File

@ -14,6 +14,7 @@ import ErrorBoundary from "./ErrorBoundary";
import { SnackbarProvider } from "./snackbars"; import { SnackbarProvider } from "./snackbars";
import AdapterDateFns from "@mui/lab/AdapterDateFns"; import AdapterDateFns from "@mui/lab/AdapterDateFns";
import "./index.css"; import "./index.css";
import { BrowserRouter } from "react-router-dom";
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
@ -34,7 +35,9 @@ ReactDOM.render(
<ErrorBoundary> <ErrorBoundary>
<LocalizationProvider dateAdapter={AdapterDateFns}> <LocalizationProvider dateAdapter={AdapterDateFns}>
<SnackbarProvider autoHideDuration={5000}> <SnackbarProvider autoHideDuration={5000}>
<BrowserRouter >
<App /> <App />
</BrowserRouter>
</SnackbarProvider> </SnackbarProvider>
</LocalizationProvider> </LocalizationProvider>
</ErrorBoundary> </ErrorBoundary>