mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-01 08:32:18 +03:00
Improve motion review and add motion search (#22253)
* implement motion search and motion previews * tweaks * fix merge issue * fix copilot instructions
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import useApiFilter from "@/hooks/use-api-filter";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
||||
import { useTimezone } from "@/hooks/use-date-utils";
|
||||
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
getEndOfDayTimestamp,
|
||||
} from "@/utils/dateUtil";
|
||||
import EventView from "@/views/events/EventView";
|
||||
import MotionSearchView from "@/views/motion-search/MotionSearchView";
|
||||
import { RecordingView } from "@/views/recording/RecordingView";
|
||||
import axios from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -34,6 +36,7 @@ export default function Events() {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const timezone = useTimezone(config);
|
||||
const allowedCameras = useAllowedCameras();
|
||||
|
||||
// recordings viewer
|
||||
|
||||
@@ -52,6 +55,74 @@ export default function Events() {
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const [motionPreviewsCamera, setMotionPreviewsCamera] = useOverlayState<
|
||||
string | undefined
|
||||
>("motionPreviewsCamera", undefined);
|
||||
|
||||
const [motionSearchCamera, setMotionSearchCamera] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const motionSearchCameras = useMemo(() => {
|
||||
if (!config?.cameras) {
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
return Object.keys(config.cameras).filter((cam) =>
|
||||
allowedCameras.includes(cam),
|
||||
);
|
||||
}, [allowedCameras, config?.cameras]);
|
||||
|
||||
const selectedMotionSearchCamera = useMemo(() => {
|
||||
if (!motionSearchCamera) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (motionSearchCameras.includes(motionSearchCamera)) {
|
||||
return motionSearchCamera;
|
||||
}
|
||||
|
||||
return motionSearchCameras[0] ?? null;
|
||||
}, [motionSearchCamera, motionSearchCameras]);
|
||||
|
||||
const motionSearchTimeRange = useMemo(() => {
|
||||
if (motionSearchDay) {
|
||||
return {
|
||||
after: getBeginningOfDayTimestamp(new Date(motionSearchDay)),
|
||||
before: getEndOfDayTimestamp(new Date(motionSearchDay)),
|
||||
};
|
||||
}
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
return {
|
||||
after: now - 86400,
|
||||
before: now,
|
||||
};
|
||||
}, [motionSearchDay]);
|
||||
|
||||
const closeMotionSearch = useCallback(() => {
|
||||
setMotionSearchCamera(null);
|
||||
setMotionSearchDay(undefined);
|
||||
setBeforeTs(Date.now() / 1000);
|
||||
}, []);
|
||||
|
||||
const handleMotionSearchCameraSelect = useCallback((camera: string) => {
|
||||
setMotionSearchCamera(camera);
|
||||
}, []);
|
||||
|
||||
const handleMotionSearchDaySelect = useCallback((day: Date | undefined) => {
|
||||
if (day == undefined) {
|
||||
setMotionSearchDay(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedDay = new Date(day);
|
||||
normalizedDay.setHours(0, 0, 0, 0);
|
||||
setMotionSearchDay(normalizedDay);
|
||||
}, []);
|
||||
|
||||
const [notificationTab, setNotificationTab] =
|
||||
useState<TimelineType>("timeline");
|
||||
@@ -508,7 +579,24 @@ export default function Events() {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
return motionSearchCamera ? (
|
||||
!config || !selectedMotionSearchCamera ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<MotionSearchView
|
||||
config={config}
|
||||
cameras={motionSearchCameras}
|
||||
selectedCamera={selectedMotionSearchCamera}
|
||||
onCameraSelect={handleMotionSearchCameraSelect}
|
||||
cameraLocked={true}
|
||||
selectedDay={motionSearchDay}
|
||||
onDaySelect={handleMotionSearchDaySelect}
|
||||
timeRange={motionSearchTimeRange}
|
||||
timezone={timezone}
|
||||
onBack={closeMotionSearch}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<EventView
|
||||
reviewItems={reviewItems}
|
||||
currentReviewItems={currentItems}
|
||||
@@ -525,6 +613,11 @@ export default function Events() {
|
||||
markItemAsReviewed={markItemAsReviewed}
|
||||
markAllItemsAsReviewed={markAllItemsAsReviewed}
|
||||
onOpenRecording={setRecording}
|
||||
motionPreviewsCamera={motionPreviewsCamera ?? null}
|
||||
setMotionPreviewsCamera={(camera) =>
|
||||
setMotionPreviewsCamera(camera ?? undefined)
|
||||
}
|
||||
setMotionSearchCamera={setMotionSearchCamera}
|
||||
pullLatestData={reloadData}
|
||||
updateFilter={onUpdateFilter}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { useTimezone } from "@/hooks/use-date-utils";
|
||||
import MotionSearchView from "@/views/motion-search/MotionSearchView";
|
||||
import {
|
||||
getBeginningOfDayTimestamp,
|
||||
getEndOfDayTimestamp,
|
||||
} from "@/utils/dateUtil";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
|
||||
export default function MotionSearch() {
|
||||
const { t } = useTranslation(["views/motionSearch"]);
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const timezone = useTimezone(config);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("documentTitle");
|
||||
}, [t]);
|
||||
|
||||
// Get allowed cameras
|
||||
const allowedCameras = useAllowedCameras();
|
||||
|
||||
const cameras = useMemo(() => {
|
||||
if (!config?.cameras) return [];
|
||||
return Object.keys(config.cameras).filter((cam) =>
|
||||
allowedCameras.includes(cam),
|
||||
);
|
||||
}, [config?.cameras, allowedCameras]);
|
||||
|
||||
// Selected camera state
|
||||
const [selectedCamera, setSelectedCamera] = useState<string | null>(null);
|
||||
const [cameraLocked, setCameraLocked] = useState(false);
|
||||
|
||||
useSearchEffect("camera", (camera: string) => {
|
||||
if (cameras.length > 0 && cameras.includes(camera)) {
|
||||
setSelectedCamera(camera);
|
||||
setCameraLocked(true);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Initialize with first camera when available (only if not set by camera param)
|
||||
useEffect(() => {
|
||||
if (cameras.length === 0) return;
|
||||
if (!selectedCamera) {
|
||||
setSelectedCamera(cameras[0]);
|
||||
}
|
||||
}, [cameras, selectedCamera]);
|
||||
|
||||
// Time range state - default to last 24 hours
|
||||
const [selectedDay, setSelectedDay] = useState<Date | undefined>(undefined);
|
||||
|
||||
const timeRange = useMemo(() => {
|
||||
if (selectedDay) {
|
||||
return {
|
||||
after: getBeginningOfDayTimestamp(new Date(selectedDay)),
|
||||
before: getEndOfDayTimestamp(new Date(selectedDay)),
|
||||
};
|
||||
}
|
||||
// Default to last 24 hours
|
||||
const now = Date.now() / 1000;
|
||||
return {
|
||||
after: now - 86400,
|
||||
before: now,
|
||||
};
|
||||
}, [selectedDay]);
|
||||
|
||||
const handleCameraSelect = useCallback((camera: string) => {
|
||||
setSelectedCamera(camera);
|
||||
}, []);
|
||||
|
||||
const handleDaySelect = useCallback((day: Date | undefined) => {
|
||||
if (day == undefined) {
|
||||
setSelectedDay(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedDay = new Date(day);
|
||||
normalizedDay.setHours(0, 0, 0, 0);
|
||||
setSelectedDay(normalizedDay);
|
||||
}, []);
|
||||
|
||||
if (!config || cameras.length === 0) {
|
||||
return (
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<ActivityIndicator />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionSearchView
|
||||
config={config}
|
||||
cameras={cameras}
|
||||
selectedCamera={selectedCamera ?? null}
|
||||
onCameraSelect={handleCameraSelect}
|
||||
cameraLocked={cameraLocked}
|
||||
selectedDay={selectedDay}
|
||||
onDaySelect={handleDaySelect}
|
||||
timeRange={timeRange}
|
||||
timezone={timezone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user