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