Stop using deprecated @mui/styles

I was struggling to upgrade the version of mui stuff (material and date
picker). I'm hoping getting rid of the deprecated stuff eases this a
bit.

I don't love that I can't just use sx on plain HTML stuff and have to
wrap it in Box, but oh well. Looks like I'm not alone, fwiw.
https://github.com/mui/material-ui/issues/23220
This commit is contained in:
Scott Lamb 2022-10-01 08:39:27 -07:00
parent ae502200c0
commit 8d716bf4dd
7 changed files with 20889 additions and 469 deletions

21045
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,6 @@
"@mui/icons-material": "^5.5.1",
"@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.5.1",
"@mui/styles": "^5.5.1",
"@react-hook/resize-observer": "^1.2.5",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",

View File

@ -6,25 +6,14 @@ import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { Theme } from "@mui/material/styles";
import { createStyles, makeStyles } from "@mui/styles";
import { useTheme } from "@mui/material/styles";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import AccountCircle from "@mui/icons-material/AccountCircle";
import MenuIcon from "@mui/icons-material/Menu";
import React from "react";
import { LoginState } from "./App";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
title: {
flexGrow: 1,
},
activity: {
marginRight: theme.spacing(2),
},
})
);
import Box from "@mui/material/Box";
interface Props {
loginState: LoginState;
@ -36,7 +25,7 @@ interface Props {
// https://material-ui.com/components/app-bar/
function MoonfireMenu(props: Props) {
const classes = useStyles();
const theme = useTheme();
const [accountMenuAnchor, setAccountMenuAnchor] =
React.useState<null | HTMLElement>(null);
@ -66,11 +55,13 @@ function MoonfireMenu(props: Props) {
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Moonfire NVR
</Typography>
{props.activityMenuPart !== null && (
<div className={classes.activity}>{props.activityMenuPart}</div>
<Box sx={{ marginRight: theme.spacing(2) }}>
{props.activityMenuPart}
</Box>
)}
{props.loginState !== "unknown" && props.loginState !== "logged-in" && (
<Button color="inherit" onClick={props.requestLogin}>

View File

@ -2,11 +2,11 @@
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import { Camera, Stream, StreamType } from "../types";
import Checkbox from "@mui/material/Checkbox";
import { useTheme } from "@mui/material/styles";
import { makeStyles } from "@mui/styles";
import { ToplevelResponse } from "../api";
interface Props {
@ -15,30 +15,9 @@ interface Props {
setSelected: (selected: Set<number>) => void;
}
const useStyles = makeStyles({
streamSelectorTable: {
fontSize: "0.9rem",
"& td:first-child": {
paddingRight: "3px",
},
"& td:not(:first-child)": {
textAlign: "center",
},
},
check: {
padding: "3px",
},
"@media (pointer: fine)": {
check: {
padding: "0px",
},
},
});
/** Returns a table which allows selecting zero or more streams. */
const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
const theme = useTheme();
const classes = useStyles();
const setStream = (s: Stream, checked: boolean) => {
const updated = new Set(selected);
if (checked) {
@ -92,13 +71,10 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
function checkbox(st: StreamType) {
const s = c.streams[st];
if (s === undefined) {
return (
<Checkbox className={classes.check} color="secondary" disabled />
);
return <Checkbox color="secondary" disabled />;
}
return (
<Checkbox
className={classes.check}
size="small"
checked={selected.has(s.id)}
color="secondary"
@ -120,7 +96,26 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
padding: theme.spacing(1),
}}
>
<table className={classes.streamSelectorTable}>
<Box
component="table"
sx={{
fontSize: "0.9rem",
"& td:first-child": {
paddingRight: "3px",
},
"& td:not(:first-child)": {
textAlign: "center",
},
"& .MuiCheckbox-root": {
padding: "3px",
},
"@media (pointer: fine)": {
"& .MuiCheckbox-root": {
padding: "0px",
},
},
}}
>
<thead>
<tr>
<td />
@ -129,7 +124,7 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
</tr>
</thead>
<tbody>{cameraRows}</tbody>
</table>
</Box>
</Card>
);
};

View File

@ -5,8 +5,7 @@
import Box from "@mui/material/Box";
import Modal from "@mui/material/Modal";
import Paper from "@mui/material/Paper";
import { Theme } from "@mui/material/styles";
import { makeStyles } from "@mui/styles";
import { useTheme } from "@mui/material/styles";
import Table from "@mui/material/Table";
import TableContainer from "@mui/material/TableContainer";
import utcToZonedTime from "date-fns-tz/utcToZonedTime";
@ -26,56 +25,6 @@ import { FrameProps } from "../App";
import IconButton from "@mui/material/IconButton";
import FilterList from "@mui/icons-material/FilterList";
const useStyles = makeStyles((theme: Theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
margin: theme.spacing(2),
},
selectors: {
width: "max-content",
"& .MuiCard-root": {
marginRight: theme.spacing(2),
marginBottom: theme.spacing(2),
},
},
videoTable: {
flexGrow: 1,
width: "max-content",
height: "max-content",
"& .streamHeader": {
background: theme.palette.primary.light,
color: theme.palette.primary.contrastText,
},
"& .MuiTableBody-root:not(:last-child):after": {
content: "''",
display: "block",
height: theme.spacing(2),
},
"& tbody .recording": {
cursor: "pointer",
},
"& .opt": {
[theme.breakpoints.down("lg")]: {
display: "none",
},
},
},
// When there's a video modal up, make the content as large as possible
// without distorting it. Center it in the screen and ensure that the video
// element only takes up the space actually used by the content, so that
// clicking outside it will dismiss the modal.
videoModal: {
display: "flex",
alignItems: "center",
justifyContent: "center",
"& video": {
objectFit: "fill",
},
},
}));
interface FullScreenVideoProps {
src: string;
aspect: [number, number];
@ -225,7 +174,7 @@ const calcSelectedStreams = (
};
const Main = ({ toplevel, timeZoneName, Frame }: Props) => {
const classes = useStyles();
const theme = useTheme();
const {
selectedStreamIds,
@ -288,7 +237,31 @@ const Main = ({ toplevel, timeZoneName, Frame }: Props) => {
setActiveRecording(null);
};
const recordingsTable = (
<TableContainer component={Paper} className={classes.videoTable}>
<TableContainer
component={Paper}
sx={{
flexGrow: 1,
width: "max-content",
height: "max-content",
"& .streamHeader": {
background: theme.palette.primary.light,
color: theme.palette.primary.contrastText,
},
"& .MuiTableBody-root:not(:last-child):after": {
content: "''",
display: "block",
height: theme.spacing(2),
},
"& tbody .recording": {
cursor: "pointer",
},
"& .opt": {
[theme.breakpoints.down("lg")]: {
display: "none",
},
},
}}
>
<Table size="small">{videoLists}</Table>
</TableContainer>
);
@ -305,10 +278,22 @@ const Main = ({ toplevel, timeZoneName, Frame }: Props) => {
</IconButton>
}
>
<div className={classes.root}>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
margin: theme.spacing(2),
}}
>
<Box
className={classes.selectors}
sx={{ display: showSelectors ? "block" : "none" }}
sx={{
display: showSelectors ? "block" : "none",
width: "max-content",
"& .MuiCard-root": {
marginRight: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}}
>
<StreamMultiSelector
toplevel={toplevel}
@ -331,7 +316,22 @@ const Main = ({ toplevel, timeZoneName, Frame }: Props) => {
</Box>
{videoLists.length > 0 && recordingsTable}
{activeRecording != null && (
<Modal open onClose={closeModal} className={classes.videoModal}>
<Modal
open
onClose={closeModal}
sx={{
// Make the content as large as possible without distorting it.
// Center it in the screen and ensure that the video element only
// takes up the space actually used by the content, so that clicking
// outside it will dismiss the modal.
display: "flex",
alignItems: "center",
justifyContent: "center",
"& video": {
objectFit: "fill",
},
}}
>
<FullScreenVideo
src={api.recordingUrl(
activeRecording[0].camera.uuid,
@ -347,7 +347,7 @@ const Main = ({ toplevel, timeZoneName, Frame }: Props) => {
/>
</Modal>
)}
</div>
</Box>
</Frame>
);
};

View File

@ -2,12 +2,12 @@
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
import Box from "@mui/material/Box";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import React, { useReducer } from "react";
import { Camera } from "../types";
import { makeStyles } from "@mui/styles";
import { Theme } from "@mui/material/styles";
import { useTheme } from "@mui/material/styles";
import { useSearchParams } from "react-router-dom";
export interface Layout {
@ -24,52 +24,6 @@ const LAYOUTS: Layout[] = [
];
const MAX_CAMERAS = 9;
const useStyles = makeStyles((theme: Theme) => ({
root: {
flex: "1 0 0",
color: "white",
marginTop: theme.spacing(2),
overflow: "hidden",
// TODO: this mid-level div can probably be removed.
"& .mid": {
width: "100%",
height: "100%",
position: "relative",
display: "inline-block",
},
},
inner: {
// match parent's size without influencing it.
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "#000",
overflow: "hidden",
display: "grid",
gridGap: "0px",
// These class names must match LAYOUTS (above).
"&.solo": {
gridTemplateColumns: "100%",
gridTemplateRows: "100%",
},
"&.two-by-two": {
gridTemplateColumns: "repeat(2, calc(100% / 2))",
gridTemplateRows: "repeat(2, calc(100% / 2))",
},
"&.main-plus-five, &.three-by-three": {
gridTemplateColumns: "repeat(3, calc(100% / 3))",
gridTemplateRows: "repeat(3, calc(100% / 3))",
},
"&.main-plus-five > div:nth-child(1)": {
gridColumn: "span 2",
gridRow: "span 2",
},
},
}));
export interface MultiviewProps {
cameras: Camera[];
layoutIndex: number;
@ -152,6 +106,7 @@ function selectedReducer(old: SelectedCameras, op: SelectOp): SelectedCameras {
*/
const Multiview = (props: MultiviewProps) => {
const [searchParams, setSearchParams] = useSearchParams();
const theme = useTheme();
const [selected, updateSelected] = useReducer(
selectedReducer,
@ -161,7 +116,6 @@ const Multiview = (props: MultiviewProps) => {
);
const outerRef = React.useRef<HTMLDivElement>(null);
const classes = useStyles();
const layout = LAYOUTS[props.layoutIndex];
const monoviews = selected.slice(0, layout.cameras).map((e, i) => {
@ -194,13 +148,60 @@ const Multiview = (props: MultiviewProps) => {
});
return (
<div className={classes.root} ref={outerRef}>
<Box
ref={outerRef}
sx={{
flex: "1 0 0",
color: "white",
marginTop: theme.spacing(2),
overflow: "hidden",
// TODO: this mid-level div can probably be removed.
"& > .mid": {
width: "100%",
height: "100%",
position: "relative",
display: "inline-block",
},
}}
>
<div className="mid">
<div className={`${classes.inner} ${layout.className}`}>
<Box
className={layout.className}
sx={{
// match parent's size without influencing it.
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "#000",
overflow: "hidden",
display: "grid",
gridGap: "0px",
// These class names must match LAYOUTS (above).
"&.solo": {
gridTemplateColumns: "100%",
gridTemplateRows: "100%",
},
"&.two-by-two": {
gridTemplateColumns: "repeat(2, calc(100% / 2))",
gridTemplateRows: "repeat(2, calc(100% / 2))",
},
"&.main-plus-five, &.three-by-three": {
gridTemplateColumns: "repeat(3, calc(100% / 3))",
gridTemplateRows: "repeat(3, calc(100% / 3))",
},
"&.main-plus-five > div:nth-child(1)": {
gridColumn: "span 2",
gridRow: "span 2",
},
}}
>
{monoviews}
</div>
</Box>
</div>
</div>
</Box>
);
};

View File

@ -8,8 +8,7 @@ import DialogActions from "@mui/material/DialogActions";
import DialogTitle from "@mui/material/DialogTitle";
import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import { Theme } from "@mui/material/styles";
import { makeStyles } from "@mui/styles";
import { useTheme } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import LoadingButton from "@mui/lab/LoadingButton";
@ -17,12 +16,6 @@ import React, { useEffect } from "react";
import * as api from "./api";
import { useSnackbars } from "./snackbars";
const useStyles = makeStyles((theme: Theme) => ({
avatar: {
backgroundColor: theme.palette.secondary.main,
},
}));
interface Props {
open: boolean;
onSuccess: () => void;
@ -55,7 +48,7 @@ interface Props {
* <tt>--allow-unauthenticated-permissions</tt>), the caller may ignore this.
*/
const Login = ({ open, onSuccess, handleClose }: Props) => {
const classes = useStyles();
const theme = useTheme();
const snackbars = useSnackbars();
// This is a simple uncontrolled form; use refs.
@ -119,7 +112,7 @@ const Login = ({ open, onSuccess, handleClose }: Props) => {
fullWidth={true}
>
<DialogTitle id="login-title">
<Avatar className={classes.avatar}>
<Avatar sx={{ backgroundColor: theme.palette.secondary.main }}>
<LockOutlinedIcon />
</Avatar>
Log in