frigate/web/src/components/overlay/detail/AnnotationSettingsPane.tsx
GuoQing Liu d34533981f
feat: add i18n (translation/localization) (#16877)
* Translation module init

* Add more i18n keys

* fix: fix string wrong

* refactor: use namespace translation file

* chore: add more translation key

* fix: fix some page name error

* refactor: change Trans tag for t function

* chore: fix some key not work

* chore: fix SearchFilterDialog i18n key error

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* chore: fix en i18n file filter missing some keys

* chore: add some i18n keys

* chore: add more i18n keys again

* feat: add search page i18n

* feat: add explore model i18n keys

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add more live i18n keys

* feat: add more search setting i18n keys

* fix: remove some comment

* fix: fix some setting page url error

* Update web/src/views/settings/SearchSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* fix: add system missing keys

* fix: update password update i18n keys

* chore: remove outdate translation.json file

* fix: fix exploreSettings error

* chore: add object setting i18n keys

* Update web/src/views/recording/RecordingView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/public/locales/en/components/filter.json

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/overlay/ExportDialog.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add more i18n keys

* fix: fix motionDetectionTuner html node

* feat: add more page i18n keys

* fix: cameraStream i18n keys error

* feat: add Player i18n keys

* feat: add more toast i18n keys

* feat: change explore setting name

* feat: add more document title i18n keys

* feat: add more search i18n keys

* fix: fix accessDenied i18n keys error

* chore: add objectType i18n

* chore: add  inputWithTags i18n

* chore: add SearchFilterDialog i18n

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* chore: add some missing i18n keys

* chore: remove most import { t } from "i18next";

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-03-16 10:36:20 -05:00

246 lines
8.0 KiB
TypeScript

import Heading from "@/components/ui/heading";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Event } from "@/types/event";
import { FrigateConfig } from "@/types/frigateConfig";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { useCallback, useState } from "react";
import { useForm } from "react-hook-form";
import { LuExternalLink } from "react-icons/lu";
import { PiWarningCircle } from "react-icons/pi";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import useSWR from "swr";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Trans, useTranslation } from "react-i18next";
type AnnotationSettingsPaneProps = {
event: Event;
showZones: boolean;
setShowZones: React.Dispatch<React.SetStateAction<boolean>>;
annotationOffset: number;
setAnnotationOffset: React.Dispatch<React.SetStateAction<number>>;
};
export function AnnotationSettingsPane({
event,
showZones,
setShowZones,
annotationOffset,
setAnnotationOffset,
}: AnnotationSettingsPaneProps) {
const { t } = useTranslation(["views/explore"]);
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [isLoading, setIsLoading] = useState(false);
const formSchema = z.object({
annotationOffset: z.coerce.number().optional().or(z.literal("")),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onChange",
defaultValues: {
annotationOffset: annotationOffset,
},
});
const saveToConfig = useCallback(
async (annotation_offset: number | string) => {
if (!config || !event) {
return;
}
axios
.put(
`config/set?cameras.${event?.camera}.detect.annotation_offset=${annotation_offset}`,
{
requires_restart: 0,
},
)
.then((res) => {
if (res.status === 200) {
toast.success(
`Annotation offset for ${event?.camera} has been saved to the config file. Restart Frigate to apply your changes.`,
{
position: "top-center",
},
);
updateConfig();
} else {
toast.error(
t("toast.save.error", {
errorMessage: res.statusText,
ns: "common",
}),
{
position: "top-center",
},
);
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
position: "top-center",
});
})
.finally(() => {
setIsLoading(false);
});
},
[updateConfig, config, event, t],
);
function onSubmit(values: z.infer<typeof formSchema>) {
if (!values || values.annotationOffset == null || !config) {
return;
}
setIsLoading(true);
saveToConfig(values.annotationOffset);
}
function onApply(values: z.infer<typeof formSchema>) {
if (
!values ||
values.annotationOffset === null ||
values.annotationOffset === "" ||
!config
) {
return;
}
setAnnotationOffset(values.annotationOffset ?? 0);
}
return (
<div className="mb-3 space-y-3 rounded-lg border border-secondary-foreground bg-background_alt p-2">
<Heading as="h4" className="my-2">
{t("objectLifecycle.annotationSettings.title")}
</Heading>
<div className="flex flex-col">
<div className="flex flex-row items-center justify-start gap-2 p-3">
<Switch
id="show-zones"
checked={showZones}
onCheckedChange={setShowZones}
/>
<Label className="cursor-pointer" htmlFor="show-zones">
{t("objectLifecycle.annotationSettings.showAllZones")}
</Label>
</div>
<div className="text-sm text-muted-foreground">
{t("objectLifecycle.annotationSettings.showAllZones.desc")}
</div>
</div>
<Separator className="my-2 flex bg-secondary" />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-1 flex-col space-y-6"
>
<FormField
control={form.control}
name="annotationOffset"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("objectLifecycle.annotationSettings.offset.label")}
</FormLabel>
<div className="flex flex-col gap-3 md:flex-row-reverse md:gap-8">
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-0 md:my-5">
<PiWarningCircle className="size-24" />
<div>
<Trans ns="views/explore">
objectLifecycle.annotationSettings.offset.desc
</Trans>
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/reference"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t(
"objectLifecycle.annotationSettings.offset.documentation",
)}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</div>
</div>
<div className="flex flex-col">
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="0"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"objectLifecycle.annotationSettings.offset.millisecondsToOffset",
)}
<div className="mt-2">
{t("objectLifecycle.annotationSettings.offset.tips")}
</div>
</FormDescription>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-1 flex-col justify-end">
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label={t("button.apply", { ns: "common" })}
onClick={form.handleSubmit(onApply)}
>
{t("button.apply", { ns: "common" })}
</Button>
<Button
variant="select"
aria-label={t("button.save", { ns: "common" })}
disabled={isLoading}
className="flex flex-1"
type="submit"
>
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>{t("button.saving", { ns: "common" })}</span>
</div>
) : (
t("button.save", { ns: "common" })
)}
</Button>
</div>
</div>
</form>
</Form>
</div>
);
}