mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +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";
|
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;
|
||||||
|
|||||||
@ -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/", "")}`}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user