mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 06:08:22 +03:00
Persist per-camera live grid zoom state
This commit is contained in:
parent
91976498c5
commit
7969930d21
@ -31,6 +31,24 @@ export type CameraZoomPersistedState = {
|
||||
focusY: number;
|
||||
};
|
||||
|
||||
export function isCameraZoomPersistedState(
|
||||
value: unknown,
|
||||
): value is CameraZoomPersistedState {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = value as Partial<CameraZoomPersistedState>;
|
||||
return (
|
||||
typeof candidate.normalizedScale === "number" &&
|
||||
Number.isFinite(candidate.normalizedScale) &&
|
||||
typeof candidate.focusX === "number" &&
|
||||
Number.isFinite(candidate.focusX) &&
|
||||
typeof candidate.focusY === "number" &&
|
||||
Number.isFinite(candidate.focusY)
|
||||
);
|
||||
}
|
||||
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@ -160,3 +178,27 @@ export function getNextScaleFromWheelDelta(
|
||||
export function getCameraZoomStorageKey(cameraName: string): string {
|
||||
return `live:grid-card:zoom:${cameraName}`;
|
||||
}
|
||||
|
||||
export function loadPersistedCameraZoomState(
|
||||
cameraName: string,
|
||||
): CameraZoomPersistedState | undefined {
|
||||
const serialized = localStorage.getItem(getCameraZoomStorageKey(cameraName));
|
||||
|
||||
if (!serialized) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(serialized);
|
||||
return isCameraZoomPersistedState(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function savePersistedCameraZoomState(
|
||||
cameraName: string,
|
||||
state: CameraZoomPersistedState,
|
||||
): void {
|
||||
localStorage.setItem(getCameraZoomStorageKey(cameraName), JSON.stringify(state));
|
||||
}
|
||||
|
||||
@ -53,8 +53,12 @@ import {
|
||||
CAMERA_ZOOM_MIN_SCALE,
|
||||
CameraZoomRuntimeTransform,
|
||||
clampScale,
|
||||
fromPersistedCameraZoomState,
|
||||
getCursorRelativeZoomTransform,
|
||||
getNextScaleFromWheelDelta,
|
||||
loadPersistedCameraZoomState,
|
||||
savePersistedCameraZoomState,
|
||||
toPersistedCameraZoomState,
|
||||
} from "@/utils/cameraZoom";
|
||||
|
||||
type DraggableGridLayoutProps = {
|
||||
@ -434,6 +438,49 @@ export default function DraggableGridLayout({
|
||||
const [cameraZoomStates, setCameraZoomStates] = useState<
|
||||
Record<string, CameraZoomRuntimeTransform>
|
||||
>({});
|
||||
const cameraZoomViewportRefs = useRef<Record<string, HTMLDivElement | null>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const getCardZoomDimensions = useCallback((cameraName: string) => {
|
||||
const viewport = cameraZoomViewportRefs.current[cameraName];
|
||||
const content = viewport?.firstElementChild as HTMLElement | null;
|
||||
const viewportWidth = viewport?.clientWidth ?? 0;
|
||||
const viewportHeight = viewport?.clientHeight ?? 0;
|
||||
|
||||
return {
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
contentWidth: content?.clientWidth ?? viewportWidth,
|
||||
contentHeight: content?.clientHeight ?? viewportHeight,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const hydrateCameraZoomFromStorage = useCallback(
|
||||
(cameraName: string) => {
|
||||
setCameraZoomStates((prev) => {
|
||||
if (prev[cameraName]) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const persisted = loadPersistedCameraZoomState(cameraName);
|
||||
if (!persisted) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const dimensions = getCardZoomDimensions(cameraName);
|
||||
if (!dimensions.viewportWidth || !dimensions.viewportHeight) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[cameraName]: fromPersistedCameraZoomState(persisted, dimensions),
|
||||
};
|
||||
});
|
||||
},
|
||||
[getCardZoomDimensions],
|
||||
);
|
||||
|
||||
const getDefaultZoomTransform = useCallback(
|
||||
(): CameraZoomRuntimeTransform => ({
|
||||
@ -467,6 +514,15 @@ export default function DraggableGridLayout({
|
||||
cursorX,
|
||||
cursorY,
|
||||
);
|
||||
const content = event.currentTarget.firstElementChild as HTMLElement | null;
|
||||
const persisted = toPersistedCameraZoomState(next, {
|
||||
viewportWidth: bounds.width,
|
||||
viewportHeight: bounds.height,
|
||||
contentWidth: content?.clientWidth ?? bounds.width,
|
||||
contentHeight: content?.clientHeight ?? bounds.height,
|
||||
});
|
||||
|
||||
savePersistedCameraZoomState(cameraName, persisted);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
@ -477,6 +533,12 @@ export default function DraggableGridLayout({
|
||||
[getDefaultZoomTransform],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
cameras.forEach((camera) => {
|
||||
hydrateCameraZoomFromStorage(camera.name);
|
||||
});
|
||||
}, [cameras, hydrateCameraZoomFromStorage]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialStreamStatsState = getStreamStatsFromStorage();
|
||||
setGlobalStreamStatsEnabled(initialStreamStatsState);
|
||||
@ -716,6 +778,13 @@ export default function DraggableGridLayout({
|
||||
>
|
||||
<div
|
||||
className="size-full overflow-hidden rounded-lg md:rounded-2xl"
|
||||
ref={(node) => {
|
||||
cameraZoomViewportRefs.current[camera.name] = node;
|
||||
|
||||
if (node) {
|
||||
hydrateCameraZoomFromStorage(camera.name);
|
||||
}
|
||||
}}
|
||||
onWheel={(event) => handleCardWheelZoom(camera.name, event)}
|
||||
>
|
||||
<div
|
||||
|
||||
Loading…
Reference in New Issue
Block a user