mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 09:02:15 +03:00
Use user-namespaced keys for idb persistence (#21110)
* add new hooks * use new hooks for user based keys * fix layout race condition
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { usePersistence } from "@/hooks/use-persistence";
|
||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
||||
import {
|
||||
AllGroupsStreamingSettings,
|
||||
BirdseyeConfig,
|
||||
@@ -40,7 +40,7 @@ import { IoClose } from "react-icons/io5";
|
||||
import { LuLayoutDashboard, LuPencil } from "react-icons/lu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EditGroupDialog } from "@/components/filter/CameraGroupSelector";
|
||||
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
||||
import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state";
|
||||
import { FaCompress, FaExpand } from "react-icons/fa";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -102,8 +102,8 @@ export default function DraggableGridLayout({
|
||||
|
||||
// preferred live modes per camera
|
||||
|
||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
||||
const [globalAutoLive] = useUserPersistence("autoLiveView", true);
|
||||
const [displayCameraNames] = useUserPersistence("displayCameraNames", false);
|
||||
|
||||
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
|
||||
useStreamingSettings();
|
||||
@@ -118,11 +118,14 @@ export default function DraggableGridLayout({
|
||||
|
||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
||||
|
||||
const [gridLayout, setGridLayout, isGridLayoutLoaded] = usePersistence<
|
||||
const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence<
|
||||
Layout[]
|
||||
>(`${cameraGroup}-draggable-layout`);
|
||||
|
||||
const [group] = usePersistedOverlayState("cameraGroup", "default" as string);
|
||||
const [group] = useUserPersistedOverlayState(
|
||||
"cameraGroup",
|
||||
"default" as string,
|
||||
);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
if (!config) {
|
||||
@@ -142,6 +145,11 @@ export default function DraggableGridLayout({
|
||||
useEffect(() => {
|
||||
setIsEditMode(false);
|
||||
setEditGroup(false);
|
||||
// Reset camera tracking state when group changes to prevent the camera-change
|
||||
// effect from incorrectly overwriting the loaded layout
|
||||
setCurrentCameras(undefined);
|
||||
setCurrentIncludeBirdseye(undefined);
|
||||
setCurrentGridLayout(undefined);
|
||||
}, [cameraGroup, setIsEditMode]);
|
||||
|
||||
// camera state
|
||||
@@ -165,104 +173,120 @@ export default function DraggableGridLayout({
|
||||
[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) {
|
||||
const generateLayout = useCallback(
|
||||
(baseLayout: Layout[] | undefined) => {
|
||||
if (!isGridLayoutLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
let aspectRatio;
|
||||
let col;
|
||||
const cameraNames =
|
||||
includeBirdseye && birdseyeConfig?.enabled
|
||||
? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
|
||||
: cameras.map((camera) => camera?.name || "");
|
||||
|
||||
// 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
|
||||
}
|
||||
const optionsMap: Layout[] = baseLayout
|
||||
? baseLayout.filter((layout) => cameraNames?.includes(layout.i))
|
||||
: [];
|
||||
|
||||
// Calculate layout options based on aspect ratio
|
||||
const columnsPerPlayer = 4;
|
||||
let height;
|
||||
let width;
|
||||
cameraNames.forEach((cameraName, index) => {
|
||||
const existingLayout = optionsMap.find(
|
||||
(layout) => layout.i === cameraName,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
// Skip if the camera already exists in the layout
|
||||
if (existingLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
i: cameraName,
|
||||
x: col * width,
|
||||
y: 0, // don't set y, grid does automatically
|
||||
w: width,
|
||||
h: height,
|
||||
};
|
||||
let aspectRatio;
|
||||
let col;
|
||||
|
||||
optionsMap.push(options);
|
||||
});
|
||||
// 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
|
||||
}
|
||||
|
||||
return optionsMap;
|
||||
}, [
|
||||
cameras,
|
||||
isGridLayoutLoaded,
|
||||
currentGridLayout,
|
||||
includeBirdseye,
|
||||
birdseyeConfig,
|
||||
]);
|
||||
// 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,
|
||||
};
|
||||
|
||||
optionsMap.push(options);
|
||||
});
|
||||
|
||||
return optionsMap;
|
||||
},
|
||||
[cameras, isGridLayoutLoaded, includeBirdseye, birdseyeConfig],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isGridLayoutLoaded) {
|
||||
if (gridLayout) {
|
||||
// set current grid layout from loaded
|
||||
setCurrentGridLayout(gridLayout);
|
||||
// set current grid layout from loaded, possibly adding new cameras
|
||||
const updatedLayout = generateLayout(gridLayout);
|
||||
setCurrentGridLayout(updatedLayout);
|
||||
// Only save if cameras were added (layout changed)
|
||||
if (!isEqual(updatedLayout, gridLayout)) {
|
||||
setGridLayout(updatedLayout);
|
||||
}
|
||||
// Set camera tracking state so the camera-change effect has a baseline
|
||||
setCurrentCameras(cameras);
|
||||
setCurrentIncludeBirdseye(includeBirdseye);
|
||||
} else {
|
||||
// idb is empty, set it with an initial layout
|
||||
setGridLayout(generateLayout());
|
||||
const newLayout = generateLayout(undefined);
|
||||
setCurrentGridLayout(newLayout);
|
||||
setGridLayout(newLayout);
|
||||
setCurrentCameras(cameras);
|
||||
setCurrentIncludeBirdseye(includeBirdseye);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isEditMode,
|
||||
gridLayout,
|
||||
currentGridLayout,
|
||||
setGridLayout,
|
||||
isGridLayoutLoaded,
|
||||
generateLayout,
|
||||
cameras,
|
||||
includeBirdseye,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only regenerate layout when cameras change WITHIN an already-loaded group
|
||||
// Skip if currentCameras is undefined (means we just switched groups and
|
||||
// the first useEffect hasn't run yet to set things up)
|
||||
if (!isGridLayoutLoaded || currentCameras === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!isEqual(cameras, currentCameras) ||
|
||||
includeBirdseye !== currentIncludeBirdseye
|
||||
@@ -270,15 +294,17 @@ export default function DraggableGridLayout({
|
||||
setCurrentCameras(cameras);
|
||||
setCurrentIncludeBirdseye(includeBirdseye);
|
||||
|
||||
// set new grid layout in idb
|
||||
setGridLayout(generateLayout());
|
||||
// Regenerate layout based on current layout, adding any new cameras
|
||||
const updatedLayout = generateLayout(currentGridLayout);
|
||||
setCurrentGridLayout(updatedLayout);
|
||||
setGridLayout(updatedLayout);
|
||||
}
|
||||
}, [
|
||||
cameras,
|
||||
includeBirdseye,
|
||||
currentCameras,
|
||||
currentIncludeBirdseye,
|
||||
setCurrentGridLayout,
|
||||
currentGridLayout,
|
||||
generateLayout,
|
||||
setGridLayout,
|
||||
isGridLayoutLoaded,
|
||||
|
||||
@@ -101,7 +101,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { usePersistence } from "@/hooks/use-persistence";
|
||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import axios from "axios";
|
||||
@@ -146,7 +146,7 @@ export default function LiveCameraView({
|
||||
|
||||
// supported features
|
||||
|
||||
const [streamName, setStreamName] = usePersistence<string>(
|
||||
const [streamName, setStreamName] = useUserPersistence<string>(
|
||||
`${camera.name}-stream`,
|
||||
Object.values(camera.live.streams)[0],
|
||||
);
|
||||
@@ -279,7 +279,7 @@ export default function LiveCameraView({
|
||||
const [pip, setPip] = useState(false);
|
||||
const [lowBandwidth, setLowBandwidth] = useState(false);
|
||||
|
||||
const [playInBackground, setPlayInBackground] = usePersistence<boolean>(
|
||||
const [playInBackground, setPlayInBackground] = useUserPersistence<boolean>(
|
||||
`${camera.name}-background-play`,
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { usePersistence } from "@/hooks/use-persistence";
|
||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
||||
import {
|
||||
AllGroupsStreamingSettings,
|
||||
CameraConfig,
|
||||
@@ -78,7 +78,7 @@ export default function LiveDashboardView({
|
||||
|
||||
// layout
|
||||
|
||||
const [mobileLayout, setMobileLayout] = usePersistence<"grid" | "list">(
|
||||
const [mobileLayout, setMobileLayout] = useUserPersistence<"grid" | "list">(
|
||||
"live-layout",
|
||||
isDesktop ? "grid" : "list",
|
||||
);
|
||||
@@ -211,8 +211,8 @@ export default function LiveDashboardView({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
||||
const [globalAutoLive] = useUserPersistence("autoLiveView", true);
|
||||
const [displayCameraNames] = useUserPersistence("displayCameraNames", false);
|
||||
|
||||
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
|
||||
useStreamingSettings();
|
||||
|
||||
Reference in New Issue
Block a user