mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* show id field when editing zone * improve zone capitalization * Update NPU models and docs * fix mobilepage in tracked object details * Use thread lock for openvino to avoid concurrent requests with JinaV2 * fix hashing function to avoid collisions * remove extra flex div causing overflow * ensure header stays on top of video controls * don't smart capitalize friendly names * Fix incorrect object classification crop * don't display submit to plus if object doesn't have a snapshot * check for snapshot and clip in actions menu * frigate plus submission fix still show frigate+ section if snapshot has already been submitted and run optimistic update, local state was being overridden * Don't fail to show 0% when showing classification * Don't fail on file system error * Improve title and description for review genai * fix overflowing truncated review item description in detail stream * catch events with review items that start after the first timeline entry review items may start later than events within them, so subtract a padding from the start time in the filter so the start of events are not incorrectly filtered out of the list in the detail stream * also pad on review end_time * fix * change order of timeline zoom buttons on mobile * use grid to ensure genai title does not cause overflow * small tweaks * Cleanup --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { Event } from "@/types/event";
|
|
import { baseUrl } from "@/api/baseUrl";
|
|
import { ReviewSegment, REVIEW_PADDING } from "@/types/review";
|
|
import useSWR from "swr";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuPortal,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { HiDotsHorizontal } from "react-icons/hi";
|
|
import { SearchResult } from "@/types/search";
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
|
|
|
type Props = {
|
|
search: SearchResult | Event;
|
|
config?: FrigateConfig;
|
|
setSearch?: (s: SearchResult | undefined) => void;
|
|
setSimilarity?: () => void;
|
|
faceNames?: string[];
|
|
onTrainFace?: (name: string) => void;
|
|
hasFace?: boolean;
|
|
};
|
|
|
|
export default function DetailActionsMenu({
|
|
search,
|
|
config,
|
|
setSearch,
|
|
setSimilarity,
|
|
}: Props) {
|
|
const { t } = useTranslation(["views/explore", "views/faceLibrary"]);
|
|
const navigate = useNavigate();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const clipTimeRange = useMemo(() => {
|
|
const startTime = (search.start_time ?? 0) - REVIEW_PADDING;
|
|
const endTime = (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING;
|
|
return `start/${startTime}/end/${endTime}`;
|
|
}, [search]);
|
|
|
|
const { data: reviewItem } = useSWR<ReviewSegment>([
|
|
`review/event/${search.id}`,
|
|
]);
|
|
|
|
return (
|
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
|
<DropdownMenuTrigger>
|
|
<div className="rounded" role="button">
|
|
<HiDotsHorizontal className="size-4 text-muted-foreground" />
|
|
</div>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuPortal>
|
|
<DropdownMenuContent align="end">
|
|
{search.has_snapshot && (
|
|
<DropdownMenuItem>
|
|
<a
|
|
className="w-full"
|
|
href={`${baseUrl}api/events/${search.id}/snapshot.jpg?bbox=1`}
|
|
download={`${search.camera}_${search.label}.jpg`}
|
|
>
|
|
<div className="flex cursor-pointer items-center gap-2">
|
|
<span>{t("itemMenu.downloadSnapshot.label")}</span>
|
|
</div>
|
|
</a>
|
|
</DropdownMenuItem>
|
|
)}
|
|
{search.has_clip && (
|
|
<DropdownMenuItem>
|
|
<a
|
|
className="w-full"
|
|
href={`${baseUrl}api/${search.camera}/${clipTimeRange}/clip.mp4`}
|
|
download
|
|
>
|
|
<div className="flex cursor-pointer items-center gap-2">
|
|
<span>{t("itemMenu.downloadVideo.label")}</span>
|
|
</div>
|
|
</a>
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
{config?.semantic_search.enabled &&
|
|
setSimilarity != undefined &&
|
|
search.data?.type == "object" && (
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
setTimeout(() => {
|
|
setSearch?.(undefined);
|
|
setSimilarity?.();
|
|
}, 0);
|
|
}}
|
|
>
|
|
<div className="flex cursor-pointer items-center gap-2">
|
|
<span>{t("itemMenu.findSimilar.label")}</span>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
{reviewItem && reviewItem.id && (
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
setTimeout(() => {
|
|
navigate(`/review?id=${reviewItem.id}`);
|
|
}, 0);
|
|
}}
|
|
>
|
|
<div className="flex cursor-pointer items-center gap-2">
|
|
<span>{t("itemMenu.viewInHistory.label")}</span>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
{config?.semantic_search.enabled && search.data.type == "object" && (
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
setTimeout(() => {
|
|
navigate(
|
|
`/settings?page=triggers&camera=${search.camera}&event_id=${search.id}`,
|
|
);
|
|
}, 0);
|
|
}}
|
|
>
|
|
<div className="flex cursor-pointer items-center gap-2">
|
|
<span>{t("itemMenu.addTrigger.label")}</span>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenuPortal>
|
|
</DropdownMenu>
|
|
);
|
|
}
|