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

6376
ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -373,80 +373,82 @@ const TimerangeSelector = ({
<Card>
<CardContent>
<Box>
<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"}>
<FormLabel component="legend">From</FormLabel>
<SmallStaticDatePicker
displayStaticWrapperAs="desktop"
value={endDate}
shouldDisableDate={(d: Date | null) =>
days.endType !== "other-day" || shouldDisableDate(d)
}
value={startDate}
disabled={days.allowed === null}
shouldDisableDate={shouldDisableDate}
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) => {
updateDays({ op: "set-end-day", newEndDate: d! });
updateDays({ op: "set-start-day", newStartDate: d });
}}
/>
</Collapse>
<MyTimePicker
value={endTime}
onChange={(newValue) => {
if (newValue === null || isFinite((newValue as Date).getTime())) {
setEndTime(newValue);
}
}}
disabled={days.allowed === null}
/>
<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
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>
</CardContent>
</Card>

View File

@ -15,7 +15,7 @@ import Fullscreen from "@mui/icons-material/Fullscreen";
export interface Layout {
className: string;
cameras: number;
name: string
name: string;
}
// These class names must match useStyles rules (below).
@ -24,7 +24,7 @@ const LAYOUTS: Layout[] = [
{ className: "dual", cameras: 2, name: "2" },
{ className: "main-plus-five", cameras: 6, name: "Main + 5" },
{ 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;
@ -114,9 +114,9 @@ const Multiview = (props: MultiviewProps) => {
const [selected, updateSelected] = useReducer(
selectedReducer,
searchParams.has("cams")
? JSON.parse(searchParams.get("cams") || "") :
localStorage.getItem("camsSelected") !== null ?
JSON.parse(localStorage.getItem("camsSelected") || "")
? JSON.parse(searchParams.get("cams") || "")
: localStorage.getItem("camsSelected") !== null
? JSON.parse(localStorage.getItem("camsSelected") || "")
: Array(MAX_CAMERAS).fill(null)
);
@ -124,7 +124,8 @@ const Multiview = (props: MultiviewProps) => {
* Save previously selected cameras to local storage.
*/
useEffect(() => {
if (searchParams.has("cams")) localStorage.setItem("camsSelected", (searchParams.get("cams") || ""));
if (searchParams.has("cams"))
localStorage.setItem("camsSelected", searchParams.get("cams") || "");
}, [searchParams]);
const outerRef = React.useRef<HTMLDivElement>(null);
@ -148,7 +149,6 @@ const Multiview = (props: MultiviewProps) => {
}
}, [outerRef]);
const monoviews = selected.slice(0, layout.cameras).map((e, i) => {
// When a camera is selected, use the camera's index as the key.
// This allows swapping cameras' positions without tearing down their
@ -196,11 +196,23 @@ const Multiview = (props: MultiviewProps) => {
}}
>
<Tooltip title="Toggle full screen">
<IconButton size="small" sx={{
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": {
opacity: '1'
}
}} onClick={handleFullScreen}>
<IconButton
size="small"
sx={{
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": {
opacity: "1",
},
}}
onClick={handleFullScreen}
>
<Fullscreen />
</IconButton>
</Tooltip>
@ -227,12 +239,12 @@ const Multiview = (props: MultiviewProps) => {
gridTemplateColumns: {
xs: "100%",
sm: "100%",
md: "repeat(2, calc(100% / 2))"
md: "repeat(2, calc(100% / 2))",
},
gridTemplateRows: {
xs: "50%",
sm: "50%",
md: "repeat(1, calc(100% / 1))"
md: "repeat(1, calc(100% / 1))",
},
},
"&.two-by-two": {

View File

@ -19,15 +19,21 @@ export interface LiveProps {
const Live = ({ cameras, Frame }: LiveProps) => {
const [searchParams, setSearchParams] = useSearchParams();
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(() => {
if (searchParams.has("layout"))
localStorage.setItem("multiviewLayoutIndex", (searchParams.get("layout") || "0"));
localStorage.setItem(
"multiviewLayoutIndex",
searchParams.get("layout") || "0"
);
}, [searchParams]);
if ("MediaSource" in window === false) {

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,11 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/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,
value: vi.fn().mockImplementation(query => ({
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
@ -21,4 +21,4 @@ Object.defineProperty(window, 'matchMedia', {
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
});

View File

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