prettify code

This commit is contained in:
michioxd 2024-04-14 10:04:54 +07:00 committed by Scott Lamb
parent a787703a31
commit 8036aa40b7
14 changed files with 4539 additions and 2313 deletions

File diff suppressed because it is too large Load Diff

View File

@ -76,12 +76,14 @@ function MoonfireMenu(props: Props) {
</Box> </Box>
)} )}
<Tooltip title="Toggle theme"> <Tooltip title="Toggle theme">
<IconButton <IconButton onClick={changeTheme} color="inherit" size="small">
onClick={changeTheme} {choosenTheme === CurrentMode.Light ? (
color="inherit" <Brightness7 />
size="small" ) : choosenTheme === CurrentMode.Dark ? (
> <Brightness2 />
{choosenTheme === CurrentMode.Light ? <Brightness7 /> : choosenTheme === CurrentMode.Dark ? <Brightness2 /> : <BrightnessAuto />} ) : (
<BrightnessAuto />
)}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{props.loginState !== "unknown" && props.loginState !== "logged-in" && ( {props.loginState !== "unknown" && props.loginState !== "logged-in" && (

View File

@ -41,61 +41,65 @@ const DisplaySelector = (props: Props) => {
}} }}
> >
<CardContent> <CardContent>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id="split90k-label" shrink> <InputLabel id="split90k-label" shrink>
Max video duration Max video duration
</InputLabel> </InputLabel>
<Select <Select
labelId="split90k-label" labelId="split90k-label"
label="Max video duration" label="Max video duration"
id="split90k" id="split90k"
size="small" size="small"
value={props.split90k} value={props.split90k}
onChange={(e) => onChange={(e) =>
props.setSplit90k( props.setSplit90k(
typeof e.target.value === "string" typeof e.target.value === "string"
? parseInt(e.target.value) ? parseInt(e.target.value)
: e.target.value : e.target.value
) )
} }
displayEmpty displayEmpty
> >
{DURATIONS.map(([l, d]) => ( {DURATIONS.map(([l, d]) => (
<MenuItem key={l} value={d}> <MenuItem key={l} value={d}>
{l} {l}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControlLabel <FormControlLabel
title="Trim each segment of video so that it is fully title="Trim each segment of video so that it is fully
contained within the select time range. When this is not selected, contained within the select time range. When this is not selected,
all segments will overlap with the selected time range but may start all segments will overlap with the selected time range but may start
and/or end outside it." and/or end outside it."
control={ control={
<Checkbox <Checkbox
checked={props.trimStartAndEnd} checked={props.trimStartAndEnd}
size="small" size="small"
onChange={(event) => props.setTrimStartAndEnd(event.target.checked)} onChange={(event) =>
name="trim-start-and-end" props.setTrimStartAndEnd(event.target.checked)
color="secondary" }
/> name="trim-start-and-end"
} color="secondary"
label="Trim start and end" />
/> }
<FormControlLabel label="Trim start and end"
title="Include a text track in each .mp4 with the />
<FormControlLabel
title="Include a text track in each .mp4 with the
timestamp at which the video was recorded." timestamp at which the video was recorded."
control={ control={
<Checkbox <Checkbox
checked={props.timestampTrack} checked={props.timestampTrack}
size="small" size="small"
onChange={(event) => props.setTimestampTrack(event.target.checked)} onChange={(event) =>
name="timestamp-track" props.setTimestampTrack(event.target.checked)
color="secondary" }
/> name="timestamp-track"
} color="secondary"
label="Timestamp track" />
}
label="Timestamp track"
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -90,37 +90,36 @@ const StreamMultiSelector = ({ toplevel, selected, setSelected }: Props) => {
); );
}); });
return ( return (
<Card <Card>
>
<CardContent> <CardContent>
<Box <Box
component="table" component="table"
sx={{ sx={{
fontSize: "0.9rem", fontSize: "0.9rem",
"& td:first-of-type": { "& td:first-of-type": {
paddingRight: "3px", paddingRight: "3px",
},
"& td:not(:first-of-type)": {
textAlign: "center",
},
"& .MuiCheckbox-root": {
padding: "3px",
},
"@media (pointer: fine)": {
"& .MuiCheckbox-root": {
padding: "0px",
}, },
}, "& td:not(:first-of-type)": {
}} textAlign: "center",
> },
<thead> "& .MuiCheckbox-root": {
<tr> padding: "3px",
<td /> },
<td onClick={() => toggleType("main")}>main</td> "@media (pointer: fine)": {
<td onClick={() => toggleType("sub")}>sub</td> "& .MuiCheckbox-root": {
</tr> padding: "0px",
</thead> },
<tbody>{cameraRows}</tbody> },
}}
>
<thead>
<tr>
<td />
<td onClick={() => toggleType("main")}>main</td>
<td onClick={() => toggleType("sub")}>sub</td>
</tr>
</thead>
<tbody>{cameraRows}</tbody>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -373,80 +373,82 @@ const TimerangeSelector = ({
<Card> <Card>
<CardContent> <CardContent>
<Box> <Box>
<FormLabel component="legend">From</FormLabel> <FormLabel component="legend">From</FormLabel>
<SmallStaticDatePicker
displayStaticWrapperAs="desktop"
value={startDate}
disabled={days.allowed === null}
shouldDisableDate={shouldDisableDate}
maxDate={
days.allowed === null ? today : new Date(days.allowed.maxMillis)
}
minDate={
days.allowed === null ? today : new Date(days.allowed.minMillis)
}
onChange={(d: Date | null) => {
updateDays({ op: "set-start-day", newStartDate: d });
}}
/>
<MyTimePicker
value={startTime}
onChange={(newValue) => {
if (newValue === null || isFinite((newValue as Date).getTime())) {
setStartTime(newValue);
}
}}
disabled={days.allowed === null}
/>
</Box>
<Box>
<FormLabel sx={{ mt: 1 }} component="legend">To</FormLabel>
<RadioGroup
row
value={days.endType}
onChange={(e) => {
updateDays({
op: "set-end-type",
newEndType: e.target.value as EndDayType,
});
}}
>
<FormControlLabel
value="same-day"
control={<Radio size="small" color="secondary" />}
label="Same day"
/>
<FormControlLabel
value="other-day"
control={<Radio size="small" color="secondary" />}
label="Other day"
/>
</RadioGroup>
<Collapse in={days.endType === "other-day"}>
<SmallStaticDatePicker <SmallStaticDatePicker
displayStaticWrapperAs="desktop" displayStaticWrapperAs="desktop"
value={endDate} value={startDate}
shouldDisableDate={(d: Date | null) => disabled={days.allowed === null}
days.endType !== "other-day" || shouldDisableDate(d) shouldDisableDate={shouldDisableDate}
}
maxDate={ maxDate={
startDate === null ? today : new Date(days.allowed!.maxMillis) days.allowed === null ? today : new Date(days.allowed.maxMillis)
}
minDate={
days.allowed === null ? today : new Date(days.allowed.minMillis)
} }
minDate={startDate === null ? today : startDate}
onChange={(d: Date | null) => { onChange={(d: Date | null) => {
updateDays({ op: "set-end-day", newEndDate: d! }); updateDays({ op: "set-start-day", newStartDate: d });
}} }}
/> />
</Collapse> <MyTimePicker
<MyTimePicker value={startTime}
value={endTime} onChange={(newValue) => {
onChange={(newValue) => { if (newValue === null || isFinite((newValue as Date).getTime())) {
if (newValue === null || isFinite((newValue as Date).getTime())) { setStartTime(newValue);
setEndTime(newValue); }
} }}
}} disabled={days.allowed === null}
disabled={days.allowed === null} />
/> </Box>
<Box>
<FormLabel sx={{ mt: 1 }} component="legend">
To
</FormLabel>
<RadioGroup
row
value={days.endType}
onChange={(e) => {
updateDays({
op: "set-end-type",
newEndType: e.target.value as EndDayType,
});
}}
>
<FormControlLabel
value="same-day"
control={<Radio size="small" color="secondary" />}
label="Same day"
/>
<FormControlLabel
value="other-day"
control={<Radio size="small" color="secondary" />}
label="Other day"
/>
</RadioGroup>
<Collapse in={days.endType === "other-day"}>
<SmallStaticDatePicker
displayStaticWrapperAs="desktop"
value={endDate}
shouldDisableDate={(d: Date | null) =>
days.endType !== "other-day" || shouldDisableDate(d)
}
maxDate={
startDate === null ? today : new Date(days.allowed!.maxMillis)
}
minDate={startDate === null ? today : startDate}
onChange={(d: Date | null) => {
updateDays({ op: "set-end-day", newEndDate: d! });
}}
/>
</Collapse>
<MyTimePicker
value={endTime}
onChange={(newValue) => {
if (newValue === null || isFinite((newValue as Date).getTime())) {
setEndTime(newValue);
}
}}
disabled={days.allowed === null}
/>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -15,7 +15,7 @@ import Fullscreen from "@mui/icons-material/Fullscreen";
export interface Layout { export interface Layout {
className: string; className: string;
cameras: number; cameras: number;
name: string name: string;
} }
// These class names must match useStyles rules (below). // These class names must match useStyles rules (below).
@ -24,7 +24,7 @@ const LAYOUTS: Layout[] = [
{ className: "dual", cameras: 2, name: "2" }, { className: "dual", cameras: 2, name: "2" },
{ className: "main-plus-five", cameras: 6, name: "Main + 5" }, { className: "main-plus-five", cameras: 6, name: "Main + 5" },
{ className: "two-by-two", cameras: 4, name: "2x2" }, { className: "two-by-two", cameras: 4, name: "2x2" },
{ className: "three-by-three", cameras: 9, name: "3x3" } { className: "three-by-three", cameras: 9, name: "3x3" },
]; ];
const MAX_CAMERAS = 9; const MAX_CAMERAS = 9;
@ -114,9 +114,9 @@ const Multiview = (props: MultiviewProps) => {
const [selected, updateSelected] = useReducer( const [selected, updateSelected] = useReducer(
selectedReducer, selectedReducer,
searchParams.has("cams") searchParams.has("cams")
? JSON.parse(searchParams.get("cams") || "") : ? JSON.parse(searchParams.get("cams") || "")
localStorage.getItem("camsSelected") !== null ? : localStorage.getItem("camsSelected") !== null
JSON.parse(localStorage.getItem("camsSelected") || "") ? JSON.parse(localStorage.getItem("camsSelected") || "")
: Array(MAX_CAMERAS).fill(null) : Array(MAX_CAMERAS).fill(null)
); );
@ -124,7 +124,8 @@ const Multiview = (props: MultiviewProps) => {
* Save previously selected cameras to local storage. * Save previously selected cameras to local storage.
*/ */
useEffect(() => { useEffect(() => {
if (searchParams.has("cams")) localStorage.setItem("camsSelected", (searchParams.get("cams") || "")); if (searchParams.has("cams"))
localStorage.setItem("camsSelected", searchParams.get("cams") || "");
}, [searchParams]); }, [searchParams]);
const outerRef = React.useRef<HTMLDivElement>(null); const outerRef = React.useRef<HTMLDivElement>(null);
@ -148,7 +149,6 @@ const Multiview = (props: MultiviewProps) => {
} }
}, [outerRef]); }, [outerRef]);
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
@ -196,11 +196,23 @@ const Multiview = (props: MultiviewProps) => {
}} }}
> >
<Tooltip title="Toggle full screen"> <Tooltip title="Toggle full screen">
<IconButton size="small" sx={{ <IconButton
position: 'fixed', background: 'rgba(50,50,50,0.4) !important', transition: '0.2s', opacity: '0.4', bottom: 10, right: 10, zIndex: 9, color: "#fff", ":hover": { size="small"
opacity: '1' sx={{
} position: "fixed",
}} onClick={handleFullScreen}> background: "rgba(50,50,50,0.4) !important",
transition: "0.2s",
opacity: "0.4",
bottom: 10,
right: 10,
zIndex: 9,
color: "#fff",
":hover": {
opacity: "1",
},
}}
onClick={handleFullScreen}
>
<Fullscreen /> <Fullscreen />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -227,12 +239,12 @@ const Multiview = (props: MultiviewProps) => {
gridTemplateColumns: { gridTemplateColumns: {
xs: "100%", xs: "100%",
sm: "100%", sm: "100%",
md: "repeat(2, calc(100% / 2))" md: "repeat(2, calc(100% / 2))",
}, },
gridTemplateRows: { gridTemplateRows: {
xs: "50%", xs: "50%",
sm: "50%", sm: "50%",
md: "repeat(1, calc(100% / 1))" md: "repeat(1, calc(100% / 1))",
}, },
}, },
"&.two-by-two": { "&.two-by-two": {

View File

@ -19,15 +19,21 @@ export interface LiveProps {
const Live = ({ cameras, Frame }: LiveProps) => { const Live = ({ cameras, Frame }: LiveProps) => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [multiviewLayoutIndex, setMultiviewLayoutIndex] = useState( const [multiviewLayoutIndex, setMultiviewLayoutIndex] = useState(
Number.parseInt(searchParams.get("layout") || localStorage.getItem("multiviewLayoutIndex") || "0", 10) Number.parseInt(
searchParams.get("layout") ||
localStorage.getItem("multiviewLayoutIndex") ||
"0",
10
)
); );
useEffect(() => { useEffect(() => {
if (searchParams.has("layout")) if (searchParams.has("layout"))
localStorage.setItem("multiviewLayoutIndex", (searchParams.get("layout") || "0")); localStorage.setItem(
"multiviewLayoutIndex",
searchParams.get("layout") || "0"
);
}, [searchParams]); }, [searchParams]);
if ("MediaSource" in window === false) { if ("MediaSource" in window === false) {

View File

@ -118,7 +118,7 @@ const Login = ({ open, onSuccess, handleClose }: Props) => {
</DialogTitle> </DialogTitle>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<DialogContent> <DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<TextField <TextField
id="username" id="username"
label="Username" label="Username"

View File

@ -18,15 +18,21 @@ import { useReducer } from "react";
import { LoginState } from "../App"; import { LoginState } from "../App";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
export default function Header({ loginState, logout, setChangePasswordOpen, activityMenuPart, setLoginState, toplevel }: export default function Header({
{ loginState,
loginState: LoginState, logout,
logout: () => void, setChangePasswordOpen,
setChangePasswordOpen: React.Dispatch<React.SetStateAction<boolean>>, activityMenuPart,
activityMenuPart?: JSX.Element, setLoginState,
setLoginState: React.Dispatch<React.SetStateAction<LoginState>>, toplevel,
toplevel: api.ToplevelResponse | null }: {
}) { loginState: LoginState;
logout: () => void;
setChangePasswordOpen: React.Dispatch<React.SetStateAction<boolean>>;
activityMenuPart?: JSX.Element;
setLoginState: React.Dispatch<React.SetStateAction<LoginState>>;
toplevel: api.ToplevelResponse | null;
}) {
const [showMenu, toggleShowMenu] = useReducer((m: boolean) => !m, false); const [showMenu, toggleShowMenu] = useReducer((m: boolean) => !m, false);
return ( return (
@ -90,5 +96,5 @@ export default function Header({ loginState, logout, setChangePasswordOpen, acti
</List> </List>
</Drawer> </Drawer>
</> </>
) );
} }

View File

@ -9,19 +9,21 @@ import React, { createContext } from "react";
export enum CurrentMode { export enum CurrentMode {
Auto = 0, Auto = 0,
Light = 1, Light = 1,
Dark = 2 Dark = 2,
} }
interface ThemeProps { interface ThemeProps {
changeTheme: () => void, changeTheme: () => void;
currentTheme: 'dark' | 'light', currentTheme: "dark" | "light";
choosenTheme: CurrentMode choosenTheme: CurrentMode;
} }
export const ThemeContext = createContext<ThemeProps>({ export const ThemeContext = createContext<ThemeProps>({
currentTheme: window.matchMedia("(prefers-color-scheme: dark)").matches ? 'dark' : 'light', currentTheme: window.matchMedia("(prefers-color-scheme: dark)").matches
changeTheme: () => { }, ? "dark"
choosenTheme: CurrentMode.Auto : "light",
changeTheme: () => {},
choosenTheme: CurrentMode.Auto,
}); });
const ThemeMode = ({ children }: { children: JSX.Element }): JSX.Element => { const ThemeMode = ({ children }: { children: JSX.Element }): JSX.Element => {
@ -40,22 +42,34 @@ const ThemeMode = ({ children }: { children: JSX.Element }): JSX.Element => {
return matches; return matches;
}; };
const detectedSystemColorScheme = useMediaQuery("(prefers-color-scheme: dark)") ? "dark" : "light"; const detectedSystemColorScheme = useMediaQuery(
"(prefers-color-scheme: dark)"
)
? "dark"
: "light";
const changeTheme = React.useCallback(() => { const changeTheme = React.useCallback(() => {
setMode(mode === 'dark' ? 'light' : mode === 'light' ? 'system' : 'dark') setMode(mode === "dark" ? "light" : mode === "light" ? "system" : "dark");
}, [mode, setMode]); }, [mode, setMode]);
const currentTheme = mode === 'system' ? detectedSystemColorScheme : (mode ?? detectedSystemColorScheme); const currentTheme =
const choosenTheme = mode === 'dark' ? CurrentMode.Dark : mode === 'light' ? CurrentMode.Light : CurrentMode.Auto; mode === "system"
? detectedSystemColorScheme
: mode ?? detectedSystemColorScheme;
const choosenTheme =
mode === "dark"
? CurrentMode.Dark
: mode === "light"
? CurrentMode.Light
: CurrentMode.Auto;
return ( return (
<ThemeContext.Provider value={{ changeTheme, currentTheme, choosenTheme }}> <ThemeContext.Provider value={{ changeTheme, currentTheme, choosenTheme }}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
) );
} };
export default ThemeMode; export default ThemeMode;
export const useThemeMode = () => React.useContext(ThemeContext); export const useThemeMode = () => React.useContext(ThemeContext);

View File

@ -23,4 +23,4 @@ a {
[data-mui-color-scheme="dark"] { [data-mui-color-scheme="dark"] {
color-scheme: dark; color-scheme: dark;
} }

View File

@ -3,7 +3,10 @@
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import { Experimental_CssVarsProvider, experimental_extendTheme } from "@mui/material/styles"; import {
Experimental_CssVarsProvider,
experimental_extendTheme,
} from "@mui/material/styles";
import StyledEngineProvider from "@mui/material/StyledEngineProvider"; import StyledEngineProvider from "@mui/material/StyledEngineProvider";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import "@fontsource/roboto"; import "@fontsource/roboto";
@ -22,15 +25,15 @@ const themeExtended = experimental_extendTheme({
dark: { dark: {
palette: { palette: {
primary: { primary: {
main: "#000000" main: "#000000",
}, },
secondary: { secondary: {
main: "#e65100" main: "#e65100",
} },
} },
}, },
} },
}) });
const container = document.getElementById("root"); const container = document.getElementById("root");
const root = createRoot(container!); const root = createRoot(container!);
root.render( root.render(

View File

@ -7,11 +7,11 @@
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { vi } from 'vitest' import { vi } from "vitest";
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, "matchMedia", {
writable: true, writable: true,
value: vi.fn().mockImplementation(query => ({ value: vi.fn().mockImplementation((query) => ({
matches: false, matches: false,
media: query, media: query,
onchange: null, onchange: null,
@ -21,4 +21,4 @@ Object.defineProperty(window, 'matchMedia', {
removeEventListener: vi.fn(), removeEventListener: vi.fn(),
dispatchEvent: vi.fn(), dispatchEvent: vi.fn(),
})), })),
}) });

View File

@ -11,9 +11,13 @@ const target = process.env.PROXY_TARGET ?? "http://localhost:8080/";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), viteCompression(), viteLegacyPlugin({ plugins: [
targets: ['defaults', 'fully supports es6-module'], react(),
})], viteCompression(),
viteLegacyPlugin({
targets: ["defaults", "fully supports es6-module"],
}),
],
server: { server: {
proxy: { proxy: {
"/api": { "/api": {