mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 04:57:42 +03:00
new activity stream panel in history view
This commit is contained in:
parent
ada15e1686
commit
be0b0cfd72
@ -7,6 +7,7 @@ import PreviewPlayer, {
|
||||
import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController";
|
||||
import DynamicVideoPlayer from "@/components/player/dynamic/DynamicVideoPlayer";
|
||||
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
||||
import ActivityStream from "@/components/timeline/ActivityStream";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useOverlayState } from "@/hooks/use-overlay-state";
|
||||
@ -40,7 +41,11 @@ import { IoMdArrowRoundBack } from "react-icons/io";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import useSWR from "swr";
|
||||
import { TimeRange, TimelineType } from "@/types/timeline";
|
||||
import {
|
||||
TimeRange,
|
||||
TimelineType,
|
||||
ObjectLifecycleSequence,
|
||||
} from "@/types/timeline";
|
||||
import MobileCameraDrawer from "@/components/overlay/MobileCameraDrawer";
|
||||
import MobileTimelineDrawer from "@/components/overlay/MobileTimelineDrawer";
|
||||
import MobileReviewSettingsDrawer from "@/components/overlay/MobileReviewSettingsDrawer";
|
||||
@ -66,6 +71,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
import { ActivityStreamProvider } from "@/contexts/ActivityStreamContext";
|
||||
import { GenAISummaryDialog } from "@/components/overlay/chip/GenAISummaryChip";
|
||||
|
||||
const DATA_REFRESH_TIME = 600000; // 10 minutes
|
||||
@ -159,6 +165,46 @@ export function RecordingView({
|
||||
chunkedTimeRange[chunkedTimeRange.length - 1],
|
||||
[selectedRangeIdx, chunkedTimeRange],
|
||||
);
|
||||
|
||||
// timeline data for activity stream
|
||||
const { data: timelineResponse } = useSWR<{
|
||||
start: number;
|
||||
end: number;
|
||||
count: number;
|
||||
hours: { [key: string]: ObjectLifecycleSequence[] };
|
||||
}>([
|
||||
"timeline/hourly",
|
||||
{
|
||||
cameras: mainCamera,
|
||||
before: timeRange.before,
|
||||
after: timeRange.after,
|
||||
limit: 1000,
|
||||
},
|
||||
]);
|
||||
|
||||
const timelineData = useMemo(() => {
|
||||
if (!timelineResponse?.hours) return [];
|
||||
let data = Object.values(timelineResponse.hours).flat();
|
||||
|
||||
// Filter by review filter
|
||||
if (filter?.labels && filter.labels.length > 0) {
|
||||
data = data.filter((item) =>
|
||||
filter.labels!.includes(item.data.label.replace("-verified", "")),
|
||||
);
|
||||
}
|
||||
|
||||
if (filter?.zones && filter.zones.length > 0) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.data.zones &&
|
||||
Array.isArray(item.data.zones) &&
|
||||
item.data.zones.some((zone) => filter.zones!.includes(zone)),
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [timelineResponse, filter]);
|
||||
|
||||
const reviewFilterList = useMemo(() => {
|
||||
const uniqueLabels = new Set<string>();
|
||||
|
||||
@ -521,6 +567,11 @@ export function RecordingView({
|
||||
);
|
||||
|
||||
return (
|
||||
<ActivityStreamProvider
|
||||
isActivityMode={timelineType === "activity"}
|
||||
currentTime={currentTime}
|
||||
camera={mainCamera}
|
||||
>
|
||||
<div ref={contentRef} className="flex size-full flex-col pt-2">
|
||||
<Toaster closeButton={true} />
|
||||
<div className="relative mb-2 flex h-11 w-full items-center justify-between px-2">
|
||||
@ -635,6 +686,13 @@ export function RecordingView({
|
||||
>
|
||||
<div className="">{t("events.label")}</div>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "activity" ? "" : "text-muted-foreground"}`}
|
||||
value="activity"
|
||||
aria-label="Activity Stream"
|
||||
>
|
||||
<div className="">Activity</div>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
) : (
|
||||
<MobileTimelineDrawer
|
||||
@ -670,12 +728,21 @@ export function RecordingView({
|
||||
>
|
||||
<div
|
||||
ref={cameraLayoutRef}
|
||||
className={cn("flex flex-1 flex-wrap", isDesktop ? "w-[80%]" : "")}
|
||||
className={cn(
|
||||
"flex flex-1 flex-wrap",
|
||||
isDesktop
|
||||
? timelineType === "activity"
|
||||
? "w-full"
|
||||
: "w-[80%]"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-full items-center",
|
||||
mainCameraAspect == "tall"
|
||||
timelineType === "activity"
|
||||
? "flex-col"
|
||||
: mainCameraAspect == "tall"
|
||||
? "flex-row justify-evenly"
|
||||
: "flex-col justify-center gap-2",
|
||||
)}
|
||||
@ -739,6 +806,7 @@ export function RecordingView({
|
||||
);
|
||||
}}
|
||||
onClipEnded={onClipEnded}
|
||||
onSeekToTime={manuallySetCurrentTime}
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
}}
|
||||
@ -749,7 +817,9 @@ export function RecordingView({
|
||||
containerRef={mainLayoutRef}
|
||||
/>
|
||||
</div>
|
||||
{isDesktop && effectiveCameras.length > 1 && (
|
||||
{isDesktop &&
|
||||
effectiveCameras.length > 1 &&
|
||||
timelineType !== "activity" && (
|
||||
<div
|
||||
ref={previewRowRef}
|
||||
className={cn(
|
||||
@ -809,7 +879,8 @@ export function RecordingView({
|
||||
contentRef={contentRef}
|
||||
mainCamera={mainCamera}
|
||||
timelineType={
|
||||
(exportRange == undefined ? timelineType : "timeline") ?? "timeline"
|
||||
(exportRange == undefined ? timelineType : "timeline") ??
|
||||
"timeline"
|
||||
}
|
||||
timeRange={timeRange}
|
||||
mainCameraReviewItems={mainCameraReviewItems}
|
||||
@ -820,10 +891,12 @@ export function RecordingView({
|
||||
manuallySetCurrentTime={manuallySetCurrentTime}
|
||||
setScrubbing={setScrubbing}
|
||||
setExportRange={setExportRange}
|
||||
timelineData={timelineData}
|
||||
onAnalysisOpen={onAnalysisOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ActivityStreamProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -841,6 +914,7 @@ type TimelineProps = {
|
||||
manuallySetCurrentTime: (time: number, force: boolean) => void;
|
||||
setScrubbing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setExportRange: (range: TimeRange) => void;
|
||||
timelineData?: ObjectLifecycleSequence[];
|
||||
onAnalysisOpen: (open: boolean) => void;
|
||||
};
|
||||
function Timeline({
|
||||
@ -857,6 +931,7 @@ function Timeline({
|
||||
manuallySetCurrentTime,
|
||||
setScrubbing,
|
||||
setExportRange,
|
||||
timelineData,
|
||||
onAnalysisOpen,
|
||||
}: TimelineProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
@ -938,9 +1013,9 @@ function Timeline({
|
||||
className={cn(
|
||||
"relative",
|
||||
isDesktop
|
||||
? `${timelineType == "timeline" ? "w-[100px]" : "w-60"} no-scrollbar overflow-y-auto`
|
||||
: `overflow-hidden portrait:flex-grow ${timelineType == "timeline" ? "landscape:w-[100px]" : "landscape:w-[175px]"}`,
|
||||
)}
|
||||
? `${timelineType == "timeline" ? "w-[100px]" : timelineType == "activity" ? "w-[30%]" : "w-60"} no-scrollbar overflow-y-auto`
|
||||
: `overflow-hidden portrait:flex-grow ${timelineType == "timeline" ? "landscape:w-[100px]" : timelineType == "activity" ? "flex-1" : "landscape:w-[175px]"} `
|
||||
} relative`}
|
||||
>
|
||||
{isMobile && (
|
||||
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen} />
|
||||
@ -975,6 +1050,12 @@ function Timeline({
|
||||
) : (
|
||||
<Skeleton className="size-full" />
|
||||
)
|
||||
) : timelineType == "activity" ? (
|
||||
<ActivityStream
|
||||
timelineData={timelineData ?? []}
|
||||
currentTime={currentTime}
|
||||
onSeek={(timestamp) => manuallySetCurrentTime(timestamp, true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="scrollbar-container h-full overflow-auto bg-secondary">
|
||||
<div
|
||||
|
||||
Loading…
Reference in New Issue
Block a user