Refactor motion search (#23378)

* refactor motion search

* cleanup dead code and tests

* tweaks

* fix multi-day seeking

* start playback a few seconds before the change so the motion is in view
This commit is contained in:
Josh Hawkins
2026-06-01 12:08:46 -05:00
committed by GitHub
parent 47a06c8b30
commit 8073174c20
14 changed files with 1502 additions and 506 deletions
@@ -7,6 +7,7 @@ import { LuHand, LuPencil } from "react-icons/lu";
import { FrigateConfig } from "@/types/frigateConfig";
import { TimeRange } from "@/types/timeline";
import { RecordingsSummary } from "@/types/review";
import { ASPECT_PORTRAIT_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { Button } from "@/components/ui/button";
@@ -44,7 +45,11 @@ import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { TimezoneAwareCalendar } from "@/components/overlay/ReviewActivityCalendar";
import { useApiHost } from "@/api";
import { useFormattedTimestamp, use24HourTime } from "@/hooks/use-date-utils";
import {
useFormattedTimestamp,
use24HourTime,
useTimezone,
} from "@/hooks/use-date-utils";
import { getUTCOffset } from "@/utils/dateUtil";
import useSWR from "swr";
import { cn } from "@/lib/utils";
@@ -69,8 +74,6 @@ type MotionSearchDialogProps = {
setThreshold: React.Dispatch<React.SetStateAction<number>>;
minArea: number;
setMinArea: React.Dispatch<React.SetStateAction<number>>;
frameSkip: number;
setFrameSkip: React.Dispatch<React.SetStateAction<number>>;
maxResults: number;
setMaxResults: React.Dispatch<React.SetStateAction<number>>;
searchRange?: TimeRange;
@@ -100,8 +103,6 @@ export default function MotionSearchDialog({
setThreshold,
minArea,
setMinArea,
frameSkip,
setFrameSkip,
maxResults,
setMaxResults,
searchRange,
@@ -117,6 +118,16 @@ export default function MotionSearchDialog({
const [imageLoaded, setImageLoaded] = useState(false);
const [panMode, setPanMode] = useState(false);
const recordingsTimezone = useTimezone(config);
const { data: recordingsSummary } = useSWR<RecordingsSummary>(
selectedCamera
? [
"recordings/summary",
{ timezone: recordingsTimezone, cameras: selectedCamera },
]
: null,
);
const cameraConfig = useMemo(() => {
if (!selectedCamera) return undefined;
return config.cameras[selectedCamera];
@@ -437,23 +448,6 @@ export default function MotionSearchDialog({
{t("settings.minAreaDesc")}
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="frameSkip">{t("settings.frameSkip")}</Label>
<div className="flex items-center gap-2">
<Slider
id="frameSkip"
min={1}
max={120}
step={1}
value={[frameSkip]}
onValueChange={([value]) => setFrameSkip(value)}
/>
<span className="w-12 text-sm">{frameSkip}</span>
</div>
<p className="text-xs text-muted-foreground">
{t("settings.frameSkipDesc")}
</p>
</div>
<div className="grid gap-2">
<div className="flex items-center justify-between gap-2">
<Label htmlFor="parallelMode">
@@ -496,6 +490,7 @@ export default function MotionSearchDialog({
setRange={setSearchRange}
defaultRange={defaultRange}
timezone={timezone}
recordingsSummary={recordingsSummary}
/>
<Button
@@ -519,6 +514,7 @@ type SearchRangeSelectorProps = {
setRange: React.Dispatch<React.SetStateAction<TimeRange | undefined>>;
defaultRange: TimeRange;
timezone?: string;
recordingsSummary?: RecordingsSummary;
};
function SearchRangeSelector({
@@ -526,6 +522,7 @@ function SearchRangeSelector({
setRange,
defaultRange,
timezone,
recordingsSummary,
}: SearchRangeSelectorProps) {
const { t } = useTranslation(["views/motionSearch", "common"]);
const [startOpen, setStartOpen] = useState(false);
@@ -630,6 +627,7 @@ function SearchRangeSelector({
<PopoverContent className="flex flex-col items-center">
<TimezoneAwareCalendar
timezone={timezone}
recordingsSummary={recordingsSummary}
selectedDay={new Date(startTime * 1000)}
onSelect={(day) => {
if (!day) {
@@ -696,6 +694,7 @@ function SearchRangeSelector({
<PopoverContent className="flex flex-col items-center">
<TimezoneAwareCalendar
timezone={timezone}
recordingsSummary={recordingsSummary}
selectedDay={new Date(endTime * 1000)}
onSelect={(day) => {
if (!day) {