import { useCallback, useMemo, useState } from "react"; import axios from "axios"; import { toast } from "sonner"; 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 { Button } from "@/components/ui/button"; import { HiDotsHorizontal } from "react-icons/hi"; import { SearchResult } from "@/types/search"; import { FrigateConfig } from "@/types/frigateConfig"; import { useIsAdmin } from "@/hooks/use-is-admin"; 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", "views/replay", ]); const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); const [isStarting, setIsStarting] = useState(false); const isAdmin = useIsAdmin(); 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]); // currently, audio event ids are not saved in review items const { data: reviewItem } = useSWR( search.data?.type === "audio" ? null : [`review/event/${search.id}`], ); const handleDebugReplay = useCallback(() => { setIsStarting(true); axios .post("debug_replay/start", { camera: search.camera, start_time: search.start_time, end_time: search.end_time, }) .then((response) => { if (response.status === 202 || response.status === 200) { navigate("/replay"); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; if (error.response?.status === 409) { toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), { position: "top-center", closeButton: true, dismissible: false, action: ( ), }); } else { toast.error(t("dialog.toast.error", { error: errorMessage }), { position: "top-center", }); } }) .finally(() => { setIsStarting(false); }); }, [navigate, search.camera, search.start_time, search.end_time, t]); // don't render menu at all if no options are available const hasSemanticSearchOption = config?.semantic_search.enabled && setSimilarity !== undefined && search.data?.type === "object"; const hasReviewItem = !!(reviewItem && reviewItem.id); const hasAdminTriggerOption = isAdmin && config?.semantic_search.enabled && search.data?.type === "object"; if ( !search.has_snapshot && !search.has_clip && !hasSemanticSearchOption && !hasReviewItem && !hasAdminTriggerOption ) { return null; } return (
{search.has_snapshot && (
{t("itemMenu.downloadSnapshot.label")}
)} {search.has_snapshot && (
{t("itemMenu.downloadCleanSnapshot.label")}
)} {search.has_clip && (
{t("itemMenu.downloadVideo.label")}
)} {config?.semantic_search.enabled && setSimilarity != undefined && search.data?.type == "object" && ( { setIsOpen(false); setTimeout(() => { setSearch?.(undefined); setSimilarity?.(); }, 0); }} >
{t("itemMenu.findSimilar.label")}
)} {reviewItem && reviewItem.id && ( { setIsOpen(false); setTimeout(() => { navigate(`/review?id=${reviewItem.id}`); }, 0); }} >
{t("itemMenu.viewInHistory.label")}
)} {isAdmin && config?.semantic_search.enabled && search.data.type == "object" && ( { setIsOpen(false); setTimeout(() => { navigate( `/settings?page=triggers&camera=${search.camera}&event_id=${search.id}`, ); }, 0); }} >
{t("itemMenu.addTrigger.label")}
)} {search.has_clip && ( { setIsOpen(false); handleDebugReplay(); }} > {isStarting ? t("dialog.starting", { ns: "views/replay" }) : t("itemMenu.debugReplay.label")} )}
); }