Files
frigate/web/src/components/timeline/EventMenu.tsx
T

205 lines
6.3 KiB
TypeScript
Raw Normal View History

2025-10-16 08:24:14 -05:00
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
2025-10-24 07:50:06 -05:00
DropdownMenuSeparator,
2025-10-16 08:24:14 -05:00
} from "@/components/ui/dropdown-menu";
import { HiDotsHorizontal } from "react-icons/hi";
import { useApiHost } from "@/api";
2026-05-17 15:40:33 -05:00
import { baseUrl } from "@/api/baseUrl";
2025-10-16 08:24:14 -05:00
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
2025-10-24 07:50:06 -05:00
import { Event } from "@/types/event";
2026-05-22 14:41:07 -05:00
import { REVIEW_PADDING } from "@/types/review";
2025-10-24 07:50:06 -05:00
import { FrigateConfig } from "@/types/frigateConfig";
2026-03-04 10:07:34 -06:00
import { useCallback, useState } from "react";
import { useIsAdmin } from "@/hooks/use-is-admin";
2026-03-04 10:07:34 -06:00
import axios from "axios";
import { toast } from "sonner";
import { Button } from "../ui/button";
2025-10-16 08:24:14 -05:00
type EventMenuProps = {
event: Event;
config?: FrigateConfig;
onOpenUpload?: (e: Event) => void;
onOpenSimilarity?: (e: Event) => void;
2025-10-25 17:15:36 -05:00
isSelected?: boolean;
onToggleSelection?: (event: Event | undefined) => void;
2025-10-16 08:24:14 -05:00
};
export default function EventMenu({
event,
config,
onOpenUpload,
onOpenSimilarity,
2025-10-25 17:15:36 -05:00
isSelected = false,
onToggleSelection,
2025-10-16 08:24:14 -05:00
}: EventMenuProps) {
const apiHost = useApiHost();
const navigate = useNavigate();
2026-03-04 10:07:34 -06:00
const { t } = useTranslation(["views/explore", "views/replay"]);
2025-10-25 17:15:36 -05:00
const [isOpen, setIsOpen] = useState(false);
const isAdmin = useIsAdmin();
2026-03-04 10:07:34 -06:00
const [isStarting, setIsStarting] = useState(false);
2025-10-16 08:24:14 -05:00
2025-10-24 07:50:06 -05:00
const handleObjectSelect = () => {
2025-10-25 17:15:36 -05:00
if (isSelected) {
onToggleSelection?.(undefined);
2025-10-24 07:50:06 -05:00
} else {
2025-10-25 17:15:36 -05:00
onToggleSelection?.(event);
2025-10-24 07:50:06 -05:00
}
};
2026-03-04 10:07:34 -06:00
const handleDebugReplay = useCallback(
(event: Event) => {
setIsStarting(true);
axios
.post("debug_replay/start", {
camera: event.camera,
2026-05-22 14:41:07 -05:00
start_time: (event.start_time ?? 0) - REVIEW_PADDING,
end_time: (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
2026-03-04 10:07:34 -06:00
})
.then((response) => {
if (response.status === 202 || response.status === 200) {
2026-03-04 10:07:34 -06:00
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: (
2026-05-17 15:40:33 -05:00
<a
href={`${baseUrl}replay`}
target="_blank"
rel="noopener noreferrer"
>
2026-03-04 10:07:34 -06:00
<Button>
{t("dialog.toast.goToReplay", { ns: "views/replay" })}
</Button>
</a>
),
},
);
} else {
2026-05-24 07:48:52 -05:00
toast.error(
t("dialog.toast.error", {
ns: "views/replay",
error: errorMessage,
}),
{
position: "top-center",
},
);
2026-03-04 10:07:34 -06:00
}
})
.finally(() => {
setIsStarting(false);
});
},
[navigate, t],
);
2025-10-16 08:24:14 -05:00
return (
2025-10-24 07:50:06 -05:00
<>
<span tabIndex={0} className="sr-only" />
2026-05-02 17:35:42 -05:00
<DropdownMenu modal={false} open={isOpen} onOpenChange={setIsOpen}>
2025-10-24 07:50:06 -05:00
<DropdownMenuTrigger>
<div className="rounded p-1 pr-2" role="button">
<HiDotsHorizontal className="size-4 text-muted-foreground" />
</div>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
2025-11-07 07:53:27 -06:00
<DropdownMenuItem
className="cursor-pointer"
onSelect={handleObjectSelect}
>
2025-10-25 17:15:36 -05:00
{isSelected
2025-10-24 07:50:06 -05:00
? t("itemMenu.hideObjectDetails.label")
: t("itemMenu.showObjectDetails.label")}
</DropdownMenuItem>
<DropdownMenuSeparator className="my-0.5" />
2025-10-16 08:24:14 -05:00
<DropdownMenuItem
2025-11-07 07:53:27 -06:00
className="cursor-pointer"
2025-10-16 08:24:14 -05:00
onSelect={() => {
2025-10-24 07:50:06 -05:00
navigate(`/explore?event_id=${event.id}`);
2025-10-16 08:24:14 -05:00
}}
>
2025-10-24 07:50:06 -05:00
{t("details.item.button.viewInExplore")}
2025-10-16 08:24:14 -05:00
</DropdownMenuItem>
2025-11-07 07:53:27 -06:00
<DropdownMenuItem className="cursor-pointer" asChild>
2025-10-24 07:50:06 -05:00
<a
download
href={
event.has_snapshot
? `${apiHost}api/events/${event.id}/snapshot.jpg?crop=0&bbox=1&timestamp=0`
2025-10-24 07:50:06 -05:00
: `${apiHost}api/events/${event.id}/thumbnail.webp`
}
>
{t("itemMenu.downloadSnapshot.label")}
</a>
</DropdownMenuItem>
{isAdmin &&
event.has_snapshot &&
2025-10-24 07:50:06 -05:00
event.plus_id == undefined &&
event.data.type == "object" &&
config?.plus?.enabled && (
<DropdownMenuItem
2025-11-07 07:53:27 -06:00
className="cursor-pointer"
2025-10-24 07:50:06 -05:00
onSelect={() => {
2025-10-25 17:15:36 -05:00
setIsOpen(false);
2025-10-24 07:50:06 -05:00
onOpenUpload?.(event);
}}
>
{t("itemMenu.submitToPlus.label")}
</DropdownMenuItem>
)}
{event.has_snapshot && config?.semantic_search?.enabled && (
<DropdownMenuItem
2025-11-07 07:53:27 -06:00
className="cursor-pointer"
2025-10-24 07:50:06 -05:00
onSelect={() => {
if (onOpenSimilarity) onOpenSimilarity(event);
else
navigate(
`/explore?search_type=similarity&event_id=${event.id}`,
);
}}
>
{t("itemMenu.findSimilar.label")}
</DropdownMenuItem>
)}
2026-05-24 07:48:52 -05:00
{isAdmin && event.has_clip && (
2026-03-04 10:07:34 -06:00
<DropdownMenuItem
className="cursor-pointer"
disabled={isStarting}
onSelect={() => {
handleDebugReplay(event);
}}
>
{isStarting
? t("dialog.starting", { ns: "views/replay" })
: t("itemMenu.debugReplay.label")}
</DropdownMenuItem>
)}
2025-10-24 07:50:06 -05:00
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
</>
2025-10-16 08:24:14 -05:00
);
}