prettify code
This commit is contained in:
parent
a787703a31
commit
8036aa40b7
6376
ui/pnpm-lock.yaml
6376
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -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" && (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -23,4 +23,4 @@ a {
|
||||||
|
|
||||||
[data-mui-color-scheme="dark"] {
|
[data-mui-color-scheme="dark"] {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(),
|
||||||
})),
|
})),
|
||||||
})
|
});
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
Loading…
Reference in New Issue