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