frigate/web/src/pages/Settings.tsx

273 lines
8.1 KiB
TypeScript
Raw Normal View History

import {
2024-04-11 23:48:35 +03:00
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
2024-04-18 16:07:13 +03:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
2024-04-11 23:48:35 +03:00
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
2024-04-05 15:25:23 +03:00
import MotionTuner from "@/components/settings/MotionTuner";
2024-04-11 23:48:35 +03:00
import MasksAndZones from "@/components/settings/MasksAndZones";
import { Button } from "@/components/ui/button";
2024-04-18 16:07:13 +03:00
import { useCallback, useEffect, useMemo, useState } from "react";
2024-04-11 23:48:35 +03:00
import useOptimisticState from "@/hooks/use-optimistic-state";
import { isMobile } from "react-device-detect";
import { FaVideo } from "react-icons/fa";
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import General from "@/components/settings/General";
2024-04-18 03:59:00 +03:00
import FilterSwitch from "@/components/filter/FilterSwitch";
2024-04-13 15:48:31 +03:00
import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter";
import { PolygonType } from "@/types/canvas";
2024-04-18 07:15:44 +03:00
import ObjectSettings from "@/components/settings/ObjectSettings";
2024-04-14 07:23:54 +03:00
export default function Settings() {
const settingsViews = [
"general",
"objects",
"masks / zones",
"motion tuner",
] as const;
type SettingsType = (typeof settingsViews)[number];
const [page, setPage] = useState<SettingsType>("general");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const { data: config } = useSWR<FrigateConfig>("config");
2024-04-18 07:15:44 +03:00
// TODO: confirm leave page
2024-04-14 07:23:54 +03:00
const [unsavedChanges, setUnsavedChanges] = useState(false);
2024-04-18 16:07:13 +03:00
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
2024-04-14 07:23:54 +03:00
const cameras = useMemo(() => {
if (!config) {
return [];
}
return Object.values(config.cameras)
.filter((conf) => conf.ui.dashboard && conf.enabled)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);
2024-04-18 07:15:44 +03:00
const [selectedCamera, setSelectedCamera] = useState<string>("");
2024-04-14 07:23:54 +03:00
const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>();
2024-04-18 16:07:13 +03:00
const handleDialog = useCallback(
(save: boolean) => {
if (unsavedChanges && save) {
// TODO
}
setConfirmationDialogOpen(false);
setUnsavedChanges(false);
},
[unsavedChanges],
);
2024-04-15 04:57:00 +03:00
useEffect(() => {
2024-04-17 23:59:55 +03:00
if (cameras.length) {
2024-04-15 04:57:00 +03:00
setSelectedCamera(cameras[0].name);
}
2024-04-18 07:15:44 +03:00
// only run once
// eslint-disable-next-line react-hooks/exhaustive-deps
2024-04-17 00:03:25 +03:00
}, []);
2024-04-15 04:57:00 +03:00
2024-04-14 07:23:54 +03:00
return (
<div className="size-full p-2 flex flex-col">
<div className="w-full h-11 relative flex justify-between items-center">
2024-04-19 07:47:16 +03:00
<div className="flex flex-row overflow-x-auto">
<ToggleGroup
className="*:px-3 *:py-4 *:rounded-md flex-shrink-0"
type="single"
size="sm"
value={pageToggle}
onValueChange={(value: SettingsType) => {
if (value) {
setPageToggle(value);
}
}}
>
{Object.values(settingsViews).map((item) => (
<ToggleGroupItem
key={item}
className={`flex items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
value={item}
aria-label={`Select ${item}`}
>
<div className="capitalize">{item}</div>
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
2024-04-14 07:23:54 +03:00
{(page == "objects" ||
page == "masks / zones" ||
page == "motion tuner") && (
2024-04-19 07:47:16 +03:00
<div className="flex items-center gap-2 ml-2 flex-shrink-0">
2024-04-15 02:36:39 +03:00
{page == "masks / zones" && (
2024-04-14 07:23:54 +03:00
<ZoneMaskFilterButton
selectedZoneMask={filterZoneMask}
updateZoneMaskFilter={setFilterZoneMask}
/>
)}
<CameraSelectButton
allCameras={cameras}
selectedCamera={selectedCamera}
setSelectedCamera={setSelectedCamera}
/>
</div>
)}
</div>
<div className="mt-2 flex flex-col items-start w-full h-full md:h-dvh md:pb-24">
2024-04-14 07:23:54 +03:00
{page == "general" && <General />}
2024-04-18 07:15:44 +03:00
{page == "objects" && (
<ObjectSettings selectedCamera={selectedCamera} />
)}
2024-04-14 07:23:54 +03:00
{page == "masks / zones" && (
<MasksAndZones
selectedCamera={selectedCamera}
selectedZoneMask={filterZoneMask}
setUnsavedChanges={setUnsavedChanges}
/>
)}
2024-04-15 02:36:39 +03:00
{page == "motion tuner" && (
2024-04-18 07:15:44 +03:00
<MotionTuner
selectedCamera={selectedCamera}
setUnsavedChanges={setUnsavedChanges}
/>
2024-04-15 02:36:39 +03:00
)}
2024-04-14 07:23:54 +03:00
</div>
2024-04-18 16:07:13 +03:00
{confirmationDialogOpen && (
<AlertDialog
open={confirmationDialogOpen}
onOpenChange={() => setConfirmationDialogOpen(false)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>You have unsaved changes.</AlertDialogTitle>
<AlertDialogDescription>
Do you want to save your changes before continuing?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => handleDialog(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDialog(true)}>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
2024-04-14 07:23:54 +03:00
</div>
);
}
2024-04-11 23:48:35 +03:00
type CameraSelectButtonProps = {
allCameras: CameraConfig[];
selectedCamera: string;
setSelectedCamera: React.Dispatch<React.SetStateAction<string>>;
};
2024-04-11 23:48:35 +03:00
function CameraSelectButton({
allCameras,
selectedCamera,
setSelectedCamera,
}: CameraSelectButtonProps) {
const [open, setOpen] = useState(false);
2024-04-17 23:59:55 +03:00
if (!allCameras.length) {
2024-04-15 04:57:00 +03:00
return;
}
2024-04-11 23:48:35 +03:00
const trigger = (
<Button
className="flex items-center gap-2 capitalize bg-selected hover:bg-selected"
size="sm"
>
<FaVideo className="text-background dark:text-primary" />
<div className="hidden md:block text-background dark:text-primary">
2024-04-18 18:33:13 +03:00
{selectedCamera == undefined
? "No Camera"
: selectedCamera.replaceAll("_", " ")}
2024-04-11 23:48:35 +03:00
</div>
</Button>
);
const content = (
<>
{isMobile && (
<>
<DropdownMenuLabel className="flex justify-center">
Camera
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
2024-04-18 21:11:23 +03:00
<div className="h-auto p-4 mb-5 md:mb-1 overflow-y-auto overflow-x-hidden">
2024-04-18 03:59:00 +03:00
<div className="flex flex-col gap-2.5">
{allCameras.map((item) => (
<FilterSwitch
key={item.name}
isChecked={item.name === selectedCamera}
2024-04-18 16:39:36 +03:00
label={item.name.replaceAll("_", " ")}
2024-04-18 03:59:00 +03:00
onCheckedChange={(isChecked) => {
if (isChecked) {
setSelectedCamera(item.name);
setOpen(false);
}
}}
/>
))}
</div>
</div>
</>
);
2024-04-11 23:48:35 +03:00
if (isMobile) {
return (
<Drawer
open={open}
onOpenChange={(open: boolean) => {
if (!open) {
setSelectedCamera(selectedCamera);
}
setOpen(open);
}}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<DropdownMenu
open={open}
onOpenChange={(open: boolean) => {
if (!open) {
setSelectedCamera(selectedCamera);
}
setOpen(open);
}}
>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent>{content}</DropdownMenuContent>
</DropdownMenu>
);
}