mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-24 00:58:22 +03:00
Merge pull request #33 from ibs0d/codex/add-zoom-persistence-for-camera-grid-cards
Persist per-camera zoom for live grid cards
This commit is contained in:
commit
6af6468899
@ -31,6 +31,24 @@ export type CameraZoomPersistedState = {
|
|||||||
focusY: number;
|
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 {
|
export function clamp(value: number, min: number, max: number): number {
|
||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
@ -160,3 +178,27 @@ export function getNextScaleFromWheelDelta(
|
|||||||
export function getCameraZoomStorageKey(cameraName: string): string {
|
export function getCameraZoomStorageKey(cameraName: string): string {
|
||||||
return `live:grid-card:zoom:${cameraName}`;
|
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,
|
CAMERA_ZOOM_MIN_SCALE,
|
||||||
CameraZoomRuntimeTransform,
|
CameraZoomRuntimeTransform,
|
||||||
clampScale,
|
clampScale,
|
||||||
|
fromPersistedCameraZoomState,
|
||||||
getCursorRelativeZoomTransform,
|
getCursorRelativeZoomTransform,
|
||||||
getNextScaleFromWheelDelta,
|
getNextScaleFromWheelDelta,
|
||||||
|
loadPersistedCameraZoomState,
|
||||||
|
savePersistedCameraZoomState,
|
||||||
|
toPersistedCameraZoomState,
|
||||||
} from "@/utils/cameraZoom";
|
} from "@/utils/cameraZoom";
|
||||||
|
|
||||||
type DraggableGridLayoutProps = {
|
type DraggableGridLayoutProps = {
|
||||||
@ -434,6 +438,49 @@ export default function DraggableGridLayout({
|
|||||||
const [cameraZoomStates, setCameraZoomStates] = useState<
|
const [cameraZoomStates, setCameraZoomStates] = useState<
|
||||||
Record<string, CameraZoomRuntimeTransform>
|
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(
|
const getDefaultZoomTransform = useCallback(
|
||||||
(): CameraZoomRuntimeTransform => ({
|
(): CameraZoomRuntimeTransform => ({
|
||||||
@ -467,6 +514,15 @@ export default function DraggableGridLayout({
|
|||||||
cursorX,
|
cursorX,
|
||||||
cursorY,
|
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 {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@ -477,6 +533,12 @@ export default function DraggableGridLayout({
|
|||||||
[getDefaultZoomTransform],
|
[getDefaultZoomTransform],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
hydrateCameraZoomFromStorage(camera.name);
|
||||||
|
});
|
||||||
|
}, [cameras, hydrateCameraZoomFromStorage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialStreamStatsState = getStreamStatsFromStorage();
|
const initialStreamStatsState = getStreamStatsFromStorage();
|
||||||
setGlobalStreamStatsEnabled(initialStreamStatsState);
|
setGlobalStreamStatsEnabled(initialStreamStatsState);
|
||||||
@ -716,6 +778,13 @@ export default function DraggableGridLayout({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="size-full overflow-hidden rounded-lg md:rounded-2xl"
|
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)}
|
onWheel={(event) => handleCardWheelZoom(camera.name, event)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user