From ff2948a76b8f864ff927564faec83f5ba8a51112 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Tue, 7 May 2024 09:28:10 -0500
Subject: [PATCH 46/56] Drag to reorder/resize cameras in camera groups
(#11279)
* draggable/resizable cameras in camera groups on desktop/tablets
* fix edit button location on tablets
* assume 1rem is 16px
---
web/package-lock.json | 71 +++
web/package.json | 2 +
.../components/filter/CameraGroupSelector.tsx | 28 +-
web/src/components/settings/General.tsx | 82 +++-
web/src/hooks/use-overlay-state.tsx | 14 +-
web/src/hooks/use-persistence.ts | 10 +-
web/src/pages/Live.tsx | 1 +
web/src/views/live/DraggableGridLayout.tsx | 461 ++++++++++++++++++
web/src/views/live/LiveDashboardView.tsx | 104 ++--
9 files changed, 714 insertions(+), 59 deletions(-)
create mode 100644 web/src/views/live/DraggableGridLayout.tsx
diff --git a/web/package-lock.json b/web/package-lock.json
index 122c26570..1df2344a4 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -48,6 +48,7 @@
"react-day-picker": "^8.10.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
+ "react-grid-layout": "^1.4.4",
"react-hook-form": "^7.51.3",
"react-icons": "^5.1.0",
"react-konva": "^18.2.10",
@@ -76,6 +77,7 @@
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
+ "@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10",
"@types/strftime": "^0.9.8",
@@ -2572,6 +2574,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-grid-layout": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz",
+ "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-icons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz",
@@ -4392,6 +4403,11 @@
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true
},
+ "node_modules/fast-equals": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
+ "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -6329,6 +6345,44 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-draggable": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
+ "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
+ "dependencies": {
+ "clsx": "^1.1.1",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0",
+ "react-dom": ">= 16.3.0"
+ }
+ },
+ "node_modules/react-draggable/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react-grid-layout": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.4.4.tgz",
+ "integrity": "sha512-7+Lg8E8O8HfOH5FrY80GCIR1SHTn2QnAYKh27/5spoz+OHhMmEhU/14gIkRzJOtympDPaXcVRX/nT1FjmeOUmQ==",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "fast-equals": "^4.0.3",
+ "prop-types": "^15.8.1",
+ "react-draggable": "^4.4.5",
+ "react-resizable": "^3.0.5",
+ "resize-observer-polyfill": "^1.5.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0",
+ "react-dom": ">= 16.3.0"
+ }
+ },
"node_modules/react-hook-form": {
"version": "7.51.3",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
@@ -6448,6 +6502,18 @@
}
}
},
+ "node_modules/react-resizable": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz",
+ "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==",
+ "dependencies": {
+ "prop-types": "15.x",
+ "react-draggable": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3"
+ }
+ },
"node_modules/react-router": {
"version": "6.22.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
@@ -6639,6 +6705,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
+ "node_modules/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
diff --git a/web/package.json b/web/package.json
index 3e8fc2907..980223748 100644
--- a/web/package.json
+++ b/web/package.json
@@ -53,6 +53,7 @@
"react-day-picker": "^8.10.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
+ "react-grid-layout": "^1.4.4",
"react-hook-form": "^7.51.3",
"react-icons": "^5.1.0",
"react-konva": "^18.2.10",
@@ -81,6 +82,7 @@
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
+ "@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10",
"@types/strftime": "^0.9.8",
diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx
index 128645ce2..84d1a36b5 100644
--- a/web/src/components/filter/CameraGroupSelector.tsx
+++ b/web/src/components/filter/CameraGroupSelector.tsx
@@ -59,6 +59,7 @@ import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
import ActivityIndicator from "../indicators/activity-indicator";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
+import { usePersistence } from "@/hooks/use-persistence";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
@@ -89,7 +90,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
// groups
- const [group, setGroup] = usePersistedOverlayState(
+ const [group, setGroup, deleteGroup] = usePersistedOverlayState(
"cameraGroup",
"default" as string,
);
@@ -118,6 +119,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
currentGroups={groups}
activeGroup={group}
setGroup={setGroup}
+ deleteGroup={deleteGroup}
/>
void;
+ deleteGroup: () => void;
};
function NewGroupDialog({
open,
@@ -205,6 +208,7 @@ function NewGroupDialog({
currentGroups,
activeGroup,
setGroup,
+ deleteGroup,
}: NewGroupDialogProps) {
const { mutate: updateConfig } = useSWR
("config");
@@ -225,11 +229,16 @@ function NewGroupDialog({
const [editState, setEditState] = useState<"none" | "add" | "edit">("none");
const [isLoading, setIsLoading] = useState(false);
+ const [, , , deleteGridLayout] = usePersistence(
+ `${activeGroup}-draggable-layout`,
+ );
+
// callbacks
const onDeleteGroup = useCallback(
async (name: string) => {
- // TODO: reset order on groups when deleting
+ deleteGridLayout();
+ deleteGroup();
await axios
.put(`config/set?camera_groups.${name}`, { requires_restart: 0 })
@@ -260,7 +269,14 @@ function NewGroupDialog({
setIsLoading(false);
});
},
- [updateConfig, activeGroup, setGroup, setOpen],
+ [
+ updateConfig,
+ activeGroup,
+ setGroup,
+ setOpen,
+ deleteGroup,
+ deleteGridLayout,
+ ],
);
const onSave = () => {
@@ -479,7 +495,11 @@ export function CameraGroupEdit({
{
message: "Camera group name already exists.",
},
- ),
+ )
+ .refine((value: string) => value.toLowerCase() !== "default", {
+ message: "Invalid camera group name.",
+ }),
+
cameras: z.array(z.string()).min(2, {
message: "You must select at least two cameras.",
}),
diff --git a/web/src/components/settings/General.tsx b/web/src/components/settings/General.tsx
index 70f6203b2..bdd30fdb7 100644
--- a/web/src/components/settings/General.tsx
+++ b/web/src/components/settings/General.tsx
@@ -1,19 +1,91 @@
import Heading from "@/components/ui/heading";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
-import { useEffect } from "react";
+import { useCallback, useEffect } from "react";
+import { Toaster } from "sonner";
+import { toast } from "sonner";
+import { Separator } from "../ui/separator";
+import { Button } from "../ui/button";
+import useSWR from "swr";
+import { FrigateConfig } from "@/types/frigateConfig";
+import { del as delData } from "idb-keyval";
export default function General() {
+ const { data: config } = useSWR("config");
+
+ const clearStoredLayouts = useCallback(() => {
+ if (!config) {
+ return [];
+ }
+
+ Object.entries(config.camera_groups).forEach(async (value) => {
+ await delData(`${value[0]}-draggable-layout`)
+ .then(() => {
+ toast.success(`Cleared stored layout for ${value[0]}`, {
+ position: "top-center",
+ });
+ })
+ .catch((error) => {
+ toast.error(
+ `Failed to clear stored layout: ${error.response.data.message}`,
+ { position: "top-center" },
+ );
+ });
+ });
+ }, [config]);
+
useEffect(() => {
document.title = "General Settings - Frigate";
}, []);
return (
<>
- Settings
-
-
{}} />
-
+
+
+
+
+ General Settings
+
+
+
+
+
+
Stored Layouts
+
+
+ The layout of cameras in a camera group can be
+ dragged/resized. The positions are stored in your browser's
+ local storage.
+
+
+
+
+
+
+
+
+
+
+
Low Data Mode
+
+
+ Not yet implemented. Default: disabled
+
+
+
+
+ {}}
+ />
+
+
+
+
+
>
);
diff --git a/web/src/hooks/use-overlay-state.tsx b/web/src/hooks/use-overlay-state.tsx
index 391712c6a..f39717288 100644
--- a/web/src/hooks/use-overlay-state.tsx
+++ b/web/src/hooks/use-overlay-state.tsx
@@ -33,14 +33,15 @@ export function useOverlayState(
export function usePersistedOverlayState(
key: string,
defaultValue: S | undefined = undefined,
-): [S | undefined, (value: S | undefined, replace?: boolean) => void] {
- const [persistedValue, setPersistedValue] = usePersistence(
- key,
- defaultValue,
- );
+): [
+ S | undefined,
+ (value: S | undefined, replace?: boolean) => void,
+ () => void,
+] {
+ const [persistedValue, setPersistedValue, , deletePersistedValue] =
+ usePersistence(key, defaultValue);
const location = useLocation();
const navigate = useNavigate();
-
const currentLocationState = useMemo(() => location.state, [location]);
const setOverlayStateValue = useCallback(
@@ -63,6 +64,7 @@ export function usePersistedOverlayState(
return [
overlayStateValue ?? persistedValue ?? defaultValue,
setOverlayStateValue,
+ deletePersistedValue,
];
}
diff --git a/web/src/hooks/use-persistence.ts b/web/src/hooks/use-persistence.ts
index 1b2f2a4d4..8762c1970 100644
--- a/web/src/hooks/use-persistence.ts
+++ b/web/src/hooks/use-persistence.ts
@@ -1,10 +1,11 @@
import { useEffect, useState, useCallback } from "react";
-import { get as getData, set as setData } from "idb-keyval";
+import { get as getData, set as setData, del as delData } from "idb-keyval";
type usePersistenceReturn = [
value: S | undefined,
setValue: (value: S | undefined) => void,
loaded: boolean,
+ deleteValue: () => void,
];
export function usePersistence(
@@ -26,6 +27,11 @@ export function usePersistence(
[key],
);
+ const deleteValue = useCallback(async () => {
+ await delData(key);
+ setInternalValue(defaultValue);
+ }, [key, defaultValue]);
+
useEffect(() => {
setLoaded(false);
setInternalValue(defaultValue);
@@ -41,5 +47,5 @@ export function usePersistence(
load();
}, [key, defaultValue, setValue]);
- return [value, setValue, loaded];
+ return [value, setValue, loaded, deleteValue];
}
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx
index 387aa3856..bae56b6bb 100644
--- a/web/src/pages/Live.tsx
+++ b/web/src/pages/Live.tsx
@@ -89,6 +89,7 @@ function Live() {
return (
diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx
new file mode 100644
index 000000000..f0a64b92a
--- /dev/null
+++ b/web/src/views/live/DraggableGridLayout.tsx
@@ -0,0 +1,461 @@
+import { usePersistence } from "@/hooks/use-persistence";
+import {
+ BirdseyeConfig,
+ CameraConfig,
+ FrigateConfig,
+} from "@/types/frigateConfig";
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { Layout, Responsive, WidthProvider } from "react-grid-layout";
+import "react-grid-layout/css/styles.css";
+import "react-resizable/css/styles.css";
+import { LivePlayerMode } from "@/types/live";
+import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useResizeObserver } from "@/hooks/resize-observer";
+import { isEqual } from "lodash";
+import useSWR from "swr";
+import { isSafari } from "react-device-detect";
+import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
+import LivePlayer from "@/components/player/LivePlayer";
+import { Button } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { IoClose } from "react-icons/io5";
+import { LuMoveDiagonal2 } from "react-icons/lu";
+
+type DraggableGridLayoutProps = {
+ cameras: CameraConfig[];
+ cameraGroup: string;
+ cameraRef: (node: HTMLElement | null) => void;
+ includeBirdseye: boolean;
+ onSelectCamera: (camera: string) => void;
+ windowVisible: boolean;
+ visibleCameras: string[];
+};
+export default function DraggableGridLayout({
+ cameras,
+ cameraGroup,
+ cameraRef,
+ includeBirdseye,
+ onSelectCamera,
+ windowVisible,
+ visibleCameras,
+}: DraggableGridLayoutProps) {
+ const { data: config } = useSWR("config");
+ const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
+
+ const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
+
+ const [gridLayout, setGridLayout, isGridLayoutLoaded] = usePersistence<
+ Layout[]
+ >(`${cameraGroup}-draggable-layout`);
+
+ const [currentCameras, setCurrentCameras] = useState();
+ const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
+ useState();
+ const [currentGridLayout, setCurrentGridLayout] = useState<
+ Layout[] | undefined
+ >();
+
+ const [isEditMode, setIsEditMode] = useState(false);
+
+ const handleLayoutChange = useCallback(
+ (currentLayout: Layout[]) => {
+ if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
+ return;
+ }
+ // save layout to idb
+ setGridLayout(currentLayout);
+ },
+ [setGridLayout, isGridLayoutLoaded, gridLayout, currentGridLayout],
+ );
+
+ const generateLayout = useCallback(() => {
+ if (!isGridLayoutLoaded) {
+ return;
+ }
+
+ const cameraNames =
+ includeBirdseye && birdseyeConfig?.enabled
+ ? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
+ : cameras.map((camera) => camera?.name || "");
+
+ const optionsMap: Layout[] = currentGridLayout
+ ? currentGridLayout.filter((layout) => cameraNames?.includes(layout.i))
+ : [];
+
+ cameraNames.forEach((cameraName, index) => {
+ const existingLayout = optionsMap.find(
+ (layout) => layout.i === cameraName,
+ );
+
+ // Skip if the camera already exists in the layout
+ if (existingLayout) {
+ return;
+ }
+
+ let aspectRatio;
+ let col;
+
+ // Handle "birdseye" camera as a special case
+ if (cameraName === "birdseye") {
+ aspectRatio =
+ (birdseyeConfig?.width || 1) / (birdseyeConfig?.height || 1);
+ col = 0; // Set birdseye camera in the first column
+ } else {
+ const camera = cameras.find((cam) => cam.name === cameraName);
+ aspectRatio =
+ (camera && camera?.detect.width / camera?.detect.height) || 16 / 9;
+ col = index % 3; // Regular cameras distributed across columns
+ }
+
+ // Calculate layout options based on aspect ratio
+ const columnsPerPlayer = 4;
+ let height;
+ let width;
+
+ if (aspectRatio < 1) {
+ // Portrait
+ height = 2 * columnsPerPlayer;
+ width = columnsPerPlayer;
+ } else if (aspectRatio > 2) {
+ // Wide
+ height = 1 * columnsPerPlayer;
+ width = 2 * columnsPerPlayer;
+ } else {
+ // Landscape
+ height = 1 * columnsPerPlayer;
+ width = columnsPerPlayer;
+ }
+
+ const options = {
+ i: cameraName,
+ x: col * width,
+ y: 0, // don't set y, grid does automatically
+ w: width,
+ h: height,
+ isDraggable: isEditMode,
+ isResizable: isEditMode,
+ };
+
+ optionsMap.push(options);
+ });
+
+ return optionsMap;
+ }, [
+ cameras,
+ isEditMode,
+ isGridLayoutLoaded,
+ currentGridLayout,
+ includeBirdseye,
+ birdseyeConfig,
+ ]);
+
+ const toggleEditMode = useCallback(() => {
+ if (currentGridLayout) {
+ const updatedGridLayout = currentGridLayout.map((layout) => ({
+ ...layout,
+ isDraggable: !isEditMode,
+ isResizable: !isEditMode,
+ }));
+ if (isEditMode) {
+ setGridLayout(updatedGridLayout);
+ setCurrentGridLayout(updatedGridLayout);
+ } else {
+ setGridLayout(updatedGridLayout);
+ }
+ setIsEditMode((prevIsEditMode) => !prevIsEditMode);
+ }
+ }, [currentGridLayout, isEditMode, setGridLayout]);
+
+ useEffect(() => {
+ if (isGridLayoutLoaded) {
+ if (gridLayout) {
+ // set current grid layout from loaded
+ setCurrentGridLayout(gridLayout);
+ } else {
+ // idb is empty, set it with an initial layout
+ setGridLayout(generateLayout());
+ }
+ }
+ }, [
+ isEditMode,
+ gridLayout,
+ currentGridLayout,
+ setGridLayout,
+ isGridLayoutLoaded,
+ generateLayout,
+ ]);
+
+ useEffect(() => {
+ if (
+ !isEqual(cameras, currentCameras) ||
+ includeBirdseye !== currentIncludeBirdseye
+ ) {
+ setCurrentCameras(cameras);
+ setCurrentIncludeBirdseye(includeBirdseye);
+
+ // set new grid layout in idb
+ setGridLayout(generateLayout());
+ }
+ }, [
+ cameras,
+ includeBirdseye,
+ currentCameras,
+ currentIncludeBirdseye,
+ setCurrentGridLayout,
+ generateLayout,
+ setGridLayout,
+ isGridLayoutLoaded,
+ ]);
+
+ const gridContainerRef = useRef(null);
+
+ const [{ width: containerWidth }] = useResizeObserver(gridContainerRef);
+
+ const cellHeight = useMemo(() => {
+ const aspectRatio = 16 / 9;
+ const totalMarginWidth = 11 * 13; // 11 margins with 13px each
+ const rowHeight =
+ ((containerWidth ?? window.innerWidth) - totalMarginWidth) /
+ (13 * aspectRatio);
+ return rowHeight;
+ }, [containerWidth]);
+
+ return (
+ <>
+ {!isGridLayoutLoaded || !currentGridLayout ? (
+
+ {includeBirdseye && birdseyeConfig?.enabled && (
+
+ )}
+ {cameras.map((camera) => {
+ return (
+
+ );
+ })}
+
+ ) : (
+
+
+ {includeBirdseye && birdseyeConfig?.enabled && (
+ onSelectCamera("birdseye")}
+ >
+ {isEditMode && (
+ <>
+
+
+
+
+ >
+ )}
+
+ )}
+ {cameras.map((camera) => {
+ let grow;
+ const aspectRatio = camera.detect.width / camera.detect.height;
+ if (aspectRatio > ASPECT_WIDE_LAYOUT) {
+ grow = `aspect-wide`;
+ } else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
+ grow = `aspect-tall`;
+ } else {
+ grow = "aspect-video";
+ }
+ return (
+ {
+ !isEditMode && onSelectCamera(camera.name);
+ }}
+ >
+ {isEditMode && (
+ <>
+
+
+
+
+ >
+ )}
+
+ );
+ })}
+
+
+
+
+
+
+
+ {isEditMode ? "Exit Editing" : "Edit Layout"}
+
+
+
+
+ )}
+ >
+ );
+}
+
+type BirdseyeLivePlayerGridItemProps = {
+ style?: React.CSSProperties;
+ className?: string;
+ onMouseDown?: React.MouseEventHandler;
+ onMouseUp?: React.MouseEventHandler;
+ onTouchEnd?: React.TouchEventHandler;
+ children?: React.ReactNode;
+ birdseyeConfig: BirdseyeConfig;
+ liveMode: LivePlayerMode;
+ onClick: () => void;
+};
+
+const BirdseyeLivePlayerGridItem = React.forwardRef<
+ HTMLDivElement,
+ BirdseyeLivePlayerGridItemProps
+>(
+ (
+ {
+ style,
+ className,
+ onMouseDown,
+ onMouseUp,
+ onTouchEnd,
+ children,
+ birdseyeConfig,
+ liveMode,
+ onClick,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+
+ {children}
+
+ );
+ },
+);
+
+type LivePlayerGridItemProps = {
+ style?: React.CSSProperties;
+ className: string;
+ onMouseDown?: React.MouseEventHandler;
+ onMouseUp?: React.MouseEventHandler;
+ onTouchEnd?: React.TouchEventHandler;
+ children?: React.ReactNode;
+ cameraRef: (node: HTMLElement | null) => void;
+ windowVisible: boolean;
+ cameraConfig: CameraConfig;
+ preferredLiveMode: LivePlayerMode;
+ onClick: () => void;
+};
+
+const LivePlayerGridItem = React.forwardRef<
+ HTMLDivElement,
+ LivePlayerGridItemProps
+>(
+ (
+ {
+ style,
+ className,
+ onMouseDown,
+ onMouseUp,
+ onTouchEnd,
+ children,
+ cameraRef,
+ windowVisible,
+ cameraConfig,
+ preferredLiveMode,
+ onClick,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+
+ {children}
+
+ );
+ },
+);
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx
index 31be2dce1..1c7d2d3e0 100644
--- a/web/src/views/live/LiveDashboardView.tsx
+++ b/web/src/views/live/LiveDashboardView.tsx
@@ -12,16 +12,24 @@ import { usePersistence } from "@/hooks/use-persistence";
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
import { ReviewSegment } from "@/types/review";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { isDesktop, isMobile, isSafari } from "react-device-detect";
+import {
+ isDesktop,
+ isMobile,
+ isMobileOnly,
+ isSafari,
+} from "react-device-detect";
import useSWR from "swr";
+import DraggableGridLayout from "./DraggableGridLayout";
type LiveDashboardViewProps = {
cameras: CameraConfig[];
+ cameraGroup?: string;
includeBirdseye: boolean;
onSelectCamera: (camera: string) => void;
};
export default function LiveDashboardView({
cameras,
+ cameraGroup,
includeBirdseye,
onSelectCamera,
}: LiveDashboardViewProps) {
@@ -29,7 +37,7 @@ export default function LiveDashboardView({
// layout
- const [layout, setLayout] = usePersistence<"grid" | "list">(
+ const [mobileLayout, setMobileLayout] = usePersistence<"grid" | "list">(
"live-layout",
isDesktop ? "grid" : "list",
);
@@ -150,25 +158,25 @@ export default function LiveDashboardView({
@@ -187,41 +195,53 @@ export default function LiveDashboardView({
)}
-
- {includeBirdseye && birdseyeConfig?.enabled && (
-
onSelectCamera("birdseye")}
- />
- )}
- {cameras.map((camera) => {
- let grow;
- const aspectRatio = camera.detect.width / camera.detect.height;
- if (aspectRatio > 2) {
- grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`;
- } else if (aspectRatio < 1) {
- grow = `${layout == "grid" ? "row-span-2 aspect-tall md:h-full" : ""} aspect-tall`;
- } else {
- grow = "aspect-video";
- }
- return (
- onSelectCamera(camera.name)}
+ {!cameraGroup || cameraGroup == "default" || isMobileOnly ? (
+
+ {includeBirdseye && birdseyeConfig?.enabled && (
+ onSelectCamera("birdseye")}
/>
- );
- })}
-
+ )}
+ {cameras.map((camera) => {
+ let grow;
+ const aspectRatio = camera.detect.width / camera.detect.height;
+ if (aspectRatio > 2) {
+ grow = `${mobileLayout == "grid" ? "col-span-2" : ""} aspect-wide`;
+ } else if (aspectRatio < 1) {
+ grow = `${mobileLayout == "grid" ? "row-span-2 aspect-tall md:h-full" : ""} aspect-tall`;
+ } else {
+ grow = "aspect-video";
+ }
+ return (
+ onSelectCamera(camera.name)}
+ />
+ );
+ })}
+
+ ) : (
+
+ )}
);
}
From e7ba55691996f55ce0a0f18effd1f2df73558607 Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Wed, 8 May 2024 06:34:22 -0600
Subject: [PATCH 47/56] Fix setting manual event update time (#11290)
---
frigate/review/maintainer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py
index 295641656..8963c555c 100644
--- a/frigate/review/maintainer.py
+++ b/frigate/review/maintainer.py
@@ -511,7 +511,7 @@ class ReviewSegmentMaintainer(threading.Thread):
manual_info["label"]
)
# temporarily make it so this event can not end
- self.active_review_segments[camera] = sys.maxsize
+ self.active_review_segments[camera].last_update = sys.maxsize
elif manual_info["state"] == ManualEventState.complete:
self.active_review_segments[camera].last_update = manual_info[
"end_time"
From db8c820677bbecb73fd5fd7c06248f1abe86be47 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 8 May 2024 07:53:22 -0500
Subject: [PATCH 48/56] Draggable camera grid tweaks (#11291)
* better math and other tweaks
* change icon
---
web/src/views/live/DraggableGridLayout.tsx | 189 ++++++++++++++-------
web/src/views/live/LiveDashboardView.tsx | 74 +++++---
2 files changed, 177 insertions(+), 86 deletions(-)
diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx
index f0a64b92a..44f7e0b53 100644
--- a/web/src/views/live/DraggableGridLayout.tsx
+++ b/web/src/views/live/DraggableGridLayout.tsx
@@ -7,6 +7,7 @@ import {
import React, {
useCallback,
useEffect,
+ useLayoutEffect,
useMemo,
useRef,
useState,
@@ -20,7 +21,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
import { isEqual } from "lodash";
import useSWR from "swr";
-import { isSafari } from "react-device-detect";
+import { isDesktop, isMobile, isSafari } from "react-device-detect";
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
import LivePlayer from "@/components/player/LivePlayer";
import { Button } from "@/components/ui/button";
@@ -30,25 +31,32 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { IoClose } from "react-icons/io5";
-import { LuMoveDiagonal2 } from "react-icons/lu";
+import { LuMove } from "react-icons/lu";
+import { cn } from "@/lib/utils";
type DraggableGridLayoutProps = {
cameras: CameraConfig[];
cameraGroup: string;
cameraRef: (node: HTMLElement | null) => void;
+ containerRef: React.RefObject;
includeBirdseye: boolean;
onSelectCamera: (camera: string) => void;
windowVisible: boolean;
visibleCameras: string[];
+ isEditMode: boolean;
+ setIsEditMode: React.Dispatch>;
};
export default function DraggableGridLayout({
cameras,
cameraGroup,
+ containerRef,
cameraRef,
includeBirdseye,
onSelectCamera,
windowVisible,
visibleCameras,
+ isEditMode,
+ setIsEditMode,
}: DraggableGridLayoutProps) {
const { data: config } = useSWR("config");
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
@@ -66,8 +74,6 @@ export default function DraggableGridLayout({
Layout[] | undefined
>();
- const [isEditMode, setIsEditMode] = useState(false);
-
const handleLayoutChange = useCallback(
(currentLayout: Layout[]) => {
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
@@ -160,12 +166,12 @@ export default function DraggableGridLayout({
birdseyeConfig,
]);
- const toggleEditMode = useCallback(() => {
+ useEffect(() => {
if (currentGridLayout) {
const updatedGridLayout = currentGridLayout.map((layout) => ({
...layout,
- isDraggable: !isEditMode,
- isResizable: !isEditMode,
+ isDraggable: isEditMode,
+ isResizable: isEditMode,
}));
if (isEditMode) {
setGridLayout(updatedGridLayout);
@@ -173,9 +179,10 @@ export default function DraggableGridLayout({
} else {
setGridLayout(updatedGridLayout);
}
- setIsEditMode((prevIsEditMode) => !prevIsEditMode);
}
- }, [currentGridLayout, isEditMode, setGridLayout]);
+ // we know that these deps are correct
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isEditMode, setGridLayout]);
useEffect(() => {
if (isGridLayoutLoaded) {
@@ -218,31 +225,58 @@ export default function DraggableGridLayout({
isGridLayoutLoaded,
]);
+ const [marginValue, setMarginValue] = useState(16);
+
+ // calculate margin value for browsers that don't have default font size of 16px
+ useLayoutEffect(() => {
+ const calculateRemValue = () => {
+ const htmlElement = document.documentElement;
+ const fontSize = window.getComputedStyle(htmlElement).fontSize;
+ setMarginValue(parseFloat(fontSize));
+ };
+
+ calculateRemValue();
+ }, []);
+
const gridContainerRef = useRef(null);
- const [{ width: containerWidth }] = useResizeObserver(gridContainerRef);
+ const [{ width: containerWidth, height: containerHeight }] =
+ useResizeObserver(gridContainerRef);
+
+ const hasScrollbar = useMemo(() => {
+ return (
+ containerHeight &&
+ containerRef.current &&
+ containerRef.current.offsetHeight <
+ (gridContainerRef.current?.scrollHeight ?? 0)
+ );
+ }, [containerRef, gridContainerRef, containerHeight]);
const cellHeight = useMemo(() => {
const aspectRatio = 16 / 9;
- const totalMarginWidth = 11 * 13; // 11 margins with 13px each
- const rowHeight =
- ((containerWidth ?? window.innerWidth) - totalMarginWidth) /
- (13 * aspectRatio);
- return rowHeight;
- }, [containerWidth]);
+ // subtract container margin, 1 camera takes up at least 4 rows
+ // account for additional margin on bottom of each row
+ return (
+ ((containerWidth ?? window.innerWidth) - 2 * marginValue) /
+ 12 /
+ aspectRatio -
+ marginValue +
+ marginValue / 4
+ );
+ }, [containerWidth, marginValue]);
return (
<>
{!isGridLayoutLoaded || !currentGridLayout ? (
{includeBirdseye && birdseyeConfig?.enabled && (
-
+
)}
{cameras.map((camera) => {
return (
);
})}
@@ -264,37 +298,33 @@ export default function DraggableGridLayout({
rowHeight={cellHeight}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
- margin={[16, 16]}
- containerPadding={[8, 8]}
- resizeHandles={["sw", "nw", "se", "ne"]}
+ margin={[marginValue, marginValue]}
+ containerPadding={[0, isEditMode ? 6 : 3]}
+ resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
onDragStop={handleLayoutChange}
onResizeStop={handleLayoutChange}
>
{includeBirdseye && birdseyeConfig?.enabled && (
onSelectCamera("birdseye")}
>
- {isEditMode && (
- <>
-
-
-
-
- >
- )}
+ {isEditMode && }
)}
{cameras.map((camera) => {
let grow;
const aspectRatio = camera.detect.width / camera.detect.height;
if (aspectRatio > ASPECT_WIDE_LAYOUT) {
- grow = `aspect-wide`;
+ grow = `aspect-wide w-full`;
} else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
- grow = `aspect-tall`;
+ grow = `aspect-tall h-full`;
} else {
grow = "aspect-video";
}
@@ -302,7 +332,12 @@ export default function DraggableGridLayout({
- {isEditMode && (
- <>
-
-
-
-
- >
- )}
+ {isEditMode && }
);
})}
-
-
-
-
-
-
- {isEditMode ? "Exit Editing" : "Edit Layout"}
-
-
-
+ {isDesktop && (
+
+ )}
)}
>
);
}
+type DesktopEditLayoutButtonProps = {
+ isEditMode?: boolean;
+ setIsEditMode: React.Dispatch>;
+ hasScrollbar?: boolean | 0 | null;
+};
+
+function DesktopEditLayoutButton({
+ isEditMode,
+ setIsEditMode,
+ hasScrollbar,
+}: DesktopEditLayoutButtonProps) {
+ return (
+
+
+
+
+
+
+ {isEditMode ? "Exit Editing" : "Edit Layout"}
+
+
+
+ );
+}
+
+function CornerCircles() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
type BirdseyeLivePlayerGridItemProps = {
style?: React.CSSProperties;
className?: string;
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx
index 1c7d2d3e0..3a9616195 100644
--- a/web/src/views/live/LiveDashboardView.tsx
+++ b/web/src/views/live/LiveDashboardView.tsx
@@ -17,9 +17,12 @@ import {
isMobile,
isMobileOnly,
isSafari,
+ isTablet,
} from "react-device-detect";
import useSWR from "swr";
import DraggableGridLayout from "./DraggableGridLayout";
+import { IoClose } from "react-icons/io5";
+import { LuMove } from "react-icons/lu";
type LiveDashboardViewProps = {
cameras: CameraConfig[];
@@ -42,6 +45,9 @@ export default function LiveDashboardView({
isDesktop ? "grid" : "list",
);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const containerRef = useRef(null);
+
// recent events
const { payload: eventUpdate } = useFrigateReviews();
const { data: allEvents, mutate: updateEvents } = useSWR([
@@ -148,37 +154,52 @@ export default function LiveDashboardView({
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
return (
-
+
{isMobile && (
-
-
-
-
+ {(!cameraGroup || cameraGroup == "default" || isMobileOnly) && (
+
+
+
+
+ )}
+ {cameraGroup && cameraGroup !== "default" && isTablet && (
+
+
+
+ )}
)}
@@ -235,11 +256,14 @@ export default function LiveDashboardView({
)}
From 3ed89ec04215cc640c275b10e1e6fd49300e4098 Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Wed, 8 May 2024 08:46:10 -0600
Subject: [PATCH 49/56] Simplify preview refreshing with custom hook (#11293)
---
web/src/components/card/AnimatedEventCard.tsx | 15 +++--
web/src/hooks/use-camera-previews.ts | 57 +++++++++++++++++++
web/src/pages/Events.tsx | 53 ++++-------------
3 files changed, 79 insertions(+), 46 deletions(-)
create mode 100644 web/src/hooks/use-camera-previews.ts
diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx
index 126dd4511..125b6dada 100644
--- a/web/src/components/card/AnimatedEventCard.tsx
+++ b/web/src/components/card/AnimatedEventCard.tsx
@@ -7,12 +7,12 @@ import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { useNavigate } from "react-router-dom";
import { RecordingStartingPoint } from "@/types/record";
import axios from "axios";
-import { Preview } from "@/types/preview";
import {
InProgressPreview,
VideoPreview,
} from "../player/PreviewThumbnailPlayer";
import { isCurrentHour } from "@/utils/dateUtil";
+import { useCameraPreviews } from "@/hooks/use-camera-previews";
type AnimatedEventCardProps = {
event: ReviewSegment;
@@ -24,10 +24,15 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
// preview
- const { data: previews } = useSWR(
- currentHour
- ? null
- : `/preview/${event.camera}/start/${Math.round(event.start_time)}/end/${Math.round(event.end_time || event.start_time + 20)}`,
+ const previews = useCameraPreviews(
+ {
+ after: Math.round(event.start_time),
+ before: Math.round(event.end_time || event.start_time + 20),
+ },
+ {
+ camera: event.camera,
+ fetchPreviews: !currentHour,
+ },
);
// interaction
diff --git a/web/src/hooks/use-camera-previews.ts b/web/src/hooks/use-camera-previews.ts
new file mode 100644
index 000000000..c01fd263a
--- /dev/null
+++ b/web/src/hooks/use-camera-previews.ts
@@ -0,0 +1,57 @@
+import { Preview } from "@/types/preview";
+import { TimeRange } from "@/types/timeline";
+import { useEffect, useState } from "react";
+import useSWR from "swr";
+
+type OptionalCameraPreviewProps = {
+ camera?: string;
+ autoRefresh?: boolean;
+ fetchPreviews?: boolean;
+};
+
+export function useCameraPreviews(
+ initialTimeRange: TimeRange,
+ {
+ camera = "all",
+ autoRefresh = true,
+ fetchPreviews = true,
+ }: OptionalCameraPreviewProps,
+) {
+ const [timeRange, setTimeRange] = useState(initialTimeRange);
+
+ useEffect(() => {
+ setTimeRange(initialTimeRange);
+ }, [initialTimeRange]);
+
+ const { data: allPreviews } = useSWR(
+ fetchPreviews
+ ? `preview/${camera}/start/${timeRange.after}/end/${timeRange.before}`
+ : null,
+ { revalidateOnFocus: false, revalidateOnReconnect: false },
+ );
+
+ // Set a timeout to update previews on the hour
+ useEffect(() => {
+ if (!autoRefresh || !fetchPreviews || !allPreviews) {
+ return;
+ }
+
+ const callback = () => {
+ const nextPreviewStart = new Date(
+ allPreviews[allPreviews.length - 1].end * 1000,
+ );
+ nextPreviewStart.setHours(nextPreviewStart.getHours() + 1);
+
+ if (Date.now() > nextPreviewStart.getTime()) {
+ setTimeRange({ after: timeRange.after, before: Date.now() / 1000 });
+ }
+ };
+ document.addEventListener("focusin", callback);
+
+ return () => {
+ document.removeEventListener("focusin", callback);
+ };
+ }, [allPreviews, autoRefresh, fetchPreviews, timeRange]);
+
+ return allPreviews;
+}
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index 0cc3cd6b5..bf325f064 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -1,9 +1,9 @@
import ActivityIndicator from "@/components/indicators/activity-indicator";
import useApiFilter from "@/hooks/use-api-filter";
+import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { useTimezone } from "@/hooks/use-date-utils";
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig";
-import { Preview } from "@/types/preview";
import { RecordingStartingPoint } from "@/types/record";
import {
ReviewFilter,
@@ -161,7 +161,6 @@ export default function Events() {
}, [updateSummary]);
// preview videos
- const [previewKey, setPreviewKey] = useState(0);
const previewTimes = useMemo(() => {
if (!reviews || reviews.length == 0) {
return undefined;
@@ -170,50 +169,22 @@ export default function Events() {
const startDate = new Date();
startDate.setMinutes(0, 0, 0);
- let endDate;
- if (previewKey == 0) {
- endDate = new Date(reviews.at(-1)?.end_time || 0);
- endDate.setHours(0, 0, 0, 0);
- } else {
- endDate = new Date();
- endDate.setMilliseconds(0);
- }
+ const endDate = new Date(reviews.at(-1)?.end_time || 0);
+ endDate.setHours(0, 0, 0, 0);
return {
- start: startDate.getTime() / 1000,
- end: endDate.getTime() / 1000,
+ after: startDate.getTime() / 1000,
+ before: endDate.getTime() / 1000,
};
- }, [reviews, previewKey]);
- const { data: allPreviews } = useSWR(
- previewTimes
- ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}`
- : null,
- { revalidateOnFocus: false, revalidateOnReconnect: false },
+ }, [reviews]);
+
+ const allPreviews = useCameraPreviews(
+ previewTimes ?? { after: 0, before: 0 },
+ {
+ fetchPreviews: previewTimes != undefined,
+ },
);
- // Set a timeout to update previews on the hour
- useEffect(() => {
- if (!allPreviews || allPreviews.length == 0) {
- return;
- }
-
- const callback = () => {
- const nextPreviewStart = new Date(
- allPreviews[allPreviews.length - 1].end * 1000,
- );
- nextPreviewStart.setHours(nextPreviewStart.getHours() + 1);
-
- if (Date.now() > nextPreviewStart.getTime()) {
- setPreviewKey(10 * Math.random());
- }
- };
- document.addEventListener("focusin", callback);
-
- return () => {
- document.removeEventListener("focusin", callback);
- };
- }, [allPreviews]);
-
// review status
const markAllItemsAsReviewed = useCallback(
From 2be15b6c017f50097d32c3cb9f575a0501f50843 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 8 May 2024 09:46:31 -0500
Subject: [PATCH 50/56] Improve config validation error messages (#11292)
---
frigate/app.py | 10 ++++++--
frigate/config.py | 62 +++++++++++++++++++++++++++--------------------
2 files changed, 44 insertions(+), 28 deletions(-)
diff --git a/frigate/app.py b/frigate/app.py
index 1306ee97d..c2a489b75 100644
--- a/frigate/app.py
+++ b/frigate/app.py
@@ -16,6 +16,7 @@ import psutil
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
+from pydantic import ValidationError
from frigate.api.app import create_app
from frigate.comms.config_updater import ConfigPublisher
@@ -611,8 +612,13 @@ class FrigateApp:
print("*************************************************************")
print("*** Config Validation Errors ***")
print("*************************************************************")
- print(e)
- print(traceback.format_exc())
+ if isinstance(e, ValidationError):
+ for error in e.errors():
+ location = ".".join(str(item) for item in error["loc"])
+ print(f"{location}: {error['msg']}")
+ else:
+ print(e)
+ print(traceback.format_exc())
print("*************************************************************")
print("*** End Config Validation Errors ***")
print("*************************************************************")
diff --git a/frigate/config.py b/frigate/config.py
index 602f03f1f..008176956 100644
--- a/frigate/config.py
+++ b/frigate/config.py
@@ -555,19 +555,24 @@ class ZoneConfig(BaseModel):
# old native resolution coordinates
if isinstance(coordinates, list):
explicit = any(p.split(",")[0] > "1.0" for p in coordinates)
- self._contour = np.array(
- [
- (
- [int(p.split(",")[0]), int(p.split(",")[1])]
- if explicit
- else [
- int(float(p.split(",")[0]) * frame_shape[1]),
- int(float(p.split(",")[1]) * frame_shape[0]),
- ]
- )
- for p in coordinates
- ]
- )
+ try:
+ self._contour = np.array(
+ [
+ (
+ [int(p.split(",")[0]), int(p.split(",")[1])]
+ if explicit
+ else [
+ int(float(p.split(",")[0]) * frame_shape[1]),
+ int(float(p.split(",")[1]) * frame_shape[0]),
+ ]
+ )
+ for p in coordinates
+ ]
+ )
+ except ValueError:
+ raise ValueError(
+ f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
+ )
if explicit:
self.coordinates = ",".join(
@@ -579,19 +584,24 @@ class ZoneConfig(BaseModel):
elif isinstance(coordinates, str):
points = coordinates.split(",")
explicit = any(p > "1.0" for p in points)
- self._contour = np.array(
- [
- (
- [int(points[i]), int(points[i + 1])]
- if explicit
- else [
- int(float(points[i]) * frame_shape[1]),
- int(float(points[i + 1]) * frame_shape[0]),
- ]
- )
- for i in range(0, len(points), 2)
- ]
- )
+ try:
+ self._contour = np.array(
+ [
+ (
+ [int(points[i]), int(points[i + 1])]
+ if explicit
+ else [
+ int(float(points[i]) * frame_shape[1]),
+ int(float(points[i + 1]) * frame_shape[0]),
+ ]
+ )
+ for i in range(0, len(points), 2)
+ ]
+ )
+ except ValueError:
+ raise ValueError(
+ f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
+ )
if explicit:
self.coordinates = ",".join(
From e1cbefb692ceb9315315eba54eb042717afd1212 Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Thu, 9 May 2024 07:19:41 -0600
Subject: [PATCH 51/56] Add link to system stats from status bar (#11303)
---
web/src/components/Statusbar.tsx | 56 ++++++++++++++++++--------------
1 file changed, 32 insertions(+), 24 deletions(-)
diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx
index 00374ad71..c2e81cf90 100644
--- a/web/src/components/Statusbar.tsx
+++ b/web/src/components/Statusbar.tsx
@@ -58,18 +58,20 @@ export default function Statusbar() {
{cpuPercent && (
-
-
- CPU {cpuPercent}%
-
+
+
+
+ CPU {cpuPercent}%
+
+
)}
{Object.entries(stats?.gpu_usages || {}).map(([name, stats]) => {
if (name == "error-gpu") {
@@ -93,18 +95,24 @@ export default function Statusbar() {
const gpu = parseInt(stats.gpu);
return (
-
-
- {gpuTitle} {gpu}%
-
+
+ {" "}
+
+
+ {gpuTitle} {gpu}%
+
+
);
})}
From 4bcbf7435af7df2387dc997372ffcbccfa2123c3 Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Thu, 9 May 2024 07:20:07 -0600
Subject: [PATCH 52/56] Web Deps (#11304)
* Update easy web deps
* Update react
* Update final
---
web/package-lock.json | 311 ++++++++++++++++++------------------------
web/package.json | 40 +++---
2 files changed, 152 insertions(+), 199 deletions(-)
diff --git a/web/package-lock.json b/web/package-lock.json
index 1df2344a4..1a680777a 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -29,32 +29,32 @@
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
- "apexcharts": "^3.48.0",
+ "apexcharts": "^3.49.0",
"axios": "^1.6.8",
"class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
+ "clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"date-fns": "^3.6.0",
"hls.js": "^1.5.8",
"idb-keyval": "^6.2.1",
- "immer": "^10.0.4",
+ "immer": "^10.1.1",
"konva": "^9.3.6",
"lodash": "^4.17.21",
- "lucide-react": "^0.372.0",
+ "lucide-react": "^0.378.0",
"monaco-yaml": "^5.1.1",
"next-themes": "^0.3.0",
- "react": "^18.2.0",
+ "react": "^18.3.1",
"react-apexcharts": "^1.4.1",
"react-day-picker": "^8.10.1",
"react-device-detect": "^2.2.3",
- "react-dom": "^18.2.0",
+ "react-dom": "^18.3.1",
"react-grid-layout": "^1.4.4",
- "react-hook-form": "^7.51.3",
- "react-icons": "^5.1.0",
+ "react-hook-form": "^7.51.4",
+ "react-icons": "^5.2.1",
"react-konva": "^18.2.10",
- "react-router-dom": "^6.22.3",
+ "react-router-dom": "^6.23.0",
"react-swipeable": "^7.0.1",
- "react-tracked": "^1.7.14",
+ "react-tracked": "^2.0.0",
"react-transition-group": "^4.4.5",
"react-use-websocket": "^4.8.1",
"react-zoom-pan-pinch": "^3.4.4",
@@ -66,17 +66,17 @@
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
- "vaul": "^0.9.0",
+ "vaul": "^0.9.1",
"vite-plugin-monaco-editor": "^1.1.0",
- "zod": "^3.22.5"
+ "zod": "^3.23.7"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.1.5",
- "@types/lodash": "^4.17.0",
- "@types/node": "^20.12.7",
- "@types/react": "^18.2.79",
- "@types/react-dom": "^18.2.25",
+ "@types/lodash": "^4.17.1",
+ "@types/node": "^20.12.11",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.0",
"@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10",
@@ -84,7 +84,7 @@
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@vitejs/plugin-react-swc": "^3.6.0",
- "@vitest/coverage-v8": "^1.4.0",
+ "@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
@@ -101,8 +101,8 @@
"prettier": "^3.2.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
- "vite": "^5.2.9",
- "vitest": "^1.4.0"
+ "vite": "^5.2.11",
+ "vitest": "^1.6.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2031,9 +2031,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.15.3",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
- "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz",
+ "integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==",
"engines": {
"node": ">=14.0.0"
}
@@ -2516,21 +2516,15 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
- "node_modules/@types/istanbul-lib-coverage": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
- "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
- "dev": true
- },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"node_modules/@types/lodash": {
- "version": "4.17.0",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
- "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz",
+ "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==",
"dev": true
},
"node_modules/@types/mute-stream": {
@@ -2543,9 +2537,9 @@
}
},
"node_modules/@types/node": {
- "version": "20.12.7",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
- "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
+ "version": "20.12.11",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
+ "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -2557,18 +2551,18 @@
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/react": {
- "version": "18.2.79",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz",
- "integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
- "version": "18.2.25",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz",
- "integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==",
+ "version": "18.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
+ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"devOptional": true,
"dependencies": {
"@types/react": "*"
@@ -2867,9 +2861,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz",
- "integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
+ "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -2884,24 +2878,23 @@
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
"strip-literal": "^2.0.0",
- "test-exclude": "^6.0.0",
- "v8-to-istanbul": "^9.2.0"
+ "test-exclude": "^6.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "1.4.0"
+ "vitest": "1.6.0"
}
},
"node_modules/@vitest/expect": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
- "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
+ "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"dev": true,
"dependencies": {
- "@vitest/spy": "1.4.0",
- "@vitest/utils": "1.4.0",
+ "@vitest/spy": "1.6.0",
+ "@vitest/utils": "1.6.0",
"chai": "^4.3.10"
},
"funding": {
@@ -2909,12 +2902,12 @@
}
},
"node_modules/@vitest/runner": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
- "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
+ "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"dev": true,
"dependencies": {
- "@vitest/utils": "1.4.0",
+ "@vitest/utils": "1.6.0",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -2950,9 +2943,9 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
- "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
+ "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -2964,9 +2957,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
- "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
+ "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -2976,9 +2969,9 @@
}
},
"node_modules/@vitest/utils": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
- "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
+ "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -3122,9 +3115,9 @@
}
},
"node_modules/apexcharts": {
- "version": "3.48.0",
- "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
- "integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.0.tgz",
+ "integrity": "sha512-2T9HnbQFLCuYRPndQLmh+bEQFoz0meUbvASaGgiSKDuYhWcLBodJtIpKql2aOtMx4B/sHrWW0dm90HsW4+h2PQ==",
"dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
@@ -3543,9 +3536,9 @@
}
},
"node_modules/clsx": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
- "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
@@ -3597,12 +3590,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true
- },
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@@ -4831,9 +4818,9 @@
}
},
"node_modules/immer": {
- "version": "10.0.4",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz",
- "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -5364,9 +5351,9 @@
}
},
"node_modules/lucide-react": {
- "version": "0.372.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.372.0.tgz",
- "integrity": "sha512-0cKdqmilHXWUwWAWnf6CrrjHD8YaqPMtLrmEHXolZusNTr9epULCsiJwIOHk2q1yFxdEwd96D4zShlAj67UJdA==",
+ "version": "0.378.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.378.0.tgz",
+ "integrity": "sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
@@ -6236,9 +6223,9 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/proxy-compare": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz",
- "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.0.tgz",
+ "integrity": "sha512-y44MCkgtZUCT9tZGuE278fB7PWVf7fRYy0vbRXAts2o5F0EfC4fIQrvQQGBJo1WJbFcVLXzApOscyJuZqHQc1w=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
@@ -6286,9 +6273,9 @@
]
},
"node_modules/react": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
- "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -6334,15 +6321,15 @@
}
},
"node_modules/react-dom": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
- "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
- "scheduler": "^0.23.0"
+ "scheduler": "^0.23.2"
},
"peerDependencies": {
- "react": "^18.2.0"
+ "react": "^18.3.1"
}
},
"node_modules/react-draggable": {
@@ -6384,9 +6371,9 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.51.3",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
- "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
+ "version": "7.51.4",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
+ "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==",
"engines": {
"node": ">=12.22.0"
},
@@ -6399,9 +6386,9 @@
}
},
"node_modules/react-icons": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.1.0.tgz",
- "integrity": "sha512-D3zug1270S4hbSlIRJ0CUS97QE1yNNKDjzQe3HqY0aefp2CBn9VgzgES27sRR2gOvFK+0CNx/BW0ggOESp6fqQ==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
+ "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
"peerDependencies": {
"react": "*"
}
@@ -6515,11 +6502,11 @@
}
},
"node_modules/react-router": {
- "version": "6.22.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
- "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz",
+ "integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==",
"dependencies": {
- "@remix-run/router": "1.15.3"
+ "@remix-run/router": "1.16.0"
},
"engines": {
"node": ">=14.0.0"
@@ -6529,12 +6516,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.22.3",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
- "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz",
+ "integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==",
"dependencies": {
- "@remix-run/router": "1.15.3",
- "react-router": "6.22.3"
+ "@remix-run/router": "1.16.0",
+ "react-router": "6.23.0"
},
"engines": {
"node": ">=14.0.0"
@@ -6575,26 +6562,16 @@
}
},
"node_modules/react-tracked": {
- "version": "1.7.14",
- "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.14.tgz",
- "integrity": "sha512-6UMlgQeRAGA+uyYzuQGm7kZB6ZQYFhc7sntgP7Oxwwd6M0Ud/POyb4K3QWT1eXvoifSa80nrAWnXWFGpOvbwkw==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-2.0.0.tgz",
+ "integrity": "sha512-Px8Ms9zhQKzAj3gnwQm6L+sJwzB0uPa8/BgHKOhB8bIuQEgB2iJfryM7GVja9oviiGAa7vtgEBtM+poT1E7V2w==",
"dependencies": {
- "proxy-compare": "2.6.0",
- "use-context-selector": "1.4.4"
+ "proxy-compare": "^3.0.0",
+ "use-context-selector": "^2.0.0"
},
"peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": "*",
- "react-native": "*",
+ "react": ">=18.0.0",
"scheduler": ">=0.19.0"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
}
},
"node_modules/react-transition-group": {
@@ -6969,9 +6946,9 @@
}
},
"node_modules/scheduler": {
- "version": "0.23.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
- "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -7740,22 +7717,12 @@
}
},
"node_modules/use-context-selector": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.4.tgz",
- "integrity": "sha512-pS790zwGxxe59GoBha3QYOwk8AFGp4DN6DOtH+eoqVmgBBRXVx4IlPDhJmmMiNQAgUaLlP+58aqRC3A4rdaSjg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-2.0.0.tgz",
+ "integrity": "sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==",
"peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": "*",
- "react-native": "*",
+ "react": ">=18.0.0",
"scheduler": ">=0.19.0"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
}
},
"node_modules/use-sidecar": {
@@ -7792,24 +7759,10 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
- "node_modules/v8-to-istanbul": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
- "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==",
- "dev": true,
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.12",
- "@types/istanbul-lib-coverage": "^2.0.1",
- "convert-source-map": "^2.0.0"
- },
- "engines": {
- "node": ">=10.12.0"
- }
- },
"node_modules/vaul": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.0.tgz",
- "integrity": "sha512-bZSySGbAHiTXmZychprnX/dE0EsSige88xtyyL3/MCRbrFotRPQZo7UdydGXZWw+CKbNOw5Ow8gwAo93/nB/Cg==",
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.1.tgz",
+ "integrity": "sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==",
"dependencies": {
"@radix-ui/react-dialog": "^1.0.4"
},
@@ -7819,9 +7772,9 @@
}
},
"node_modules/vite": {
- "version": "5.2.9",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz",
- "integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==",
+ "version": "5.2.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
+ "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.20.1",
@@ -7874,9 +7827,9 @@
}
},
"node_modules/vite-node": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
- "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
+ "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -7904,16 +7857,16 @@
}
},
"node_modules/vitest": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
- "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
+ "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true,
"dependencies": {
- "@vitest/expect": "1.4.0",
- "@vitest/runner": "1.4.0",
- "@vitest/snapshot": "1.4.0",
- "@vitest/spy": "1.4.0",
- "@vitest/utils": "1.4.0",
+ "@vitest/expect": "1.6.0",
+ "@vitest/runner": "1.6.0",
+ "@vitest/snapshot": "1.6.0",
+ "@vitest/spy": "1.6.0",
+ "@vitest/utils": "1.6.0",
"acorn-walk": "^8.3.2",
"chai": "^4.3.10",
"debug": "^4.3.4",
@@ -7925,9 +7878,9 @@
"std-env": "^3.5.0",
"strip-literal": "^2.0.0",
"tinybench": "^2.5.1",
- "tinypool": "^0.8.2",
+ "tinypool": "^0.8.3",
"vite": "^5.0.0",
- "vite-node": "1.4.0",
+ "vite-node": "1.6.0",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -7942,8 +7895,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "1.4.0",
- "@vitest/ui": "1.4.0",
+ "@vitest/browser": "1.6.0",
+ "@vitest/ui": "1.6.0",
"happy-dom": "*",
"jsdom": "*"
},
@@ -8216,9 +8169,9 @@
}
},
"node_modules/zod": {
- "version": "3.22.5",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.5.tgz",
- "integrity": "sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==",
+ "version": "3.23.7",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.7.tgz",
+ "integrity": "sha512-NBeIoqbtOiUMomACV/y+V3Qfs9+Okr18vR5c/5pHClPpufWOrsx8TENboDPe265lFdfewX2yBtNTLPvnmCxwog==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/web/package.json b/web/package.json
index 980223748..4638572a6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -34,32 +34,32 @@
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
- "apexcharts": "^3.48.0",
+ "apexcharts": "^3.49.0",
"axios": "^1.6.8",
"class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
+ "clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"date-fns": "^3.6.0",
"hls.js": "^1.5.8",
"idb-keyval": "^6.2.1",
- "immer": "^10.0.4",
+ "immer": "^10.1.1",
"konva": "^9.3.6",
"lodash": "^4.17.21",
- "lucide-react": "^0.372.0",
+ "lucide-react": "^0.378.0",
"monaco-yaml": "^5.1.1",
"next-themes": "^0.3.0",
- "react": "^18.2.0",
+ "react": "^18.3.1",
"react-apexcharts": "^1.4.1",
"react-day-picker": "^8.10.1",
"react-device-detect": "^2.2.3",
- "react-dom": "^18.2.0",
+ "react-dom": "^18.3.1",
"react-grid-layout": "^1.4.4",
- "react-hook-form": "^7.51.3",
- "react-icons": "^5.1.0",
+ "react-hook-form": "^7.51.4",
+ "react-icons": "^5.2.1",
"react-konva": "^18.2.10",
- "react-router-dom": "^6.22.3",
+ "react-router-dom": "^6.23.0",
"react-swipeable": "^7.0.1",
- "react-tracked": "^1.7.14",
+ "react-tracked": "^2.0.0",
"react-transition-group": "^4.4.5",
"react-use-websocket": "^4.8.1",
"react-zoom-pan-pinch": "^3.4.4",
@@ -71,17 +71,17 @@
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
- "vaul": "^0.9.0",
+ "vaul": "^0.9.1",
"vite-plugin-monaco-editor": "^1.1.0",
- "zod": "^3.22.5"
+ "zod": "^3.23.7"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.1.5",
- "@types/lodash": "^4.17.0",
- "@types/node": "^20.12.7",
- "@types/react": "^18.2.79",
- "@types/react-dom": "^18.2.25",
+ "@types/lodash": "^4.17.1",
+ "@types/node": "^20.12.11",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.0",
"@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10",
@@ -89,7 +89,7 @@
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@vitejs/plugin-react-swc": "^3.6.0",
- "@vitest/coverage-v8": "^1.4.0",
+ "@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
@@ -101,12 +101,12 @@
"fake-indexeddb": "^5.0.2",
"jest-websocket-mock": "^2.5.0",
"jsdom": "^24.0.0",
- "msw": "^2.2.14",
+ "msw": "^2.3.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
- "vite": "^5.2.9",
- "vitest": "^1.4.0"
+ "vite": "^5.2.11",
+ "vitest": "^1.6.0"
}
}
From 4216d080998ce20b6fa299df3fbd3649a102a31d Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Thu, 9 May 2024 07:20:33 -0600
Subject: [PATCH 53/56] Backend and webui fixes (#11309)
* Ensure that items without end times are set to not have a snapshot
* Save full frame if no frame is currently saved
* Webui fixes
* Cleanup
---
frigate/app.py | 6 ++--
frigate/review/maintainer.py | 34 +++++++++++++++++++
.../player/dynamic/DynamicVideoPlayer.tsx | 2 +-
web/src/hooks/use-camera-previews.ts | 2 +-
4 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/frigate/app.py b/frigate/app.py
index c2a489b75..1fce7c1ac 100644
--- a/frigate/app.py
+++ b/frigate/app.py
@@ -683,9 +683,9 @@ class FrigateApp:
self.stop_event.set()
# set an end_time on entries without an end_time before exiting
- Event.update(end_time=datetime.datetime.now().timestamp()).where(
- Event.end_time == None
- ).execute()
+ Event.update(
+ end_time=datetime.datetime.now().timestamp(), has_snapshot=False
+ ).where(Event.end_time == None).execute()
ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where(
ReviewSegment.end_time == None
).execute()
diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py
index 8963c555c..77d3d2a6b 100644
--- a/frigate/review/maintainer.py
+++ b/frigate/review/maintainer.py
@@ -110,6 +110,18 @@ class PendingReviewSegment:
self.frame_path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
)
+ def save_full_frame(self, camera_config: CameraConfig, frame):
+ color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
+ width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
+ self.frame = cv2.resize(
+ color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
+ )
+
+ if self.frame is not None:
+ cv2.imwrite(
+ self.frame_path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
+ )
+
def get_data(self, ended: bool) -> dict:
return {
ReviewSegment.id: self.id,
@@ -273,8 +285,30 @@ class ReviewSegmentMaintainer(threading.Thread):
if segment.severity == SeverityEnum.alert and frame_time > (
segment.last_update + THRESHOLD_ALERT_ACTIVITY
):
+ if segment.frame is None:
+ try:
+ frame_id = f"{camera_config.name}{frame_time}"
+ yuv_frame = self.frame_manager.get(
+ frame_id, camera_config.frame_shape_yuv
+ )
+ segment.save_full_frame(camera_config, yuv_frame)
+ self.frame_manager.close(frame_id)
+ except FileNotFoundError:
+ return
+
self.end_segment(segment)
elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY):
+ if segment.frame is None:
+ try:
+ frame_id = f"{camera_config.name}{frame_time}"
+ yuv_frame = self.frame_manager.get(
+ frame_id, camera_config.frame_shape_yuv
+ )
+ segment.save_full_frame(camera_config, yuv_frame)
+ self.frame_manager.close(frame_id)
+ except FileNotFoundError:
+ return
+
self.end_segment(segment)
def check_if_new_segment(
diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx
index 7d1c7e4d9..e3e677530 100644
--- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx
+++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx
@@ -204,8 +204,8 @@ export default function DynamicVideoPlayer({
/>
(
fetchPreviews
- ? `preview/${camera}/start/${timeRange.after}/end/${timeRange.before}`
+ ? `preview/${camera}/start/${Math.round(timeRange.after)}/end/${Math.round(timeRange.before)}`
: null,
{ revalidateOnFocus: false, revalidateOnReconnect: false },
);
From 50ee447e527d8136ae38a97716984749d3b2aada Mon Sep 17 00:00:00 2001
From: Marc Altmann <40744649+MarcA711@users.noreply.github.com>
Date: Thu, 9 May 2024 15:22:34 +0200
Subject: [PATCH 54/56] Fix aarch64 build (#11289)
* fix aarch64 build
* change order of packages
---
docker/main/Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile
index 3354a21cc..de823bf1c 100644
--- a/docker/main/Dockerfile
+++ b/docker/main/Dockerfile
@@ -51,7 +51,7 @@ ARG DEBIAN_FRONTEND
# Install OpenVino Runtime and Dev library
COPY docker/main/requirements-ov.txt /requirements-ov.txt
RUN apt-get -qq update \
- && apt-get -qq install -y wget python3 python3-distutils \
+ && apt-get -qq install -y wget python3 python3-dev python3-distutils gcc pkg-config libhdf5-dev \
&& wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& python3 get-pip.py "pip" \
&& pip install -r /requirements-ov.txt
From f8523d9ddf4127c8baa3232ad00fc15f5bcd62aa Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Thu, 9 May 2024 08:22:48 -0500
Subject: [PATCH 55/56] Icon picker component (#11310)
* icon picker component
* keep box the same size when filtering icons
---
.../components/filter/CameraGroupSelector.tsx | 72 ++++----
web/src/components/icons/IconPicker.tsx | 154 ++++++++++++++++++
web/src/components/ui/popover.tsx | 51 +++---
web/src/pages/UIPlayground.tsx | 12 ++
web/src/types/frigateConfig.ts | 5 +-
web/src/utils/iconUtil.tsx | 24 +--
6 files changed, 233 insertions(+), 85 deletions(-)
create mode 100644 web/src/components/icons/IconPicker.tsx
diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx
index 84d1a36b5..38a246c00 100644
--- a/web/src/components/filter/CameraGroupSelector.tsx
+++ b/web/src/components/filter/CameraGroupSelector.tsx
@@ -1,8 +1,4 @@
-import {
- CameraGroupConfig,
- FrigateConfig,
- GROUP_ICONS,
-} from "@/types/frigateConfig";
+import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
import { isDesktop, isMobile } from "react-device-detect";
import useSWR from "swr";
import { MdHome } from "react-icons/md";
@@ -10,7 +6,6 @@ import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
import { Button } from "../ui/button";
import { useCallback, useMemo, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
-import { getIconForGroup } from "@/utils/iconUtil";
import { LuPencil, LuPlus } from "react-icons/lu";
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
import { Drawer, DrawerContent } from "../ui/drawer";
@@ -31,13 +26,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
@@ -62,6 +50,9 @@ import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import { usePersistence } from "@/hooks/use-persistence";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
+import * as LuIcons from "react-icons/lu";
+import IconPicker, { IconName, IconRenderer } from "../icons/IconPicker";
+import { isValidIconName } from "@/utils/iconUtil";
type CameraGroupSelectorProps = {
className?: string;
@@ -168,7 +159,12 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
isDesktop ? showTooltip(undefined) : null
}
>
- {getIconForGroup(config.icon)}
+ {config && config.icon && isValidIconName(config.icon) && (
+
+ )}
@@ -503,7 +499,12 @@ export function CameraGroupEdit({
cameras: z.array(z.string()).min(2, {
message: "You must select at least two cameras.",
}),
- icon: z.string(),
+ icon: z
+ .string()
+ .min(1, { message: "You must select an icon." })
+ .refine((value) => Object.keys(LuIcons).includes(value), {
+ message: "Invalid icon",
+ }),
});
const onSubmit = useCallback(
@@ -559,10 +560,10 @@ export function CameraGroupEdit({
const form = useForm>({
resolver: zodResolver(formSchema),
- mode: "onChange",
+ mode: "onSubmit",
defaultValues: {
name: (editingGroup && editingGroup[0]) ?? "",
- icon: editingGroup && editingGroup[1].icon,
+ icon: editingGroup && (editingGroup[1].icon as IconName),
cameras: editingGroup && editingGroup[1].cameras,
},
});
@@ -571,7 +572,7 @@ export function CameraGroupEdit({