mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Update recordings view (#10585)
* Update recordings view * Fix opening recordings view from gif
This commit is contained in:
parent
f113acee33
commit
865c26ff18
@ -5,6 +5,7 @@ import { IconType } from "react-icons";
|
|||||||
type FilterCheckBoxProps = {
|
type FilterCheckBoxProps = {
|
||||||
label: string;
|
label: string;
|
||||||
CheckIcon?: IconType;
|
CheckIcon?: IconType;
|
||||||
|
iconClassName?: string;
|
||||||
isChecked: boolean;
|
isChecked: boolean;
|
||||||
onCheckedChange: (isChecked: boolean) => void;
|
onCheckedChange: (isChecked: boolean) => void;
|
||||||
};
|
};
|
||||||
@ -12,6 +13,7 @@ type FilterCheckBoxProps = {
|
|||||||
export default function FilterCheckBox({
|
export default function FilterCheckBox({
|
||||||
label,
|
label,
|
||||||
CheckIcon = LuCheck,
|
CheckIcon = LuCheck,
|
||||||
|
iconClassName = "size-6",
|
||||||
isChecked,
|
isChecked,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
}: FilterCheckBoxProps) {
|
}: FilterCheckBoxProps) {
|
||||||
@ -22,9 +24,9 @@ export default function FilterCheckBox({
|
|||||||
onClick={() => onCheckedChange(!isChecked)}
|
onClick={() => onCheckedChange(!isChecked)}
|
||||||
>
|
>
|
||||||
{isChecked ? (
|
{isChecked ? (
|
||||||
<CheckIcon className="w-6 h-6" />
|
<CheckIcon className={iconClassName} />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-6 h-6" />
|
<div className={iconClassName} />
|
||||||
)}
|
)}
|
||||||
<div className="ml-1 w-full flex justify-start">{label}</div>
|
<div className="ml-1 w-full flex justify-start">{label}</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -280,7 +280,7 @@ type CalendarFilterButtonProps = {
|
|||||||
day?: Date;
|
day?: Date;
|
||||||
updateSelectedDay: (day?: Date) => void;
|
updateSelectedDay: (day?: Date) => void;
|
||||||
};
|
};
|
||||||
function CalendarFilterButton({
|
export function CalendarFilterButton({
|
||||||
reviewSummary,
|
reviewSummary,
|
||||||
day,
|
day,
|
||||||
updateSelectedDay,
|
updateSelectedDay,
|
||||||
|
@ -7,6 +7,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
|||||||
import { ReviewSegment } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
|
|
||||||
type AnimatedEventThumbnailProps = {
|
type AnimatedEventThumbnailProps = {
|
||||||
event: ReviewSegment;
|
event: ReviewSegment;
|
||||||
@ -18,7 +19,13 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onOpenReview = useCallback(() => {
|
const onOpenReview = useCallback(() => {
|
||||||
navigate("events", { state: { review: event.id } });
|
navigate("events", {
|
||||||
|
state: {
|
||||||
|
camera: event.camera,
|
||||||
|
startTime: event.start_time,
|
||||||
|
severity: event.severity,
|
||||||
|
} as RecordingStartingPoint,
|
||||||
|
});
|
||||||
}, [navigate, event]);
|
}, [navigate, event]);
|
||||||
|
|
||||||
// image behavior
|
// image behavior
|
||||||
|
@ -36,11 +36,18 @@ export default function Events() {
|
|||||||
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
||||||
useApiFilter<ReviewFilter>();
|
useApiFilter<ReviewFilter>();
|
||||||
|
|
||||||
const onUpdateFilter = useCallback((newFilter: ReviewFilter) => {
|
const onUpdateFilter = useCallback(
|
||||||
setReviewFilter(newFilter);
|
(newFilter: ReviewFilter) => {
|
||||||
// we don't want this updating
|
setReviewFilter(newFilter);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
// update recording start time if filter
|
||||||
|
// was changed on recording page
|
||||||
|
if (recording != undefined && newFilter.after != undefined) {
|
||||||
|
setRecording({ ...recording, startTime: newFilter.after }, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[recording, setRecording, setReviewFilter],
|
||||||
|
);
|
||||||
|
|
||||||
// review paging
|
// review paging
|
||||||
|
|
||||||
@ -286,10 +293,8 @@ export default function Events() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
camera: recording.camera,
|
camera: recording.camera,
|
||||||
severity: recording.severity,
|
|
||||||
start_time: recording.startTime,
|
start_time: recording.startTime,
|
||||||
allCameras: allCameras,
|
allCameras: allCameras,
|
||||||
cameraSegments: reviews.filter((seg) => allCameras.includes(seg.camera)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// previews will not update after item is selected
|
// previews will not update after item is selected
|
||||||
@ -306,9 +311,11 @@ export default function Events() {
|
|||||||
startCamera={selectedReviewData.camera}
|
startCamera={selectedReviewData.camera}
|
||||||
startTime={selectedReviewData.start_time}
|
startTime={selectedReviewData.start_time}
|
||||||
allCameras={selectedReviewData.allCameras}
|
allCameras={selectedReviewData.allCameras}
|
||||||
severity={selectedReviewData.severity}
|
reviewItems={reviews}
|
||||||
reviewItems={selectedReviewData.cameraSegments}
|
reviewSummary={reviewSummary}
|
||||||
allPreviews={allPreviews}
|
allPreviews={allPreviews}
|
||||||
|
filter={reviewFilter}
|
||||||
|
updateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
|
import FilterCheckBox from "@/components/filter/FilterCheckBox";
|
||||||
|
import { CalendarFilterButton } from "@/components/filter/ReviewFilterGroup";
|
||||||
import PreviewPlayer, {
|
import PreviewPlayer, {
|
||||||
PreviewController,
|
PreviewController,
|
||||||
} from "@/components/player/PreviewPlayer";
|
} from "@/components/player/PreviewPlayer";
|
||||||
import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController";
|
import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController";
|
||||||
import DynamicVideoPlayer from "@/components/player/dynamic/DynamicVideoPlayer";
|
import DynamicVideoPlayer from "@/components/player/dynamic/DynamicVideoPlayer";
|
||||||
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
|
||||||
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { Preview } from "@/types/preview";
|
import { Preview } from "@/types/preview";
|
||||||
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
import {
|
||||||
|
MotionData,
|
||||||
|
ReviewFilter,
|
||||||
|
ReviewSegment,
|
||||||
|
ReviewSummary,
|
||||||
|
} from "@/types/review";
|
||||||
|
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||||
import { getChunkedTimeDay } from "@/utils/timelineUtil";
|
import { getChunkedTimeDay } from "@/utils/timelineUtil";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
|
import { FaCircle, FaVideo } from "react-icons/fa";
|
||||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -28,18 +30,22 @@ const SEGMENT_DURATION = 30;
|
|||||||
type RecordingViewProps = {
|
type RecordingViewProps = {
|
||||||
startCamera: string;
|
startCamera: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
severity: ReviewSeverity;
|
reviewItems?: ReviewSegment[];
|
||||||
reviewItems: ReviewSegment[];
|
reviewSummary?: ReviewSummary;
|
||||||
allCameras: string[];
|
allCameras: string[];
|
||||||
allPreviews?: Preview[];
|
allPreviews?: Preview[];
|
||||||
|
filter?: ReviewFilter;
|
||||||
|
updateFilter: (newFilter: ReviewFilter) => void;
|
||||||
};
|
};
|
||||||
export function RecordingView({
|
export function RecordingView({
|
||||||
startCamera,
|
startCamera,
|
||||||
startTime,
|
startTime,
|
||||||
severity,
|
|
||||||
reviewItems,
|
reviewItems,
|
||||||
|
reviewSummary,
|
||||||
allCameras,
|
allCameras,
|
||||||
allPreviews,
|
allPreviews,
|
||||||
|
filter,
|
||||||
|
updateFilter,
|
||||||
}: RecordingViewProps) {
|
}: RecordingViewProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -54,7 +60,7 @@ export function RecordingView({
|
|||||||
const [playbackStart, setPlaybackStart] = useState(startTime);
|
const [playbackStart, setPlaybackStart] = useState(startTime);
|
||||||
|
|
||||||
const mainCameraReviewItems = useMemo(
|
const mainCameraReviewItems = useMemo(
|
||||||
() => reviewItems.filter((cam) => cam.camera == mainCamera),
|
() => reviewItems?.filter((cam) => cam.camera == mainCamera) ?? [],
|
||||||
[reviewItems, mainCamera],
|
[reviewItems, mainCamera],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -157,19 +163,15 @@ export function RecordingView({
|
|||||||
|
|
||||||
// motion timeline data
|
// motion timeline data
|
||||||
|
|
||||||
const { data: motionData } = useSWR<MotionData[]>(
|
const { data: motionData } = useSWR<MotionData[]>([
|
||||||
severity == "significant_motion"
|
"review/activity/motion",
|
||||||
? [
|
{
|
||||||
"review/activity/motion",
|
before: timeRange.end,
|
||||||
{
|
after: timeRange.start,
|
||||||
before: timeRange.end,
|
scale: SEGMENT_DURATION / 2,
|
||||||
after: timeRange.start,
|
cameras: mainCamera,
|
||||||
scale: SEGMENT_DURATION / 2,
|
},
|
||||||
cameras: mainCamera,
|
]);
|
||||||
},
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
const mainCameraAspect = useMemo(() => {
|
const mainCameraAspect = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -201,41 +203,61 @@ export function RecordingView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={contentRef} className="relative size-full">
|
<div ref={contentRef} className="relative size-full">
|
||||||
<Button
|
<div
|
||||||
className="absolute top-0 left-0 rounded-lg"
|
className={`absolute left-0 top-0 mr-2 flex items-center justify-between ${isMobile ? "right-0" : "right-24"}`}
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
>
|
||||||
<IoMdArrowRoundBack className="size-5 mr-[10px]" />
|
<Button className="rounded-lg" onClick={() => navigate(-1)}>
|
||||||
Back
|
<IoMdArrowRoundBack className="size-5 mr-[10px]" />
|
||||||
</Button>
|
Back
|
||||||
{isMobile && (
|
</Button>
|
||||||
<DropdownMenu>
|
<div className="flex items-center justify-end">
|
||||||
<DropdownMenuTrigger asChild>
|
<CalendarFilterButton
|
||||||
<Button className="absolute top-0 right-0 rounded-lg capitalize">
|
day={
|
||||||
{mainCamera.replaceAll("_", " ")}
|
filter?.after == undefined
|
||||||
</Button>
|
? undefined
|
||||||
</DropdownMenuTrigger>
|
: new Date(filter.after * 1000)
|
||||||
<DropdownMenuContent>
|
}
|
||||||
<DropdownMenuRadioGroup
|
reviewSummary={reviewSummary}
|
||||||
value={mainCamera}
|
updateSelectedDay={(day) => {
|
||||||
onValueChange={(cam) => {
|
updateFilter({
|
||||||
setPlaybackStart(currentTime);
|
...filter,
|
||||||
setMainCamera(cam);
|
after: day == undefined ? undefined : day.getTime() / 1000,
|
||||||
}}
|
before:
|
||||||
>
|
day == undefined ? undefined : getEndOfDayTimestamp(day),
|
||||||
{allCameras.map((cam) => (
|
});
|
||||||
<DropdownMenuRadioItem
|
}}
|
||||||
key={cam}
|
/>
|
||||||
className="capitalize"
|
{isMobile && (
|
||||||
value={cam}
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="rounded-lg capitalize flex items-center gap-2"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{cam.replaceAll("_", " ")}
|
<FaVideo className="text-muted-foreground" />
|
||||||
</DropdownMenuRadioItem>
|
{mainCamera.replaceAll("_", " ")}
|
||||||
))}
|
</Button>
|
||||||
</DropdownMenuRadioGroup>
|
</DrawerTrigger>
|
||||||
</DropdownMenuContent>
|
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
||||||
</DropdownMenu>
|
{allCameras.map((cam) => (
|
||||||
)}
|
<FilterCheckBox
|
||||||
|
key={cam}
|
||||||
|
CheckIcon={FaCircle}
|
||||||
|
iconClassName="size-2"
|
||||||
|
label={cam.replaceAll("_", " ")}
|
||||||
|
isChecked={cam == mainCamera}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
setPlaybackStart(currentTime);
|
||||||
|
setMainCamera(cam);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex h-full justify-center overflow-hidden ${isDesktop ? "" : "flex-col pt-12"}`}
|
className={`flex h-full justify-center overflow-hidden ${isDesktop ? "" : "flex-col pt-12"}`}
|
||||||
@ -312,36 +334,20 @@ export function RecordingView({
|
|||||||
: "flex-grow overflow-hidden"
|
: "flex-grow overflow-hidden"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{severity != "significant_motion" ? (
|
<MotionReviewTimeline
|
||||||
<EventReviewTimeline
|
segmentDuration={30}
|
||||||
segmentDuration={30}
|
timestampSpread={15}
|
||||||
timestampSpread={15}
|
timelineStart={timeRange.end}
|
||||||
timelineStart={timeRange.end}
|
timelineEnd={timeRange.start}
|
||||||
timelineEnd={timeRange.start}
|
showHandlebar
|
||||||
showHandlebar
|
handlebarTime={currentTime}
|
||||||
handlebarTime={currentTime}
|
setHandlebarTime={setCurrentTime}
|
||||||
setHandlebarTime={setCurrentTime}
|
events={mainCameraReviewItems}
|
||||||
events={mainCameraReviewItems}
|
motion_events={motionData ?? []}
|
||||||
severityType={severity}
|
severityType="significant_motion"
|
||||||
contentRef={contentRef}
|
contentRef={contentRef}
|
||||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<MotionReviewTimeline
|
|
||||||
segmentDuration={30}
|
|
||||||
timestampSpread={15}
|
|
||||||
timelineStart={timeRange.end}
|
|
||||||
timelineEnd={timeRange.start}
|
|
||||||
showHandlebar
|
|
||||||
handlebarTime={currentTime}
|
|
||||||
setHandlebarTime={setCurrentTime}
|
|
||||||
events={mainCameraReviewItems}
|
|
||||||
motion_events={motionData ?? []}
|
|
||||||
severityType={severity}
|
|
||||||
contentRef={contentRef}
|
|
||||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user