Update recordings view (#10585)

* Update recordings view

* Fix opening recordings view from gif
This commit is contained in:
Nicolas Mowen 2024-03-21 07:43:37 -06:00 committed by GitHub
parent f113acee33
commit 865c26ff18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 124 additions and 102 deletions

View File

@ -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>

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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>