Add events list view to recordings view

This commit is contained in:
Nicolas Mowen 2024-03-26 09:58:55 -06:00
parent 35996733a9
commit bacf202fd4
5 changed files with 89 additions and 7 deletions

View File

@ -0,0 +1,65 @@
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";
type ReviewCardProps = {
event: ReviewSegment;
currentTime: number;
onClick?: () => void;
};
export default function ReviewCard({
event,
currentTime,
onClick,
}: ReviewCardProps) {
const { data: config } = useSWR<FrigateConfig>("config");
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}
>
<img
className={`size-full rounded-lg ${isSelected ? "outline outline-3 outline-offset-1 outline-selected" : ""}`}
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>
);
}

View File

@ -1,6 +1,8 @@
import { FunctionComponent, useEffect, useMemo, useState } from "react"; import { FunctionComponent, useEffect, useMemo, useState } from "react";
interface IProp { interface IProp {
/** OPTIONAL: classname */
className?: string;
/** The time to calculate time-ago from */ /** The time to calculate time-ago from */
time: number; time: number;
/** OPTIONAL: overwrite current time */ /** OPTIONAL: overwrite current time */
@ -73,6 +75,7 @@ const timeAgo = ({
}; };
const TimeAgo: FunctionComponent<IProp> = ({ const TimeAgo: FunctionComponent<IProp> = ({
className,
time, time,
manualRefreshInterval, manualRefreshInterval,
...rest ...rest
@ -105,6 +108,6 @@ const TimeAgo: FunctionComponent<IProp> = ({
[currentTime, rest, time], [currentTime, rest, time],
); );
return <span>{timeAgoValue}</span>; return <span className={className}>{timeAgoValue}</span>;
}; };
export default TimeAgo; export default TimeAgo;

View File

@ -191,7 +191,7 @@ export default function PreviewThumbnailPlayer({
<div className={`${imgLoaded ? "visible" : "invisible"}`}> <div className={`${imgLoaded ? "visible" : "invisible"}`}>
<img <img
ref={imgRef} ref={imgRef}
className={`w-full h-full transition-opacity ${ className={`size-full transition-opacity ${
playingBack ? "opacity-0" : "opacity-100" playingBack ? "opacity-0" : "opacity-100"
}`} }`}
src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`} src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}

View File

@ -671,7 +671,7 @@ function MotionReview({
} }
return timeRangeSegments.ranges.findIndex( return timeRangeSegments.ranges.findIndex(
(seg) => seg.start <= startTime && seg.end >= startTime, (seg) => seg.after <= startTime && seg.before >= startTime,
); );
// only render once // only render once
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -679,7 +679,7 @@ function MotionReview({
const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex); const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex);
const [currentTime, setCurrentTime] = useState<number>( const [currentTime, setCurrentTime] = useState<number>(
startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.end, startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.before,
); );
const currentTimeRange = useMemo( const currentTimeRange = useMemo(
() => timeRangeSegments.ranges[selectedRangeIdx], () => timeRangeSegments.ranges[selectedRangeIdx],
@ -693,11 +693,11 @@ function MotionReview({
useEffect(() => { useEffect(() => {
if ( if (
currentTime > currentTimeRange.end + 60 || currentTime > currentTimeRange.before + 60 ||
currentTime < currentTimeRange.start - 60 currentTime < currentTimeRange.after - 60
) { ) {
const index = timeRangeSegments.ranges.findIndex( const index = timeRangeSegments.ranges.findIndex(
(seg) => seg.start <= currentTime && seg.end >= currentTime, (seg) => seg.after <= currentTime && seg.before >= currentTime,
); );
if (index != -1) { if (index != -1) {

View File

@ -1,3 +1,4 @@
import ReviewCard from "@/components/card/ReviewCard";
import FilterCheckBox from "@/components/filter/FilterCheckBox"; import FilterCheckBox from "@/components/filter/FilterCheckBox";
import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup";
import PreviewPlayer, { import PreviewPlayer, {
@ -414,4 +415,17 @@ function Timeline({
</div> </div>
); );
} }
return (
<div className="w-60 h-full p-4 flex flex-col gap-4 bg-secondary overflow-auto">
{mainCameraReviewItems.map((review) => (
<ReviewCard
key={review.id}
event={review}
currentTime={currentTime}
onClick={() => setCurrentTime(review.start_time)}
/>
))}
</div>
);
} }