mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 21:25:24 +03:00
Add events list view to recordings view
This commit is contained in:
parent
35996733a9
commit
bacf202fd4
65
web/src/components/card/ReviewCard.tsx
Normal file
65
web/src/components/card/ReviewCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from "react";
|
||||
|
||||
interface IProp {
|
||||
/** OPTIONAL: classname */
|
||||
className?: string;
|
||||
/** The time to calculate time-ago from */
|
||||
time: number;
|
||||
/** OPTIONAL: overwrite current time */
|
||||
@ -73,6 +75,7 @@ const timeAgo = ({
|
||||
};
|
||||
|
||||
const TimeAgo: FunctionComponent<IProp> = ({
|
||||
className,
|
||||
time,
|
||||
manualRefreshInterval,
|
||||
...rest
|
||||
@ -105,6 +108,6 @@ const TimeAgo: FunctionComponent<IProp> = ({
|
||||
[currentTime, rest, time],
|
||||
);
|
||||
|
||||
return <span>{timeAgoValue}</span>;
|
||||
return <span className={className}>{timeAgoValue}</span>;
|
||||
};
|
||||
export default TimeAgo;
|
||||
|
||||
@ -191,7 +191,7 @@ export default function PreviewThumbnailPlayer({
|
||||
<div className={`${imgLoaded ? "visible" : "invisible"}`}>
|
||||
<img
|
||||
ref={imgRef}
|
||||
className={`w-full h-full transition-opacity ${
|
||||
className={`size-full transition-opacity ${
|
||||
playingBack ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
|
||||
|
||||
@ -671,7 +671,7 @@ function MotionReview({
|
||||
}
|
||||
|
||||
return timeRangeSegments.ranges.findIndex(
|
||||
(seg) => seg.start <= startTime && seg.end >= startTime,
|
||||
(seg) => seg.after <= startTime && seg.before >= startTime,
|
||||
);
|
||||
// only render once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -679,7 +679,7 @@ function MotionReview({
|
||||
|
||||
const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex);
|
||||
const [currentTime, setCurrentTime] = useState<number>(
|
||||
startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.end,
|
||||
startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.before,
|
||||
);
|
||||
const currentTimeRange = useMemo(
|
||||
() => timeRangeSegments.ranges[selectedRangeIdx],
|
||||
@ -693,11 +693,11 @@ function MotionReview({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentTime > currentTimeRange.end + 60 ||
|
||||
currentTime < currentTimeRange.start - 60
|
||||
currentTime > currentTimeRange.before + 60 ||
|
||||
currentTime < currentTimeRange.after - 60
|
||||
) {
|
||||
const index = timeRangeSegments.ranges.findIndex(
|
||||
(seg) => seg.start <= currentTime && seg.end >= currentTime,
|
||||
(seg) => seg.after <= currentTime && seg.before >= currentTime,
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import ReviewCard from "@/components/card/ReviewCard";
|
||||
import FilterCheckBox from "@/components/filter/FilterCheckBox";
|
||||
import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup";
|
||||
import PreviewPlayer, {
|
||||
@ -414,4 +415,17 @@ function Timeline({
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user