Files
frigate/web/src/context/detail-stream-context.tsx
T

141 lines
4.3 KiB
TypeScript
Raw Normal View History

2025-11-10 11:03:56 -06:00
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
} from "react";
2025-10-16 08:24:14 -05:00
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
2025-10-18 13:19:21 -05:00
export interface DetailStreamContextType {
2025-10-25 17:15:36 -05:00
selectedObjectIds: string[];
2025-10-16 08:24:14 -05:00
currentTime: number;
camera: string;
annotationOffset: number; // milliseconds
2025-10-26 13:12:20 -05:00
setSelectedObjectIds: React.Dispatch<React.SetStateAction<string[]>>;
2025-11-06 10:22:52 -06:00
setAnnotationOffset: React.Dispatch<React.SetStateAction<number>>;
2025-10-25 17:15:36 -05:00
toggleObjectSelection: (id: string | undefined) => void;
2025-10-16 08:24:14 -05:00
isDetailMode: boolean;
}
const DetailStreamContext = createContext<DetailStreamContextType | undefined>(
undefined,
);
interface DetailStreamProviderProps {
children: React.ReactNode;
isDetailMode: boolean;
currentTime: number;
camera: string;
initialSelectedObjectIds?: string[];
2025-10-16 08:24:14 -05:00
}
export function DetailStreamProvider({
children,
isDetailMode,
currentTime,
camera,
initialSelectedObjectIds,
2025-10-16 08:24:14 -05:00
}: DetailStreamProviderProps) {
const [selectedObjectIds, setSelectedObjectIds] = useState<string[]>(
() => initialSelectedObjectIds ?? [],
);
2025-10-16 08:24:14 -05:00
2025-11-10 11:03:56 -06:00
// When the parent provides a new initialSelectedObjectIds (for example
// when navigating between search results) update the selection so children
// like `ObjectTrackOverlay` receive the new ids immediately. We only
// perform this update when the incoming value actually changes.
useEffect(() => {
if (
initialSelectedObjectIds &&
(initialSelectedObjectIds.length !== selectedObjectIds.length ||
initialSelectedObjectIds.some((v, i) => selectedObjectIds[i] !== v))
) {
setSelectedObjectIds(initialSelectedObjectIds);
}
// Intentionally include selectedObjectIds to compare previous value and
// avoid overwriting user interactions unless the incoming prop changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialSelectedObjectIds]);
2025-10-25 17:15:36 -05:00
const toggleObjectSelection = (id: string | undefined) => {
if (id === undefined) {
setSelectedObjectIds([]);
} else {
setSelectedObjectIds((prev) => {
if (prev.includes(id)) {
return prev.filter((existingId) => existingId !== id);
} else {
return [...prev, id];
}
});
}
};
2025-10-16 08:24:14 -05:00
const { data: config } = useSWR<FrigateConfig>("config");
const [annotationOffset, setAnnotationOffset] = useState<number>(() => {
if (!config) return 0;
return config.cameras[camera]?.detect?.annotation_offset || 0;
});
useEffect(() => {
if (!config) return;
const cfgOffset = config.cameras[camera]?.detect?.annotation_offset || 0;
setAnnotationOffset(cfgOffset);
}, [config, camera]);
2025-11-10 11:03:56 -06:00
// Clear selected objects when exiting detail mode or when the camera
// changes for providers that are not initialized with an explicit
// `initialSelectedObjectIds` (e.g., the RecordingView). For providers
// that receive `initialSelectedObjectIds` (like SearchDetailDialog) we
// avoid clearing on camera change to prevent a race with children that
// immediately set selection when mounting.
const prevCameraRef = useRef<string | undefined>(undefined);
2025-10-26 07:27:07 -05:00
useEffect(() => {
2025-11-10 11:03:56 -06:00
// Always clear when leaving detail mode
if (!isDetailMode) {
setSelectedObjectIds([]);
prevCameraRef.current = camera;
return;
}
// If camera changed and the parent did not provide initialSelectedObjectIds,
// clear selection to preserve previous behavior.
if (
prevCameraRef.current !== undefined &&
prevCameraRef.current !== camera &&
initialSelectedObjectIds === undefined
) {
setSelectedObjectIds([]);
}
prevCameraRef.current = camera;
}, [isDetailMode, camera, initialSelectedObjectIds]);
2025-10-26 07:27:07 -05:00
2025-10-16 08:24:14 -05:00
const value: DetailStreamContextType = {
2025-10-25 17:15:36 -05:00
selectedObjectIds,
2025-10-16 08:24:14 -05:00
currentTime,
camera,
annotationOffset,
setAnnotationOffset,
2025-10-26 13:12:20 -05:00
setSelectedObjectIds,
2025-10-25 17:15:36 -05:00
toggleObjectSelection,
2025-10-16 08:24:14 -05:00
isDetailMode,
};
2026-03-05 08:42:38 -06:00
return <DetailStreamContext value={value}>{children}</DetailStreamContext>;
2025-10-16 08:24:14 -05:00
}
// eslint-disable-next-line react-refresh/only-export-components
export function useDetailStream() {
const context = useContext(DetailStreamContext);
if (context === undefined) {
throw new Error(
"useDetailStream must be used within an DetailStreamProvider",
);
}
return context;
}