mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-17 02:26:43 +03:00
Compare commits
6 Commits
8610a3c4e5
...
a220678321
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a220678321 | ||
|
|
4b5e8ac9ef | ||
|
|
7a4fac57bb | ||
|
|
abf3b44118 | ||
|
|
f59adf97d6 | ||
|
|
d057d4ab58 |
@ -305,7 +305,7 @@ export function GroupedClassificationCard({
|
|||||||
<div>
|
<div>
|
||||||
<ContentTitle
|
<ContentTitle
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 font-normal capitalize",
|
"flex items-center gap-2 font-normal capitalize",
|
||||||
isMobile && "px-2",
|
isMobile && "px-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -42,11 +42,11 @@ export default function SearchThumbnailFooter({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-row items-center justify-between gap-2",
|
"flex w-full flex-row items-center justify-between gap-2 text-white",
|
||||||
columns > 4 && "items-start sm:flex-col lg:flex-row lg:items-center",
|
columns > 4 && "items-start sm:flex-col lg:flex-row lg:items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-start text-xs text-primary-variant">
|
<div className="flex flex-col items-start text-xs text-white/90 drop-shadow-lg">
|
||||||
{searchResult.end_time ? (
|
{searchResult.end_time ? (
|
||||||
<TimeAgo time={searchResult.start_time * 1000} dense />
|
<TimeAgo time={searchResult.start_time * 1000} dense />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -214,10 +214,14 @@ export default function SearchResultActions({
|
|||||||
searchResult.data.type == "object" && (
|
searchResult.data.type == "object" && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<MdImageSearch
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
{/* blurred circular hover background */}
|
||||||
onClick={findSimilar}
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
/>
|
<MdImageSearch
|
||||||
|
className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white"
|
||||||
|
onClick={findSimilar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("itemMenu.findSimilar.label")}
|
{t("itemMenu.findSimilar.label")}
|
||||||
@ -233,10 +237,13 @@ export default function SearchResultActions({
|
|||||||
!searchResult.plus_id && (
|
!searchResult.plus_id && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<FrigatePlusIcon
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
onClick={showSnapshot}
|
<FrigatePlusIcon
|
||||||
/>
|
className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white"
|
||||||
|
onClick={showSnapshot}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("itemMenu.submitToPlus.label")}
|
{t("itemMenu.submitToPlus.label")}
|
||||||
@ -246,7 +253,10 @@ export default function SearchResultActions({
|
|||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<FiMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
|
<FiMoreVertical className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white" />
|
||||||
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">{menuItems}</DropdownMenuContent>
|
<DropdownMenuContent align="end">{menuItems}</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
|||||||
import { IoPlayCircleOutline } from "react-icons/io5";
|
import { IoPlayCircleOutline } from "react-icons/io5";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getTranslatedLabel } from "@/utils/i18n";
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
type ObjectLifecycleProps = {
|
type ObjectLifecycleProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -355,6 +356,52 @@ export default function ObjectLifecycle({
|
|||||||
return idx === -1 ? 0 : idx;
|
return idx === -1 ? 0 : idx;
|
||||||
}, [eventSequence, timeIndex]);
|
}, [eventSequence, timeIndex]);
|
||||||
|
|
||||||
|
// Calculate how far down the blue line should extend based on timeIndex
|
||||||
|
const calculateLineHeight = () => {
|
||||||
|
if (!eventSequence || eventSequence.length === 0) return 0;
|
||||||
|
|
||||||
|
const currentTime = timeIndex ?? 0;
|
||||||
|
|
||||||
|
// Find which events have been passed
|
||||||
|
let lastPassedIndex = -1;
|
||||||
|
for (let i = 0; i < eventSequence.length; i++) {
|
||||||
|
if (currentTime >= (eventSequence[i].timestamp ?? 0)) {
|
||||||
|
lastPassedIndex = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No events passed yet
|
||||||
|
if (lastPassedIndex < 0) return 0;
|
||||||
|
|
||||||
|
// All events passed
|
||||||
|
if (lastPassedIndex >= eventSequence.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 / (eventSequence.length - 1);
|
||||||
|
|
||||||
|
// Find progress between current and next event for smooth transition
|
||||||
|
const currentEvent = eventSequence[lastPassedIndex];
|
||||||
|
const nextEvent = eventSequence[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();
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -569,7 +616,7 @@ export default function ObjectLifecycle({
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md bg-secondary p-2 outline outline-[3px] -outline-offset-[2.8px] outline-transparent duration-500",
|
"rounded-md bg-secondary p-3 outline outline-[3px] -outline-offset-[2.8px] outline-transparent duration-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
@ -581,10 +628,12 @@ export default function ObjectLifecycle({
|
|||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
{getIconForLabel(
|
<div className={cn("ml-1 rounded-full bg-muted-foreground p-2")}>
|
||||||
event.label,
|
{getIconForLabel(
|
||||||
"size-6 text-primary dark:text-white",
|
event.label,
|
||||||
)}
|
"size-6 text-primary dark:text-white",
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex items-end gap-2">
|
<div className="flex items-end gap-2">
|
||||||
<span>{getTranslatedLabel(event.label)}</span>
|
<span>{getTranslatedLabel(event.label)}</span>
|
||||||
<span className="text-secondary-foreground">
|
<span className="text-secondary-foreground">
|
||||||
@ -602,147 +651,79 @@ export default function ObjectLifecycle({
|
|||||||
{t("detail.noObjectDetailData", { ns: "views/events" })}
|
{t("detail.noObjectDetailData", { ns: "views/events" })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-2 mt-4 space-y-2">
|
<div className="-pb-2 relative mx-2">
|
||||||
{eventSequence.map((item, idx) => {
|
<div className="absolute -top-2 bottom-8 left-4 z-0 w-0.5 -translate-x-1/2 bg-secondary-foreground" />
|
||||||
const isActive =
|
<div
|
||||||
Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5;
|
className="absolute left-4 top-2 z-[5] max-h-[calc(100%-3rem)] w-0.5 -translate-x-1/2 bg-selected transition-all duration-300"
|
||||||
const formattedEventTimestamp = config
|
style={{ height: `${blueLineHeight}%` }}
|
||||||
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
|
/>
|
||||||
timezone: config.ui.timezone,
|
<div className="space-y-2">
|
||||||
date_format:
|
{eventSequence.map((item, idx) => {
|
||||||
config.ui.time_format == "24hour"
|
const isActive =
|
||||||
? t(
|
Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5;
|
||||||
"time.formattedTimestampHourMinuteSecond.24hour",
|
const formattedEventTimestamp = config
|
||||||
{ ns: "common" },
|
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
|
||||||
)
|
timezone: config.ui.timezone,
|
||||||
: t(
|
date_format:
|
||||||
"time.formattedTimestampHourMinuteSecond.12hour",
|
config.ui.time_format == "24hour"
|
||||||
{ ns: "common" },
|
? t(
|
||||||
),
|
"time.formattedTimestampHourMinuteSecond.24hour",
|
||||||
time_style: "medium",
|
{ ns: "common" },
|
||||||
date_style: "medium",
|
)
|
||||||
})
|
: t(
|
||||||
: "";
|
"time.formattedTimestampHourMinuteSecond.12hour",
|
||||||
|
{ ns: "common" },
|
||||||
|
),
|
||||||
|
time_style: "medium",
|
||||||
|
date_style: "medium",
|
||||||
|
})
|
||||||
|
: "";
|
||||||
|
|
||||||
const ratio =
|
const ratio =
|
||||||
Array.isArray(item.data.box) && item.data.box.length >= 4
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
? (
|
? (
|
||||||
aspectRatio *
|
aspectRatio *
|
||||||
(item.data.box[2] / item.data.box[3])
|
(item.data.box[2] / item.data.box[3])
|
||||||
).toFixed(2)
|
).toFixed(2)
|
||||||
: "N/A";
|
: "N/A";
|
||||||
const areaPx =
|
const areaPx =
|
||||||
Array.isArray(item.data.box) && item.data.box.length >= 4
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(config.cameras[event.camera]?.detect?.width ?? 0) *
|
(config.cameras[event.camera]?.detect?.width ?? 0) *
|
||||||
(config.cameras[event.camera]?.detect?.height ??
|
(config.cameras[event.camera]?.detect?.height ??
|
||||||
0) *
|
0) *
|
||||||
(item.data.box[2] * item.data.box[3]),
|
(item.data.box[2] * item.data.box[3]),
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const areaPct =
|
const areaPct =
|
||||||
Array.isArray(item.data.box) && item.data.box.length >= 4
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
? (item.data.box[2] * item.data.box[3]).toFixed(4)
|
? (item.data.box[2] * item.data.box[3]).toFixed(4)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<LifecycleIconRow
|
||||||
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
|
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
|
||||||
role="button"
|
item={item}
|
||||||
onClick={() => {
|
isActive={isActive}
|
||||||
setTimeIndex(item.timestamp ?? 0);
|
formattedEventTimestamp={formattedEventTimestamp}
|
||||||
handleSetBox(
|
ratio={ratio}
|
||||||
item.data.box ?? [],
|
areaPx={areaPx}
|
||||||
item.data.attribute_box,
|
areaPct={areaPct}
|
||||||
);
|
onClick={() => {
|
||||||
setLifecycleZones(item.data.zones);
|
setTimeIndex(item.timestamp ?? 0);
|
||||||
setSelectedZone("");
|
handleSetBox(
|
||||||
}}
|
item.data.box ?? [],
|
||||||
className={cn(
|
item.data.attribute_box,
|
||||||
"flex cursor-pointer flex-col gap-1 rounded-md p-2 text-sm text-primary-variant",
|
);
|
||||||
isActive
|
setLifecycleZones(item.data.zones);
|
||||||
? "bg-secondary-highlight font-semibold text-primary outline-[1.5px] -outline-offset-[1.1px] outline-primary/40 dark:font-normal"
|
setSelectedZone("");
|
||||||
: "duration-500",
|
}}
|
||||||
)}
|
setSelectedZone={setSelectedZone}
|
||||||
>
|
getZoneColor={getZoneColor}
|
||||||
<div className="flex items-center gap-2">
|
/>
|
||||||
<div className="flex size-7 items-center justify-center">
|
);
|
||||||
<LifecycleIcon
|
})}
|
||||||
lifecycleItem={item}
|
</div>
|
||||||
className="size-5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row justify-between">
|
|
||||||
<div>{getLifecycleItemDescription(item)}</div>
|
|
||||||
<div className={cn("p-1 text-sm")}>
|
|
||||||
{formattedEventTimestamp}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-8 mt-1 flex flex-wrap items-center gap-3 text-sm text-secondary-foreground">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center 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-center gap-1">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"objectLifecycle.lifecycleItemDesc.header.area",
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{areaPx !== undefined && areaPct !== undefined ? (
|
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
px: {areaPx} · %: {areaPct}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>N/A</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.class_type === "entered_zone" && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"objectLifecycle.lifecycleItemDesc.header.zones",
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{item.data.zones.map((zone, zidx) => (
|
|
||||||
<div
|
|
||||||
key={`${zone}-${zidx}`}
|
|
||||||
className="flex cursor-pointer items-center gap-1"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setSelectedZone(zone);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="size-3 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `rgb(${getZoneColor(zone)})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="smart-capitalize">
|
|
||||||
{zone.replaceAll("_", " ")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -789,3 +770,117 @@ export function LifecycleIcon({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LifecycleIconRowProps = {
|
||||||
|
item: ObjectLifecycleSequence;
|
||||||
|
isActive?: boolean;
|
||||||
|
formattedEventTimestamp: string;
|
||||||
|
ratio: string;
|
||||||
|
areaPx?: number;
|
||||||
|
areaPct?: string;
|
||||||
|
onClick: () => void;
|
||||||
|
setSelectedZone: (z: string) => void;
|
||||||
|
getZoneColor: (zoneName: string) => number[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function LifecycleIconRow({
|
||||||
|
item,
|
||||||
|
isActive,
|
||||||
|
formattedEventTimestamp,
|
||||||
|
ratio,
|
||||||
|
areaPx,
|
||||||
|
areaPct,
|
||||||
|
onClick,
|
||||||
|
setSelectedZone,
|
||||||
|
getZoneColor,
|
||||||
|
}: LifecycleIconRowProps) {
|
||||||
|
const { t } = useTranslation(["views/explore"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"rounded-md p-2 text-sm text-primary-variant",
|
||||||
|
isActive && "bg-secondary-highlight font-semibold text-primary",
|
||||||
|
!isActive && "duration-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex size-4 items-center justify-center">
|
||||||
|
<LuCircle
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 ml-[1px] size-2.5 fill-secondary-foreground stroke-none",
|
||||||
|
isActive && "fill-selected duration-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-row justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div>{getLifecycleItemDescription(item)}</div>
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-secondary-foreground md:gap-5">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("objectLifecycle.lifecycleItemDesc.header.ratio")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">{ratio}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("objectLifecycle.lifecycleItemDesc.header.area")}
|
||||||
|
</span>
|
||||||
|
{areaPx !== undefined && areaPct !== undefined ? (
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{t("information.pixels", { ns: "common", area: areaPx })} ·{" "}
|
||||||
|
{areaPct}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>N/A</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.data?.zones && item.data.zones.length > 0 && (
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{item.data.zones.map((zone, zidx) => {
|
||||||
|
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={`${zone}-${zidx}`}
|
||||||
|
variant="outline"
|
||||||
|
className="inline-flex cursor-pointer items-center gap-2"
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedZone(zone);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
borderColor: `rgba(${color}, 0.6)`,
|
||||||
|
background: `rgba(${color}, 0.08)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="size-1 rounded-full"
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
backgroundColor: `rgb(${color})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="smart-capitalize">
|
||||||
|
{zone.replaceAll("_", " ")}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn("p-1 text-sm")}>{formattedEventTimestamp}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -858,14 +858,20 @@ function FaceAttemptGroup({
|
|||||||
faceNames={faceNames}
|
faceNames={faceNames}
|
||||||
onTrainAttempt={(name) => onTrainAttempt(data, name)}
|
onTrainAttempt={(name) => onTrainAttempt(data, name)}
|
||||||
>
|
>
|
||||||
<AddFaceIcon className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" />
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
|
<AddFaceIcon className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white" />
|
||||||
|
</div>
|
||||||
</FaceSelectionDialog>
|
</FaceSelectionDialog>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<LuRefreshCw
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40"
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
onClick={() => onReprocess(data)}
|
<LuRefreshCw
|
||||||
/>
|
className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white"
|
||||||
|
onClick={() => onReprocess(data)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
|
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -811,7 +811,10 @@ function StateTrainGrid({
|
|||||||
image={data.filename}
|
image={data.filename}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
>
|
>
|
||||||
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" />
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
|
<TbCategoryPlus className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white" />
|
||||||
|
</div>
|
||||||
</ClassificationSelectionDialog>
|
</ClassificationSelectionDialog>
|
||||||
</ClassificationCard>
|
</ClassificationCard>
|
||||||
</div>
|
</div>
|
||||||
@ -958,7 +961,10 @@ function ObjectTrainGrid({
|
|||||||
image={data.filename}
|
image={data.filename}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
>
|
>
|
||||||
<TbCategoryPlus className="size-7 cursor-pointer p-1 text-gray-200 hover:rounded-full hover:bg-primary-foreground/40" />
|
<div className="group relative inline-flex items-center justify-center">
|
||||||
|
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||||
|
<TbCategoryPlus className="relative z-10 size-5 cursor-pointer text-white/85 hover:text-white" />
|
||||||
|
</div>
|
||||||
</ClassificationSelectionDialog>
|
</ClassificationSelectionDialog>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -577,7 +577,7 @@ export default function SearchView({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square w-full overflow-hidden rounded-t-lg border",
|
"relative aspect-square w-full overflow-hidden rounded-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SearchThumbnail
|
<SearchThumbnail
|
||||||
@ -634,38 +634,38 @@ export default function SearchView({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-30 bg-gradient-to-t from-black/70 to-transparent p-2">
|
||||||
|
<SearchThumbnailFooter
|
||||||
|
searchResult={value}
|
||||||
|
columns={columns}
|
||||||
|
findSimilar={() => {
|
||||||
|
if (config?.semantic_search.enabled) {
|
||||||
|
setSimilaritySearch(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
refreshResults={refresh}
|
||||||
|
showObjectLifecycle={() =>
|
||||||
|
onSelectSearch(value, false, "object_lifecycle")
|
||||||
|
}
|
||||||
|
showSnapshot={() =>
|
||||||
|
onSelectSearch(value, false, "snapshot")
|
||||||
|
}
|
||||||
|
addTrigger={() => {
|
||||||
|
if (
|
||||||
|
config?.semantic_search.enabled &&
|
||||||
|
value.data.type == "object"
|
||||||
|
) {
|
||||||
|
navigate(
|
||||||
|
`/settings?page=triggers&camera=${value.camera}&event_id=${value.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-selected outline-selected` : "outline-transparent duration-500"}`}
|
className={`review-item-ring pointer-events-none absolute inset-0 z-30 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-selected outline-selected` : "outline-transparent duration-500"}`}
|
||||||
/>
|
/>
|
||||||
<div className="flex w-full grow items-center justify-between rounded-b-lg border border-t-0 bg-card p-3 text-card-foreground">
|
|
||||||
<SearchThumbnailFooter
|
|
||||||
searchResult={value}
|
|
||||||
columns={columns}
|
|
||||||
findSimilar={() => {
|
|
||||||
if (config?.semantic_search.enabled) {
|
|
||||||
setSimilaritySearch(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
refreshResults={refresh}
|
|
||||||
showObjectLifecycle={() =>
|
|
||||||
onSelectSearch(value, false, "object_lifecycle")
|
|
||||||
}
|
|
||||||
showSnapshot={() =>
|
|
||||||
onSelectSearch(value, false, "snapshot")
|
|
||||||
}
|
|
||||||
addTrigger={() => {
|
|
||||||
if (
|
|
||||||
config?.semantic_search.enabled &&
|
|
||||||
value.data.type == "object"
|
|
||||||
) {
|
|
||||||
navigate(
|
|
||||||
`/settings?page=triggers&camera=${value.camera}&event_id=${value.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user