mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-12 10:07:36 +03:00
fix progress line and add area and ratio tooltip
This commit is contained in:
parent
6b65cf6f98
commit
404ddbfbbc
@ -21,6 +21,7 @@ import { getTranslatedLabel } from "@/utils/i18n";
|
|||||||
import EventMenu from "@/components/timeline/EventMenu";
|
import EventMenu from "@/components/timeline/EventMenu";
|
||||||
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
|
||||||
type DetailStreamProps = {
|
type DetailStreamProps = {
|
||||||
reviewItems?: ReviewSegment[];
|
reviewItems?: ReviewSegment[];
|
||||||
@ -506,14 +507,14 @@ function EventList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LifecycleItemProps = {
|
type LifecycleItemProps = {
|
||||||
event: ObjectLifecycleSequence;
|
item: ObjectLifecycleSequence;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onSeek?: (timestamp: number, play?: boolean) => void;
|
onSeek?: (timestamp: number, play?: boolean) => void;
|
||||||
effectiveTime?: number;
|
effectiveTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function LifecycleItem({
|
function LifecycleItem({
|
||||||
event,
|
item,
|
||||||
isActive,
|
isActive,
|
||||||
onSeek,
|
onSeek,
|
||||||
effectiveTime,
|
effectiveTime,
|
||||||
@ -521,8 +522,19 @@ function LifecycleItem({
|
|||||||
const { t } = useTranslation("views/events");
|
const { t } = useTranslation("views/events");
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const aspectRatio = useMemo(() => {
|
||||||
|
if (!config || !item?.camera) {
|
||||||
|
return 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
config.cameras[item.camera].detect.width /
|
||||||
|
config.cameras[item.camera].detect.height
|
||||||
|
);
|
||||||
|
}, [config, item]);
|
||||||
|
|
||||||
const formattedEventTimestamp = config
|
const formattedEventTimestamp = config
|
||||||
? formatUnixTimestampToDateTime(event.timestamp ?? 0, {
|
? formatUnixTimestampToDateTime(item?.timestamp ?? 0, {
|
||||||
timezone: config.ui.timezone,
|
timezone: config.ui.timezone,
|
||||||
date_format:
|
date_format:
|
||||||
config.ui.time_format == "24hour"
|
config.ui.time_format == "24hour"
|
||||||
@ -537,11 +549,28 @@ function LifecycleItem({
|
|||||||
})
|
})
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const ratio =
|
||||||
|
Array.isArray(item?.data.box) && item?.data.box.length >= 4
|
||||||
|
? (aspectRatio * (item?.data.box[2] / item?.data.box[3])).toFixed(2)
|
||||||
|
: "N/A";
|
||||||
|
const areaPx =
|
||||||
|
Array.isArray(item?.data.box) && item?.data.box.length >= 4
|
||||||
|
? Math.round(
|
||||||
|
(config?.cameras[item?.camera]?.detect?.width ?? 0) *
|
||||||
|
(config?.cameras[item?.camera]?.detect?.height ?? 0) *
|
||||||
|
(item?.data.box[2] * item?.data.box[3]),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
const areaPct =
|
||||||
|
Array.isArray(item?.data.box) && item?.data.box.length >= 4
|
||||||
|
? (item?.data.box[2] * item?.data.box[3]).toFixed(4)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSeek?.(event.timestamp ?? 0, false);
|
onSeek?.(item.timestamp ?? 0, false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer items-center gap-2 text-sm text-primary-variant",
|
"flex cursor-pointer items-center gap-2 text-sm text-primary-variant",
|
||||||
@ -553,14 +582,43 @@ function LifecycleItem({
|
|||||||
<div className="relative flex size-4 items-center justify-center">
|
<div className="relative flex size-4 items-center justify-center">
|
||||||
<LuCircle
|
<LuCircle
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-10 size-2.5 fill-secondary-foreground stroke-none",
|
"relative z-10 ml-[1px] size-2.5 fill-secondary-foreground stroke-none",
|
||||||
(isActive || (effectiveTime ?? 0) >= (event.timestamp ?? 0)) &&
|
(isActive || (effectiveTime ?? 0) >= (item?.timestamp ?? 0)) &&
|
||||||
"fill-selected duration-300",
|
"fill-selected duration-300",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-row justify-between">
|
<div className="flex w-full flex-row justify-between">
|
||||||
<div>{getLifecycleItemDescription(event)}</div>
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<span>{getLifecycleItemDescription(item)}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="mt-1 flex flex-wrap items-start gap-3 text-sm text-secondary-foreground">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("objectLifecycle.lifecycleItemDesc.header.ratio")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-foreground">{ratio}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("objectLifecycle.lifecycleItemDesc.header.area")}
|
||||||
|
</span>
|
||||||
|
{areaPx !== undefined && areaPct !== undefined ? (
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{areaPx} {t("pixels", { ns: "common" })} · {areaPct}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>N/A</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<div className={cn("p-1 text-xs")}>{formattedEventTimestamp}</div>
|
<div className={cn("p-1 text-xs")}>{formattedEventTimestamp}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -601,24 +659,53 @@ function ObjectTimeline({
|
|||||||
const calculateLineHeight = () => {
|
const calculateLineHeight = () => {
|
||||||
if (!timeline || timeline.length === 0) return 0;
|
if (!timeline || timeline.length === 0) return 0;
|
||||||
|
|
||||||
const firstTimestamp = timeline[0].timestamp ?? 0;
|
const currentTime = effectiveTime ?? 0;
|
||||||
const lastTimestamp = timeline[timeline.length - 1].timestamp ?? 0;
|
|
||||||
|
|
||||||
if ((effectiveTime ?? 0) <= firstTimestamp) return 0;
|
// Find which events have been passed
|
||||||
if ((effectiveTime ?? 0) >= lastTimestamp) return 100;
|
let lastPassedIndex = -1;
|
||||||
|
for (let i = 0; i < timeline.length; i++) {
|
||||||
|
if (currentTime >= (timeline[i].timestamp ?? 0)) {
|
||||||
|
lastPassedIndex = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const totalDuration = lastTimestamp - firstTimestamp;
|
// No events passed yet
|
||||||
const elapsed = (effectiveTime ?? 0) - firstTimestamp;
|
if (lastPassedIndex < 0) return 0;
|
||||||
return Math.min(100, Math.max(0, (elapsed / totalDuration) * 100));
|
|
||||||
|
// All events passed
|
||||||
|
if (lastPassedIndex >= timeline.length - 1) return 100;
|
||||||
|
|
||||||
|
// Calculate percentage based on item position, not time
|
||||||
|
// Each item occupies an equal visual space regardless of time gaps
|
||||||
|
const itemPercentage = 100 / (timeline.length - 1);
|
||||||
|
|
||||||
|
// Find progress between current and next event for smooth transition
|
||||||
|
const currentEvent = timeline[lastPassedIndex];
|
||||||
|
const nextEvent = timeline[lastPassedIndex + 1];
|
||||||
|
const currentTimestamp = currentEvent.timestamp ?? 0;
|
||||||
|
const nextTimestamp = nextEvent.timestamp ?? 0;
|
||||||
|
|
||||||
|
// Calculate interpolation between the two events
|
||||||
|
const timeBetween = nextTimestamp - currentTimestamp;
|
||||||
|
const timeElapsed = currentTime - currentTimestamp;
|
||||||
|
const interpolation = timeBetween > 0 ? timeElapsed / timeBetween : 0;
|
||||||
|
|
||||||
|
// Base position plus interpolated progress to next item
|
||||||
|
return Math.min(
|
||||||
|
100,
|
||||||
|
lastPassedIndex * itemPercentage + interpolation * itemPercentage,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const blueLineHeight = calculateLineHeight();
|
const blueLineHeight = calculateLineHeight();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-pb-2 relative mx-2">
|
<div className="-pb-2 relative mx-2">
|
||||||
<div className="absolute -top-2 bottom-2 left-2 z-0 w-[1px] -translate-x-1/2 bg-secondary-foreground" />
|
<div className="absolute -top-2 bottom-2 left-2 z-0 w-0.5 -translate-x-1/2 bg-secondary-foreground" />
|
||||||
<div
|
<div
|
||||||
className="absolute left-2 top-2 z-[5] max-h-[calc(100%-1rem)] w-[1px] -translate-x-1/2 bg-selected transition-all duration-300"
|
className="absolute left-2 top-2 z-[5] max-h-[calc(100%-1rem)] w-0.5 -translate-x-1/2 bg-selected transition-all duration-300"
|
||||||
style={{ height: `${blueLineHeight}%` }}
|
style={{ height: `${blueLineHeight}%` }}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -629,7 +716,7 @@ function ObjectTimeline({
|
|||||||
return (
|
return (
|
||||||
<LifecycleItem
|
<LifecycleItem
|
||||||
key={`${event.timestamp}-${event.source_id ?? ""}-${idx}`}
|
key={`${event.timestamp}-${event.source_id ?? ""}-${idx}`}
|
||||||
event={event}
|
item={event}
|
||||||
onSeek={onSeek}
|
onSeek={onSeek}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
effectiveTime={effectiveTime}
|
effectiveTime={effectiveTime}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user