This commit is contained in:
Josh Hawkins 2024-04-18 08:07:13 -05:00
parent f92de9af2e
commit 8e24bd79df
6 changed files with 123 additions and 141 deletions

View File

@ -236,7 +236,7 @@ export function useMotionContourArea(camera: string): {
} }
export function useImproveContrast(camera: string): { export function useImproveContrast(camera: string): {
payload: string; payload: ToggleableSetting;
send: (payload: string, retain?: boolean) => void; send: (payload: string, retain?: boolean) => void;
} { } {
const { const {
@ -246,5 +246,5 @@ export function useImproveContrast(camera: string): {
`${camera}/improve_contrast/state`, `${camera}/improve_contrast/state`,
`${camera}/improve_contrast/set`, `${camera}/improve_contrast/set`,
); );
return { payload: payload as string, send }; return { payload: payload as ToggleableSetting, send };
} }

View File

@ -24,6 +24,7 @@ import MotionMaskEditPane from "./MotionMaskEditPane";
import ObjectMaskEditPane from "./ObjectMaskEditPane"; import ObjectMaskEditPane from "./ObjectMaskEditPane";
import PolygonItem from "./PolygonItem"; import PolygonItem from "./PolygonItem";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { isDesktop } from "react-device-detect";
type MasksAndZoneProps = { type MasksAndZoneProps = {
selectedCamera: string; selectedCamera: string;
@ -87,8 +88,8 @@ export default function MasksAndZones({
}, [config, selectedCamera]); }, [config, selectedCamera]);
const stretch = true; const stretch = true;
// TODO: mobile / portrait cams // may need tweaking for mobile
const fitAspect = 16 / 9; const fitAspect = isDesktop ? 16 / 9 : 3 / 4;
const scaledHeight = useMemo(() => { const scaledHeight = useMemo(() => {
if (containerRef.current && aspectRatio && detectHeight) { if (containerRef.current && aspectRatio && detectHeight) {
@ -97,7 +98,9 @@ export default function MasksAndZones({
? Math.floor( ? Math.floor(
Math.min(containerHeight, containerRef.current?.clientHeight), Math.min(containerHeight, containerRef.current?.clientHeight),
) )
: Math.floor(containerWidth / aspectRatio); : isDesktop || aspectRatio > fitAspect
? Math.floor(containerWidth / aspectRatio)
: Math.floor(containerWidth / aspectRatio) / 1.5;
const finalHeight = stretch const finalHeight = stretch
? scaledHeight ? scaledHeight
: Math.min(scaledHeight, detectHeight); : Math.min(scaledHeight, detectHeight);
@ -157,12 +160,14 @@ export default function MasksAndZones({
setEditingPolygons([...allPolygons]); setEditingPolygons([...allPolygons]);
setActivePolygonIndex(undefined); setActivePolygonIndex(undefined);
setHoveredPolygonIndex(null); setHoveredPolygonIndex(null);
}, [allPolygons]); setUnsavedChanges(false);
}, [allPolygons, setUnsavedChanges]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
setAllPolygons([...(editingPolygons ?? [])]); setAllPolygons([...(editingPolygons ?? [])]);
setHoveredPolygonIndex(null); setHoveredPolygonIndex(null);
}, [editingPolygons]); setUnsavedChanges(false);
}, [editingPolygons, setUnsavedChanges]);
useEffect(() => { useEffect(() => {
if (isLoading) { if (isLoading) {
@ -218,62 +223,52 @@ export default function MasksAndZones({
let globalObjectMasks: Polygon[] = []; let globalObjectMasks: Polygon[] = [];
let objectMasks: Polygon[] = []; let objectMasks: Polygon[] = [];
if ( // this can be an array or a string
cameraConfig.motion.mask !== null && motionMasks = (
cameraConfig.motion.mask !== undefined Array.isArray(cameraConfig.motion.mask)
) { ? cameraConfig.motion.mask
// this can be an array or a string : cameraConfig.motion.mask
motionMasks = ( ? [cameraConfig.motion.mask]
Array.isArray(cameraConfig.motion.mask) : []
? cameraConfig.motion.mask ).map((maskData, index) => ({
: cameraConfig.motion.mask type: "motion_mask" as PolygonType,
? [cameraConfig.motion.mask] typeIndex: index,
: [] camera: cameraConfig.name,
).map((maskData, index) => ({ name: `Motion Mask ${index + 1}`,
type: "motion_mask" as PolygonType, objects: [],
typeIndex: index, points: interpolatePoints(
camera: cameraConfig.name, parseCoordinates(maskData),
name: `Motion Mask ${index + 1}`, 1,
objects: [], 1,
points: interpolatePoints( scaledWidth,
parseCoordinates(maskData), scaledHeight,
1, ),
1, isFinished: true,
scaledWidth, color: [0, 0, 255],
scaledHeight, }));
),
isFinished: true,
color: [0, 0, 255],
}));
}
const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask) const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask)
? cameraConfig.objects.mask ? cameraConfig.objects.mask
: cameraConfig.objects.mask : cameraConfig.objects.mask
? [cameraConfig.objects.mask] ? [cameraConfig.objects.mask]
: []; : [];
// TODO: check to see if this is necessary
if ( globalObjectMasks = globalObjectMasksArray.map((maskData, index) => ({
cameraConfig.objects.mask !== null && type: "object_mask" as PolygonType,
cameraConfig.objects.mask !== undefined typeIndex: index,
) { camera: cameraConfig.name,
globalObjectMasks = globalObjectMasksArray.map((maskData, index) => ({ name: `Object Mask ${index + 1} (all objects)`,
type: "object_mask" as PolygonType, objects: [],
typeIndex: index, points: interpolatePoints(
camera: cameraConfig.name, parseCoordinates(maskData),
name: `Object Mask ${index + 1} (all objects)`, 1,
objects: [], 1,
points: interpolatePoints( scaledWidth,
parseCoordinates(maskData), scaledHeight,
1, ),
1, isFinished: true,
scaledWidth, color: [128, 128, 128],
scaledHeight, }));
),
isFinished: true,
color: [128, 128, 128],
}));
}
const globalObjectMasksCount = globalObjectMasks.length; const globalObjectMasksCount = globalObjectMasks.length;
let index = 0; let index = 0;

View File

@ -1,14 +1,4 @@
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import axios from "axios"; import axios from "axios";
@ -50,7 +40,6 @@ export default function MotionTuner({
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false); const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera); const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera); const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
@ -87,43 +76,31 @@ export default function MotionTuner({
improve_contrast: cameraConfig.motion.improve_contrast, improve_contrast: cameraConfig.motion.improve_contrast,
}); });
} }
}, [cameraConfig]); // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
if (cameraConfig) { if (!motionSettings.threshold) return;
const { threshold, contour_area, improve_contrast } = motionSettings;
if ( sendMotionThreshold(motionSettings.threshold);
threshold !== undefined && }, [motionSettings.threshold, sendMotionThreshold]);
cameraConfig.motion.threshold !== threshold
) {
sendMotionThreshold(threshold);
}
if ( useEffect(() => {
contour_area !== undefined && if (!motionSettings.contour_area) return;
cameraConfig.motion.contour_area !== contour_area
) {
sendMotionContourArea(contour_area);
}
if ( sendMotionContourArea(motionSettings.contour_area);
improve_contrast !== undefined && }, [motionSettings.contour_area, sendMotionContourArea]);
cameraConfig.motion.improve_contrast !== improve_contrast
) { useEffect(() => {
sendImproveContrast(improve_contrast ? "ON" : "OFF"); if (motionSettings.improve_contrast === undefined) return;
}
} sendImproveContrast(motionSettings.improve_contrast ? "ON" : "OFF");
}, [ }, [motionSettings.improve_contrast, sendImproveContrast]);
cameraConfig,
motionSettings,
sendMotionThreshold,
sendMotionContourArea,
sendImproveContrast,
]);
const handleMotionConfigChange = (newConfig: Partial<MotionSettings>) => { const handleMotionConfigChange = (newConfig: Partial<MotionSettings>) => {
setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
setUnsavedChanges(true);
setChangedValue(true); setChangedValue(true);
}; };
@ -170,17 +147,6 @@ export default function MotionTuner({
setChangedValue(false); setChangedValue(false);
}, [origMotionSettings]); }, [origMotionSettings]);
const handleDialog = useCallback(
(save: boolean) => {
if (save) {
saveToConfig();
}
setConfirmationDialogOpen(false);
setChangedValue(false);
},
[saveToConfig],
);
if (!cameraConfig && !selectedCamera) { if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@ -315,31 +281,6 @@ export default function MotionTuner({
</Button> </Button>
</div> </div>
</div> </div>
{confirmationDialogOpen && (
<AlertDialog
open={confirmationDialogOpen}
onOpenChange={() => setConfirmationDialogOpen(false)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
You have unsaved changes on this camera.
</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>
)}
</div> </div>
{cameraConfig ? ( {cameraConfig ? (

View File

@ -97,6 +97,7 @@ export default function PolygonDrawer({
onMouseOver={isActive ? handleGroupMouseOver : undefined} onMouseOver={isActive ? handleGroupMouseOver : undefined}
onTouchStart={isActive ? handleGroupMouseOver : undefined} onTouchStart={isActive ? handleGroupMouseOver : undefined}
onMouseOut={isActive ? handleGroupMouseOut : undefined} onMouseOut={isActive ? handleGroupMouseOut : undefined}
// TODO: don't use zindex
zIndex={isActive ? 999 : 100} zIndex={isActive ? 999 : 100}
> >
<Line <Line

View File

@ -53,12 +53,12 @@ export default function PolygonEditControls({
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="default"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground" className="size-6 p-1 rounded-md"
disabled={!polygons[activePolygonIndex].points.length} disabled={!polygons[activePolygonIndex].points.length}
onClick={undo} onClick={undo}
> >
<MdUndo /> <MdUndo className="text-secondary-foreground" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Undo</TooltipContent> <TooltipContent>Undo</TooltipContent>
@ -66,12 +66,12 @@ export default function PolygonEditControls({
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="default"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground" className="size-6 p-1 rounded-md"
disabled={!polygons[activePolygonIndex].points.length} disabled={!polygons[activePolygonIndex].points.length}
onClick={reset} onClick={reset}
> >
<MdOutlineRestartAlt /> <MdOutlineRestartAlt className="text-secondary-foreground" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Reset</TooltipContent> <TooltipContent>Reset</TooltipContent>

View File

@ -5,12 +5,22 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import MotionTuner from "@/components/settings/MotionTuner"; import MotionTuner from "@/components/settings/MotionTuner";
import MasksAndZones from "@/components/settings/MasksAndZones"; import MasksAndZones from "@/components/settings/MasksAndZones";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import useOptimisticState from "@/hooks/use-optimistic-state"; import useOptimisticState from "@/hooks/use-optimistic-state";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { FaVideo } from "react-icons/fa"; import { FaVideo } from "react-icons/fa";
@ -38,6 +48,7 @@ export default function Settings() {
// TODO: confirm leave page // TODO: confirm leave page
const [unsavedChanges, setUnsavedChanges] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const cameras = useMemo(() => { const cameras = useMemo(() => {
if (!config) { if (!config) {
@ -53,6 +64,17 @@ export default function Settings() {
const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>(); const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>();
const handleDialog = useCallback(
(save: boolean) => {
if (unsavedChanges && save) {
// TODO
}
setConfirmationDialogOpen(false);
setUnsavedChanges(false);
},
[unsavedChanges],
);
useEffect(() => { useEffect(() => {
if (cameras.length) { if (cameras.length) {
setSelectedCamera(cameras[0].name); setSelectedCamera(cameras[0].name);
@ -123,6 +145,29 @@ export default function Settings() {
/> />
)} )}
</div> </div>
{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>
)}
</div> </div>
); );
} }
@ -165,7 +210,7 @@ function CameraSelectButton({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>
)} )}
<div className="h-auto pt-2 overflow-y-auto overflow-x-hidden"> <div className="h-auto pt-2 p-3 mb-5 md:p-1 md:mb-1 overflow-y-auto overflow-x-hidden">
<div className="flex flex-col gap-2.5"> <div className="flex flex-col gap-2.5">
{allCameras.map((item) => ( {allCameras.map((item) => (
<FilterSwitch <FilterSwitch