mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-27 17:17:40 +03:00
add triggers from explore
This commit is contained in:
parent
aca9e15790
commit
efe5864ebc
@ -175,6 +175,10 @@
|
||||
"label": "Find similar",
|
||||
"aria": "Find similar tracked objects"
|
||||
},
|
||||
"addTrigger": {
|
||||
"label": "Add trigger",
|
||||
"aria": "Add a trigger for this tracked object"
|
||||
},
|
||||
"audioTranscription": {
|
||||
"label": "Transcribe",
|
||||
"aria": "Request audio transcription"
|
||||
|
||||
@ -673,15 +673,15 @@
|
||||
"dialog": {
|
||||
"createTrigger": {
|
||||
"title": "Create Trigger",
|
||||
"desc": "Create a new trigger for camera '{{camera}}'."
|
||||
"desc": "Create a trigger for camera {{camera}}"
|
||||
},
|
||||
"editTrigger": {
|
||||
"title": "Edit Trigger",
|
||||
"desc": "Edit the settings for an existing trigger on camera '{{camera}}'."
|
||||
"desc": "Edit the settings for trigger on camera {{camera}}"
|
||||
},
|
||||
"deleteTrigger": {
|
||||
"title": "Delete Trigger",
|
||||
"desc": "Are you sure you want to delete the trigger <strong>{{triggerName}}</strong> from camera '{{camera}}'? This action cannot be undone."
|
||||
"desc": "Are you sure you want to delete the trigger <strong>{{triggerName}}</strong> from camera {{camera}}? This action cannot be undone."
|
||||
},
|
||||
"form": {
|
||||
"name": {
|
||||
@ -725,9 +725,9 @@
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"createTrigger": "Trigger '{{name}}' created successfully.",
|
||||
"updateTrigger": "Trigger '{{name}}' updated successfully.",
|
||||
"deleteTrigger": "Trigger '{{name}}' deleted successfully."
|
||||
"createTrigger": "Trigger {{name}} created successfully.",
|
||||
"updateTrigger": "Trigger {{name}} updated successfully.",
|
||||
"deleteTrigger": "Trigger {{name}} deleted successfully."
|
||||
},
|
||||
"error": {
|
||||
"createTriggerFailed": "Failed to create trigger: {{errorMessage}}",
|
||||
|
||||
@ -15,6 +15,7 @@ type SearchThumbnailProps = {
|
||||
refreshResults: () => void;
|
||||
showObjectLifecycle: () => void;
|
||||
showSnapshot: () => void;
|
||||
addTrigger: () => void;
|
||||
};
|
||||
|
||||
export default function SearchThumbnailFooter({
|
||||
@ -24,6 +25,7 @@ export default function SearchThumbnailFooter({
|
||||
refreshResults,
|
||||
showObjectLifecycle,
|
||||
showSnapshot,
|
||||
addTrigger,
|
||||
}: SearchThumbnailProps) {
|
||||
const { t } = useTranslation(["views/search"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
@ -61,6 +63,7 @@ export default function SearchThumbnailFooter({
|
||||
refreshResults={refreshResults}
|
||||
showObjectLifecycle={showObjectLifecycle}
|
||||
showSnapshot={showSnapshot}
|
||||
addTrigger={addTrigger}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -41,6 +41,7 @@ import {
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { BsFillLightningFill } from "react-icons/bs";
|
||||
|
||||
type SearchResultActionsProps = {
|
||||
searchResult: SearchResult;
|
||||
@ -48,6 +49,7 @@ type SearchResultActionsProps = {
|
||||
refreshResults: () => void;
|
||||
showObjectLifecycle: () => void;
|
||||
showSnapshot: () => void;
|
||||
addTrigger: () => void;
|
||||
isContextMenu?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
@ -58,6 +60,7 @@ export default function SearchResultActions({
|
||||
refreshResults,
|
||||
showObjectLifecycle,
|
||||
showSnapshot,
|
||||
addTrigger,
|
||||
isContextMenu = false,
|
||||
children,
|
||||
}: SearchResultActionsProps) {
|
||||
@ -138,6 +141,16 @@ export default function SearchResultActions({
|
||||
<span>{t("itemMenu.findSimilar.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{config?.semantic_search?.enabled &&
|
||||
searchResult.data.type == "object" && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.addTrigger.aria")}
|
||||
onClick={addTrigger}
|
||||
>
|
||||
<BsFillLightningFill className="mr-2 size-4" />
|
||||
<span>{t("itemMenu.addTrigger.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMobileOnly &&
|
||||
config?.plus?.enabled &&
|
||||
searchResult.has_snapshot &&
|
||||
|
||||
@ -95,9 +95,7 @@ export default function CreateTriggerDialog({
|
||||
.number()
|
||||
.min(0, t("triggers.dialog.form.threshold.error.min"))
|
||||
.max(1, t("triggers.dialog.form.threshold.error.max")),
|
||||
actions: z
|
||||
.array(z.enum(["alert", "notification"]))
|
||||
.min(1, t("triggers.dialog.form.actions.error.min")),
|
||||
actions: z.array(z.enum(["alert", "notification"])),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@ -109,7 +107,7 @@ export default function CreateTriggerDialog({
|
||||
type: trigger?.type ?? "description",
|
||||
data: trigger?.data ?? "",
|
||||
threshold: trigger?.threshold ?? 0.5,
|
||||
actions: trigger?.actions ?? ["alert"],
|
||||
actions: trigger?.actions ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
@ -138,17 +136,22 @@ export default function CreateTriggerDialog({
|
||||
type: "description",
|
||||
data: "",
|
||||
threshold: 0.5,
|
||||
actions: ["alert"],
|
||||
actions: [],
|
||||
});
|
||||
} else if (trigger) {
|
||||
form.reset({
|
||||
enabled: trigger.enabled,
|
||||
name: trigger.name,
|
||||
type: trigger.type,
|
||||
data: trigger.data,
|
||||
threshold: trigger.threshold,
|
||||
actions: trigger.actions,
|
||||
});
|
||||
form.reset(
|
||||
{
|
||||
enabled: trigger.enabled,
|
||||
name: trigger.name,
|
||||
type: trigger.type,
|
||||
data: trigger.data,
|
||||
threshold: trigger.threshold,
|
||||
actions: trigger.actions,
|
||||
},
|
||||
{ keepDirty: false, keepTouched: false }, // Reset validation state
|
||||
);
|
||||
// Trigger validation to ensure isValid updates
|
||||
// form.trigger();
|
||||
}
|
||||
}, [show, trigger, form]);
|
||||
|
||||
|
||||
@ -173,7 +173,7 @@ export default function Settings() {
|
||||
}
|
||||
}
|
||||
// don't clear url params if we're creating a new object mask
|
||||
return !searchParams.has("object_mask");
|
||||
return !(searchParams.has("object_mask") || searchParams.has("event_id"));
|
||||
});
|
||||
|
||||
useSearchEffect("camera", (camera: string) => {
|
||||
@ -181,8 +181,8 @@ export default function Settings() {
|
||||
if (cameraNames.includes(camera)) {
|
||||
setSelectedCamera(camera);
|
||||
}
|
||||
// don't clear url params if we're creating a new object mask
|
||||
return !searchParams.has("object_mask");
|
||||
// don't clear url params if we're creating a new object mask or trigger
|
||||
return !(searchParams.has("object_mask") || searchParams.has("event_id"));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -218,6 +218,7 @@ function ExploreThumbnailImage({
|
||||
const apiHost = useApiHost();
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleFindSimilar = () => {
|
||||
if (config?.semantic_search.enabled) {
|
||||
@ -233,6 +234,12 @@ function ExploreThumbnailImage({
|
||||
onSelectSearch(event, false, "snapshot");
|
||||
};
|
||||
|
||||
const handleAddTrigger = () => {
|
||||
navigate(
|
||||
`/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchResultActions
|
||||
searchResult={event}
|
||||
@ -240,6 +247,7 @@ function ExploreThumbnailImage({
|
||||
refreshResults={mutate}
|
||||
showObjectLifecycle={handleShowObjectLifecycle}
|
||||
showSnapshot={handleShowSnapshot}
|
||||
addTrigger={handleAddTrigger}
|
||||
isContextMenu={true}
|
||||
>
|
||||
<div className="relative size-full">
|
||||
|
||||
@ -32,6 +32,7 @@ import Chip from "@/components/indicators/Chip";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import SearchActionGroup from "@/components/filter/SearchActionGroup";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type SearchViewProps = {
|
||||
search: string;
|
||||
@ -76,6 +77,7 @@ export default function SearchView({
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
// grid
|
||||
|
||||
@ -648,6 +650,16 @@ export default function SearchView({
|
||||
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>
|
||||
|
||||
@ -26,6 +26,7 @@ import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog";
|
||||
import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Trigger, TriggerAction, TriggerType } from "@/types/trigger";
|
||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
|
||||
type ConfigSetBody = {
|
||||
requires_restart: number;
|
||||
@ -296,6 +297,24 @@ export default function TriggerView({
|
||||
}
|
||||
}, [selectedCamera, setUnsavedChanges]);
|
||||
|
||||
// for adding a trigger with event id via explore context menu
|
||||
|
||||
useSearchEffect("event_id", (eventId: string) => {
|
||||
if (!config || isLoading) {
|
||||
return false;
|
||||
}
|
||||
setShowCreate(true);
|
||||
setSelectedTrigger({
|
||||
enabled: true,
|
||||
name: "",
|
||||
type: "thumbnail",
|
||||
data: eventId,
|
||||
threshold: 0.5,
|
||||
actions: [],
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!config || !selectedCamera) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user