forked from Github/frigate
Redesign Recordings View (#10690)
* Use full width top bar * Make each item in review filter group optional * Remove export creation from export page * Consolidate packages and fix opening recording from event * Use common type for time range * Move timeline to separate component * Add events list view to recordings view * Fix loading of images * Fix incorrect labels * use overlay state for selected timeline type * Fix up for mobile view for now * replace overlay state * fix comparison * remove unused
This commit is contained in:
90
web/src/components/card/AnimatedEventCard.tsx
Normal file
90
web/src/components/card/AnimatedEventCard.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import TimeAgo from "../dynamic/TimeAgo";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
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";
|
||||
import axios from "axios";
|
||||
|
||||
type AnimatedEventCardProps = {
|
||||
event: ReviewSegment;
|
||||
};
|
||||
export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// interaction
|
||||
|
||||
const navigate = useNavigate();
|
||||
const onOpenReview = useCallback(() => {
|
||||
navigate("events", {
|
||||
state: {
|
||||
severity: event.severity,
|
||||
recording: {
|
||||
camera: event.camera,
|
||||
startTime: event.start_time,
|
||||
severity: event.severity,
|
||||
} as RecordingStartingPoint,
|
||||
},
|
||||
});
|
||||
axios.post(`reviews/viewed`, { ids: [event.id] });
|
||||
}, [navigate, event]);
|
||||
|
||||
// image behavior
|
||||
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState(0);
|
||||
const imageUrl = useMemo(() => {
|
||||
if (error > 0) {
|
||||
return `${baseUrl}api/review/${event.id}/preview.gif?key=${error}`;
|
||||
}
|
||||
|
||||
return `${baseUrl}api/review/${event.id}/preview.gif`;
|
||||
}, [error, event]);
|
||||
|
||||
const aspectRatio = useMemo(() => {
|
||||
if (!config) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const detect = config.cameras[event.camera].detect;
|
||||
return detect.width / detect.height;
|
||||
}, [config, event]);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="h-24 relative"
|
||||
style={{
|
||||
aspectRatio: aspectRatio,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className="size-full rounded object-cover object-center cursor-pointer"
|
||||
src={imageUrl}
|
||||
onClick={onOpenReview}
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => {
|
||||
if (error < 2) {
|
||||
setError(error + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{!loaded && <Skeleton className="absolute inset-0" />}
|
||||
<div className="absolute bottom-0 inset-x-0 h-6 bg-gradient-to-t from-slate-900/50 to-transparent rounded">
|
||||
<div className="w-full absolute left-1 bottom-0 text-xs text-white">
|
||||
<TimeAgo time={event.start_time * 1000} dense />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{`${[...event.data.objects, ...event.data.audio, ...(event.data.sub_labels || [])].join(", ")} detected`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
73
web/src/components/card/ReviewCard.tsx
Normal file
73
web/src/components/card/ReviewCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil";
|
||||
import { isSafari } from "react-device-detect";
|
||||
import useSWR from "swr";
|
||||
import TimeAgo from "../dynamic/TimeAgo";
|
||||
import { useMemo } from "react";
|
||||
import useImageLoaded from "@/hooks/use-image-loaded";
|
||||
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
||||
|
||||
type ReviewCardProps = {
|
||||
event: ReviewSegment;
|
||||
currentTime: number;
|
||||
onClick?: () => void;
|
||||
};
|
||||
export default function ReviewCard({
|
||||
event,
|
||||
currentTime,
|
||||
onClick,
|
||||
}: ReviewCardProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||
const formattedDate = useFormattedTimestamp(
|
||||
event.start_time,
|
||||
config?.ui.time_format == "24hour" ? "%H:%M" : "%I:%M %p",
|
||||
);
|
||||
const isSelected = useMemo(
|
||||
() => event.start_time <= currentTime && event.end_time >= currentTime,
|
||||
[event, currentTime],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex flex-col gap-1.5 cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<ImageLoadingIndicator
|
||||
className="size-full aspect-video"
|
||||
imgLoaded={imgLoaded}
|
||||
/>
|
||||
<img
|
||||
ref={imgRef}
|
||||
className={`size-full rounded-lg ${isSelected ? "outline outline-3 outline-offset-1 outline-selected" : ""} ${imgLoaded ? "visible" : "invisible"}`}
|
||||
src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`}
|
||||
loading={isSafari ? "eager" : "lazy"}
|
||||
onLoad={() => {
|
||||
onImgLoad();
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-evenly items-center gap-1">
|
||||
{event.data.objects.map((object) => {
|
||||
return getIconForLabel(object, "size-3 text-white");
|
||||
})}
|
||||
{event.data.audio.map((audio) => {
|
||||
return getIconForLabel(audio, "size-3 text-white");
|
||||
})}
|
||||
{event.data.sub_labels?.map((sub) => {
|
||||
return getIconForSubLabel(sub, "size-3 text-white");
|
||||
})}
|
||||
<div className="font-extra-light text-xs">{formattedDate}</div>
|
||||
</div>
|
||||
<TimeAgo
|
||||
className="text-xs text-muted-foreground"
|
||||
time={event.start_time * 1000}
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user