mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-06 13:37:43 +03:00
refactor: change Trans tag for t function
This commit is contained in:
parent
507ce019ff
commit
ef646203a7
@ -4,8 +4,9 @@ import {
|
||||
StatusMessage,
|
||||
} from "@/context/statusbar-provider";
|
||||
import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
|
||||
import { t } from "i18next";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { FaCheck } from "react-icons/fa";
|
||||
import { IoIosWarning } from "react-icons/io";
|
||||
import { MdCircle } from "react-icons/md";
|
||||
@ -130,7 +131,7 @@ export default function Statusbar() {
|
||||
{Object.entries(messages).length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FaCheck className="size-3 text-green-500" />
|
||||
<Trans ns="views/system">stats.healthy</Trans>
|
||||
{t("stats.healthy", { ns: "views/system" })}
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(messages).map(([key, messageArray]) => (
|
||||
|
||||
@ -15,7 +15,6 @@ import { DateRange } from "react-day-picker";
|
||||
import { useState } from "react";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type CalendarFilterButtonProps = {
|
||||
reviewSummary?: ReviewSummary;
|
||||
@ -70,7 +69,7 @@ export default function CalendarFilterButton({
|
||||
updateSelectedDay(undefined);
|
||||
}}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -70,18 +70,20 @@ import {
|
||||
MobilePageHeader,
|
||||
MobilePageTitle,
|
||||
} from "../mobile/MobilePage";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraGroupSelectorProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// tooltip
|
||||
@ -163,7 +165,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="" side="right">
|
||||
<Trans>menu.live.allCameras</Trans>
|
||||
{t("menu.live.allCameras")}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
@ -233,6 +235,7 @@ function NewGroupDialog({
|
||||
setGroup,
|
||||
deleteGroup,
|
||||
}: NewGroupDialogProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// editing group and state
|
||||
@ -354,12 +357,8 @@ function NewGroupDialog({
|
||||
className={cn(isDesktop && "mt-5", "justify-center")}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Title>
|
||||
<Trans ns="components/camera">group.label</Trans>
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
<Trans ns="components/camera">group.edit</Trans>
|
||||
</Description>
|
||||
<Title>{t("group.label")}</Title>
|
||||
<Description className="sr-only">{t("group.edit")}</Description>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute",
|
||||
@ -406,11 +405,7 @@ function NewGroupDialog({
|
||||
}}
|
||||
>
|
||||
<Title>
|
||||
{editState == "add" ? (
|
||||
<Trans ns="components/camera">group.add</Trans>
|
||||
) : (
|
||||
<Trans ns="components/camera">group.edit</Trans>
|
||||
)}
|
||||
{editState == "add" ? t("group.add") : t("group.edit")}
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
Edit camera groups
|
||||
@ -444,6 +439,7 @@ export function EditGroupDialog({
|
||||
currentGroups,
|
||||
activeGroup,
|
||||
}: EditGroupDialogProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const Overlay = isDesktop ? Dialog : MobilePage;
|
||||
const Content = isDesktop ? DialogContent : MobilePageContent;
|
||||
const Header = isDesktop ? DialogHeader : MobilePageHeader;
|
||||
@ -483,11 +479,9 @@ export function EditGroupDialog({
|
||||
>
|
||||
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
|
||||
<Header className="mt-2" onClose={() => setOpen(false)}>
|
||||
<Title>
|
||||
<Trans ns="components/camera">group.edit</Trans>
|
||||
</Title>
|
||||
<Title>{t("group.edit")}</Title>
|
||||
<Description className="sr-only">
|
||||
<Trans ns="components/camera">group.edit.desc</Trans>
|
||||
{t("group.edit.desc")}
|
||||
</Description>
|
||||
</Header>
|
||||
|
||||
@ -517,6 +511,7 @@ export function CameraGroupRow({
|
||||
onDeleteGroup,
|
||||
onEditGroup,
|
||||
}: CameraGroupRowProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
if (!group) {
|
||||
@ -538,24 +533,18 @@ export function CameraGroupRow({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans ns="components/camera">group.delete.confirm</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("group.delete.confirm")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
<Trans values={{ name: group[0] }}>
|
||||
group.delete.confirm.desc
|
||||
</Trans>
|
||||
{t("group.delete.confirm.desc", { name: group[0] })}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>button.cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={onDeleteGroup}
|
||||
>
|
||||
<Trans>button.delete</Trans>
|
||||
{t("button.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -573,13 +562,13 @@ export function CameraGroupRow({
|
||||
aria-label="Edit group"
|
||||
onClick={onEditGroup}
|
||||
>
|
||||
<Trans>button.edit</Trans>
|
||||
{t("button.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
aria-label="Delete group"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trans>button.delete</Trans>
|
||||
{t("button.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
@ -596,9 +585,7 @@ export function CameraGroupRow({
|
||||
onClick={onEditGroup}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>button.edit</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("button.edit")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
@ -609,9 +596,7 @@ export function CameraGroupRow({
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>button.delete</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("button.delete")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
@ -637,6 +622,7 @@ export function CameraGroupEdit({
|
||||
onSave,
|
||||
onCancel,
|
||||
}: CameraGroupEditProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -656,9 +642,7 @@ export function CameraGroupEdit({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: t("group.name.errorMessage.mustLeastCharacters", {
|
||||
ns: "components/camera",
|
||||
}),
|
||||
message: t("group.name.errorMessage.mustLeastCharacters"),
|
||||
})
|
||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||
.refine(
|
||||
@ -669,9 +653,7 @@ export function CameraGroupEdit({
|
||||
);
|
||||
},
|
||||
{
|
||||
message: t("group.name.errorMessage.exists", {
|
||||
ns: "components/camera",
|
||||
}),
|
||||
message: t("group.name.errorMessage.exists"),
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
@ -683,9 +665,7 @@ export function CameraGroupEdit({
|
||||
},
|
||||
)
|
||||
.refine((value: string) => value.toLowerCase() !== "default", {
|
||||
message: t("group.name.errorMessage.invalid", {
|
||||
ns: "components/camera",
|
||||
}),
|
||||
message: t("group.name.errorMessage.invalid"),
|
||||
}),
|
||||
|
||||
cameras: z.array(z.string()),
|
||||
@ -743,7 +723,6 @@ export function CameraGroupEdit({
|
||||
toast.success(
|
||||
t("group.toast.success", {
|
||||
name: values.name,
|
||||
ns: "components/camera",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
@ -788,6 +767,7 @@ export function CameraGroupEdit({
|
||||
groupStreamingSettings,
|
||||
allGroupsStreamingSettings,
|
||||
setAllGroupsStreamingSettings,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
@ -812,15 +792,11 @@ export function CameraGroupEdit({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="components/camera">group.name.label</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("group.name.label")}</FormLabel>
|
||||
<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={t("group.name.placeholder", {
|
||||
ns: "components/camera",
|
||||
})}
|
||||
placeholder={t("group.name.placeholder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -836,12 +812,8 @@ export function CameraGroupEdit({
|
||||
name="cameras"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="components/camera">group.cameras.label</Trans>
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
<Trans ns="components/camera">group.cameras.desc</Trans>
|
||||
</FormDescription>
|
||||
<FormLabel>{t("group.cameras.label")}</FormLabel>
|
||||
<FormDescription>{t("group.cameras.desc")}</FormDescription>
|
||||
<FormMessage />
|
||||
{[
|
||||
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
|
||||
@ -925,9 +897,7 @@ export function CameraGroupEdit({
|
||||
name="icon"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col space-y-2">
|
||||
<FormLabel>
|
||||
<Trans ns="components/camera">group.icon</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("group.icon")}</FormLabel>
|
||||
<FormControl>
|
||||
<IconPicker
|
||||
selectedIcon={{
|
||||
@ -955,7 +925,7 @@ export function CameraGroupEdit({
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -967,12 +937,10 @@ export function CameraGroupEdit({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -12,8 +12,7 @@ import { isMobile } from "react-device-detect";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraFilterButtonProps = {
|
||||
allCameras: string[];
|
||||
@ -31,6 +30,7 @@ export function CamerasFilterButton({
|
||||
mainCamera,
|
||||
updateCameraFilter,
|
||||
}: CameraFilterButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
||||
selectedCameras,
|
||||
@ -46,7 +46,7 @@ export function CamerasFilterButton({
|
||||
}
|
||||
|
||||
return `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`;
|
||||
}, [selectedCameras]);
|
||||
}, [selectedCameras, t]);
|
||||
|
||||
// ui
|
||||
|
||||
@ -140,12 +140,13 @@ export function CamerasFilterContent({
|
||||
setOpen,
|
||||
updateCameraFilter,
|
||||
}: CamerasFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
{isMobile && (
|
||||
<>
|
||||
<DropdownMenuLabel className="flex justify-center">
|
||||
<Trans ns="components/filter">cameras.all.short</Trans>
|
||||
{t("cameras.all.short")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
@ -153,7 +154,7 @@ export function CamerasFilterContent({
|
||||
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
|
||||
<FilterSwitch
|
||||
isChecked={currentCameras == undefined}
|
||||
label={t("cameras.all", { ns: "components/filter" })}
|
||||
label={t("cameras.all")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setCurrentCameras(undefined);
|
||||
@ -235,7 +236,7 @@ export function CamerasFilterContent({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trans>button.apply</Trans>
|
||||
{t("button.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
@ -244,7 +245,7 @@ export function CamerasFilterContent({
|
||||
updateCameraFilter(undefined);
|
||||
}}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -23,7 +23,7 @@ import { FilterList, GeneralFilter } from "@/types/filter";
|
||||
import CalendarFilterButton from "./CalendarFilterButton";
|
||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { t } from "i18next";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
@ -280,7 +280,7 @@ function ShowReviewFilter({
|
||||
}
|
||||
/>
|
||||
<Label className="ml-2 cursor-pointer text-primary" htmlFor="reviewed">
|
||||
<Trans ns="components/filter">review.showReviewed</Trans>
|
||||
{t("review.showReviewed", { ns: "components/filter" })}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@ -366,7 +366,7 @@ function GeneralFilterButton({
|
||||
: "text-primary"
|
||||
}`}
|
||||
>
|
||||
<Trans ns="components/filter">label</Trans>
|
||||
{t("label", { ns: "components/filter" })}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -474,7 +474,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
<Trans ns="components/filter">labels.all</Trans>
|
||||
{t("labels.all", { ns: "components/filter" })}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -521,7 +521,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allZones"
|
||||
>
|
||||
<Trans ns="components/filter">zones.all</Trans>
|
||||
{t("zones.all", { ns: "components/filter" })}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -577,10 +577,10 @@ export function GeneralFilterContent({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Trans>button.apply</Trans>
|
||||
{t("button.apply")}
|
||||
</Button>
|
||||
<Button aria-label="Reset" onClick={onReset}>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@ -613,7 +613,7 @@ function ShowMotionOnlyButton({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="collapse-motion"
|
||||
>
|
||||
<Trans ns="views/events">motion.only</Trans>
|
||||
{t("motion.only", { ns: "views/events" })}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
||||
@ -24,8 +24,8 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SearchFilterGroupProps = {
|
||||
className: string;
|
||||
@ -41,6 +41,7 @@ export default function SearchFilterGroup({
|
||||
filterList,
|
||||
onUpdateFilter,
|
||||
}: SearchFilterGroupProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
@ -197,11 +198,7 @@ export default function SearchFilterGroup({
|
||||
to: new Date(filter.before * 1000),
|
||||
}
|
||||
}
|
||||
defaultText={
|
||||
isMobile
|
||||
? t("dates.all.short", { ns: "components/filter" })
|
||||
: t("dates.all", { ns: "components/filter" })
|
||||
}
|
||||
defaultText={isMobile ? t("dates.all.short") : t("dates.all")}
|
||||
updateSelectedRange={onUpdateSelectedRange}
|
||||
/>
|
||||
)}
|
||||
@ -235,6 +232,7 @@ function GeneralFilterButton({
|
||||
selectedLabels,
|
||||
updateLabelFilter,
|
||||
}: GeneralFilterButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||
selectedLabels,
|
||||
@ -242,11 +240,11 @@ function GeneralFilterButton({
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
if (isMobile) {
|
||||
return t("labels.all.short", { ns: "components/filter" });
|
||||
return t("labels.all.short");
|
||||
}
|
||||
|
||||
if (!selectedLabels || selectedLabels.length == 0) {
|
||||
return t("labels.all", { ns: "components/filter" });
|
||||
return t("labels.all");
|
||||
}
|
||||
|
||||
if (selectedLabels.length == 1) {
|
||||
@ -257,7 +255,7 @@ function GeneralFilterButton({
|
||||
count: selectedLabels.length,
|
||||
ns: "components/filter",
|
||||
});
|
||||
}, [selectedLabels]);
|
||||
}, [selectedLabels, t]);
|
||||
|
||||
// ui
|
||||
|
||||
@ -332,6 +330,7 @@ export function GeneralFilterContent({
|
||||
setCurrentLabels,
|
||||
onClose,
|
||||
}: GeneralFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-hidden">
|
||||
@ -340,7 +339,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
<Trans ns="components/filter">labels.all</Trans>
|
||||
{t("labels.all")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -392,7 +391,7 @@ export function GeneralFilterContent({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Trans>button.apply</Trans>
|
||||
{t("button.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
@ -401,7 +400,7 @@ export function GeneralFilterContent({
|
||||
updateLabelFilter(undefined);
|
||||
}}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@ -420,6 +419,7 @@ function SortTypeButton({
|
||||
selectedSortType,
|
||||
updateSortType,
|
||||
}: SortTypeButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentSortType, setCurrentSortType] = useState<
|
||||
SearchSortType | undefined
|
||||
@ -450,7 +450,7 @@ function SortTypeButton({
|
||||
<div
|
||||
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
|
||||
>
|
||||
<Trans ns="components/filter">sort.label</Trans>
|
||||
{t("sort.label")}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -505,14 +505,15 @@ export function SortTypeContent({
|
||||
setCurrentSortType,
|
||||
onClose,
|
||||
}: SortTypeContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const sortLabels = {
|
||||
date_asc: t("sort.dateAsc", { ns: "components/filter" }),
|
||||
date_desc: t("sort.dateDesc", { ns: "components/filter" }),
|
||||
score_asc: t("sort.scoreAsc", { ns: "components/filter" }),
|
||||
score_desc: t("sort.scoreDesc", { ns: "components/filter" }),
|
||||
speed_asc: t("sort.speedAsc", { ns: "components/filter" }),
|
||||
speed_desc: t("sort.speedDesc", { ns: "components/filter" }),
|
||||
relevance: t("sort.relevance", { ns: "components/filter" }),
|
||||
date_asc: t("sort.dateAsc"),
|
||||
date_desc: t("sort.dateDesc"),
|
||||
score_asc: t("sort.scoreAsc"),
|
||||
score_desc: t("sort.scoreDesc"),
|
||||
speed_asc: t("sort.speedAsc"),
|
||||
speed_desc: t("sort.speedDesc"),
|
||||
relevance: t("sort.relevance"),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
@ -566,7 +567,7 @@ export function SortTypeContent({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Trans>button.apply</Trans>
|
||||
{t("button.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
@ -575,7 +576,7 @@ export function SortTypeContent({
|
||||
updateSortType(undefined);
|
||||
}}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -7,7 +7,7 @@ import { PolygonType } from "@/types/canvas";
|
||||
import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ZoneMaskFilterButtonProps = {
|
||||
selectedZoneMask?: PolygonType[];
|
||||
@ -17,6 +17,7 @@ export function ZoneMaskFilterButton({
|
||||
selectedZoneMask,
|
||||
updateZoneMaskFilter,
|
||||
}: ZoneMaskFilterButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const trigger = (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -30,7 +31,7 @@ export function ZoneMaskFilterButton({
|
||||
<div
|
||||
className={`hidden md:block ${selectedZoneMask?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||
>
|
||||
<Trans ns="components/filter">label</Trans>
|
||||
{t("label")}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -68,6 +69,7 @@ export function GeneralFilterContent({
|
||||
selectedZoneMask,
|
||||
updateZoneMaskFilter,
|
||||
}: GeneralFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
<div className="h-auto overflow-y-auto overflow-x-hidden">
|
||||
@ -76,7 +78,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
<Trans ns="components/filter">labels.all</Trans>
|
||||
{t("labels.all")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -97,11 +99,14 @@ export function GeneralFilterContent({
|
||||
className="mx-2 w-full cursor-pointer capitalize text-primary"
|
||||
htmlFor={item}
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.
|
||||
{item.replace(/_([a-z])/g, (letter) => letter.toUpperCase()) +
|
||||
"s"}
|
||||
</Trans>
|
||||
{t(
|
||||
"masksAndZones." +
|
||||
item.replace(/_([a-z])/g, (letter) =>
|
||||
letter.toUpperCase(),
|
||||
) +
|
||||
"s",
|
||||
{ ns: "views/settings" },
|
||||
)}
|
||||
</Label>
|
||||
<Switch
|
||||
key={item}
|
||||
|
||||
@ -16,9 +16,9 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { getUnitSize } from "@/utils/storageUtil";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { CiCircleAlert } from "react-icons/ci";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraStorage = {
|
||||
[key: string]: {
|
||||
@ -43,6 +43,8 @@ export function CombinedStorageGraph({
|
||||
cameraStorage,
|
||||
totalStorage,
|
||||
}: CombinedStorageGraphProps) {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
|
||||
const { theme, systemTheme } = useTheme();
|
||||
|
||||
const entities = Object.keys(cameraStorage);
|
||||
@ -178,22 +180,12 @@ export function CombinedStorageGraph({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("storage.cameraStorage.camera")}</TableHead>
|
||||
<TableHead>{t("storage.cameraStorage.storageUsed")}</TableHead>
|
||||
<TableHead>
|
||||
<Trans ns="views/system">storage.cameraStorage.camera</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans ns="views/system">
|
||||
storage.cameraStorage.storageUsed
|
||||
</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans ns="views/system">
|
||||
storage.cameraStorage.percentageOfTotalUsed
|
||||
</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans ns="views/system">storage.cameraStorage.bandwidth</Trans>
|
||||
{t("storage.cameraStorage.percentageOfTotalUsed")}
|
||||
</TableHead>
|
||||
<TableHead>{t("storage.cameraStorage.bandwidth")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -206,9 +198,7 @@ export function CombinedStorageGraph({
|
||||
style={{ backgroundColor: item.color }}
|
||||
></div>
|
||||
{item.name === "Unused"
|
||||
? t("storage.cameraStorage.unused", {
|
||||
ns: "views/system",
|
||||
})
|
||||
? t("storage.cameraStorage.unused")
|
||||
: item.name.replaceAll("_", " ")}
|
||||
{item.name === "Unused" && (
|
||||
<Popover>
|
||||
@ -225,9 +215,7 @@ export function CombinedStorageGraph({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-2">
|
||||
<Trans ns="views/system">
|
||||
storage.cameraStorage.unused.tips
|
||||
</Trans>
|
||||
{t("storage.cameraStorage.unused.tips")}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@ -11,8 +11,8 @@ import { IoClose } from "react-icons/io5";
|
||||
import Heading from "../ui/heading";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type IconName = keyof typeof LuIcons;
|
||||
|
||||
@ -32,6 +32,7 @@ export default function IconPicker({
|
||||
selectedIcon,
|
||||
setSelectedIcon,
|
||||
}: IconPickerProps) {
|
||||
const { t } = useTranslation(["components/icons"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
@ -72,7 +73,7 @@ export default function IconPicker({
|
||||
className="mt-2 w-full text-muted-foreground"
|
||||
aria-label="Select an icon"
|
||||
>
|
||||
<Trans ns="components/icons">iconPicker.selectIcon</Trans>
|
||||
{t("iconPicker.selectIcon")}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="hover:cursor-pointer">
|
||||
@ -103,9 +104,7 @@ export default function IconPicker({
|
||||
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
|
||||
>
|
||||
<div className="mb-3 flex flex-row items-center justify-between">
|
||||
<Heading as="h4">
|
||||
<Trans ns="components/icons">iconPicker.selectIcon</Trans>
|
||||
</Heading>
|
||||
<Heading as="h4">{t("iconPicker.selectIcon")}</Heading>
|
||||
<span tabIndex={0} className="sr-only" />
|
||||
<IoClose
|
||||
size={15}
|
||||
|
||||
@ -12,8 +12,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { useMemo, useState } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { toast } from "sonner";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SaveSearchDialogProps = {
|
||||
existingNames: string[];
|
||||
@ -28,6 +27,8 @@ export function SaveSearchDialog({
|
||||
onClose,
|
||||
onSave,
|
||||
}: SaveSearchDialogProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
|
||||
const [searchName, setSearchName] = useState("");
|
||||
|
||||
const handleSave = () => {
|
||||
@ -37,7 +38,6 @@ export function SaveSearchDialog({
|
||||
toast.success(
|
||||
t("search.saveSearch.success", {
|
||||
searchName: searchName.trim(),
|
||||
ns: "components/dialog",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
@ -62,31 +62,25 @@ export function SaveSearchDialog({
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans ns="components/dialog">search.saveSearch.label</Trans>
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("search.saveSearch.label")}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
<Trans ns="components/dialog">search.saveSearch.desc</Trans>
|
||||
{t("search.saveSearch.desc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={searchName}
|
||||
className="text-md"
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
placeholder={t("search.saveSearch.placeholder", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
placeholder={t("search.saveSearch.placeholder")}
|
||||
/>
|
||||
{overwrite && (
|
||||
<div className="ml-1 text-sm text-danger">
|
||||
<Trans ns="components/dialog" values={{ searchName }}>
|
||||
search.saveSearch.overwrite
|
||||
</Trans>
|
||||
{t("search.saveSearch.overwrite", { searchName })}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button aria-label="Cancel" onClick={onClose}>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
@ -94,7 +88,7 @@ export function SaveSearchDialog({
|
||||
className="mb-2 md:mb-0"
|
||||
aria-label="Save this search"
|
||||
>
|
||||
<Trans>button.save</Trans>
|
||||
{t("button.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -21,7 +21,7 @@ import { DialogClose } from "../ui/dialog";
|
||||
import { LuLogOut, LuSquarePen } from "react-icons/lu";
|
||||
import useSWR from "swr";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
@ -123,9 +123,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.user.logout</Trans>
|
||||
</span>
|
||||
<span>{t("menu.user.logout")}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
||||
@ -55,7 +55,7 @@ import { cn } from "@/lib/utils";
|
||||
import useSWR from "swr";
|
||||
import RestartDialog from "../overlay/dialog/RestartDialog";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useLanguage } from "@/context/language-provider";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import SetPasswordDialog from "../overlay/SetPasswordDialog";
|
||||
@ -133,9 +133,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
<Trans>menu.settings</Trans>
|
||||
</p>
|
||||
<p>{t("menu.settings")}</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
@ -196,9 +194,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
)}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<DropdownMenuLabel>
|
||||
<Trans>menu.system</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t("menu.system")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
|
||||
<Link to="/system#general">
|
||||
@ -211,9 +207,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="System metrics"
|
||||
>
|
||||
<LuActivity className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.systemMetrics</Trans>
|
||||
</span>
|
||||
<span>{t("menu.systemMetrics")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/logs">
|
||||
@ -226,9 +220,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="System logs"
|
||||
>
|
||||
<LuList className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.systemLogs</Trans>
|
||||
</span>
|
||||
<span>{t("menu.systemLogs")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
@ -237,7 +229,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
<DropdownMenuLabel
|
||||
className={isDesktop && isAdmin ? "mt-3" : "mt-1"}
|
||||
>
|
||||
<Trans>menu.configuration</Trans>
|
||||
{t("menu.configuration")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
@ -251,9 +243,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="Settings"
|
||||
>
|
||||
<LuSettings className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.settings</Trans>
|
||||
</span>
|
||||
<span>{t("menu.settings")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
@ -268,16 +258,14 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label="Configuration editor"
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.configurationEditor</Trans>
|
||||
</span>
|
||||
<span>{t("menu.configurationEditor")}</span>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
<Trans>menu.appearance</Trans>
|
||||
{t("menu.appearance")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<SubItem>
|
||||
@ -287,9 +275,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
}
|
||||
>
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.languages</Trans>
|
||||
</span>
|
||||
<span>{t("menu.languages")}</span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@ -310,12 +296,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{language.trim() === "en" ? (
|
||||
<>
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
<Trans>menu.language.en</Trans>
|
||||
{t("menu.language.en")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>menu.language.en</Trans>
|
||||
</span>
|
||||
<span className="ml-6 mr-2">{t("menu.language.en")}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
@ -330,11 +314,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{language === "zh-CN" ? (
|
||||
<>
|
||||
<LuLanguages className="mr-2 size-4" />
|
||||
<Trans>menu.language.zhCN</Trans>
|
||||
{t("menu.language.zhCN")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>menu.language.zhCN</Trans>
|
||||
{t("menu.language.zhCN")}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
@ -350,12 +334,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{language === systemLanguage ? (
|
||||
<>
|
||||
<LuEarth className="mr-2 size-4 scale-100 transition-all" />
|
||||
<Trans>menu.withSystem</Trans>
|
||||
{t("menu.withSystem")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>menu.withSystem</Trans>
|
||||
</span>
|
||||
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</SubItemContent>
|
||||
@ -368,9 +350,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.darkMode.label</Trans>
|
||||
</span>
|
||||
<span>{t("menu.darkMode.label")}</span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@ -391,11 +371,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{theme === "light" ? (
|
||||
<>
|
||||
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Trans>menu.darkMode.light</Trans>
|
||||
{t("menu.darkMode.light")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>menu.darkMode.light</Trans>
|
||||
{t("menu.darkMode.light")}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
@ -411,11 +391,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{theme === "dark" ? (
|
||||
<>
|
||||
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<Trans>menu.darkMode.dark</Trans>
|
||||
{t("menu.darkMode.dark")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>menu.darkMode.dark</Trans>
|
||||
{t("menu.darkMode.dark")}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
@ -431,12 +411,10 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{theme === "system" ? (
|
||||
<>
|
||||
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
|
||||
<Trans>menu.withSystem</Trans>
|
||||
{t("menu.withSystem")}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>menu.withSystem</Trans>
|
||||
</span>
|
||||
<span className="ml-6 mr-2">{t("menu.withSystem")}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</SubItemContent>
|
||||
@ -449,9 +427,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
}
|
||||
>
|
||||
<LuSunMoon className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.theme.label</Trans>
|
||||
</span>
|
||||
<span>{t("menu.theme.label")}</span>
|
||||
</SubItemTrigger>
|
||||
<Portal>
|
||||
<SubItemContent
|
||||
@ -474,11 +450,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
{scheme === colorScheme ? (
|
||||
<>
|
||||
<IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" />
|
||||
<Trans>{friendlyColorSchemeName(scheme)}</Trans>
|
||||
{t(friendlyColorSchemeName(scheme))}
|
||||
</>
|
||||
) : (
|
||||
<span className="ml-6 mr-2">
|
||||
<Trans>{friendlyColorSchemeName(scheme)}</Trans>
|
||||
{t(friendlyColorSchemeName(scheme))}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
@ -487,7 +463,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
</Portal>
|
||||
</SubItem>
|
||||
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
|
||||
<Trans>menu.help</Trans>
|
||||
{t("menu.help")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<a href="https://docs.frigate.video" target="_blank">
|
||||
@ -498,9 +474,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
aria-label={t("menu.documentation.label")}
|
||||
>
|
||||
<LuLifeBuoy className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.documentation</Trans>
|
||||
</span>
|
||||
<span>{t("menu.documentation")}</span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
<a
|
||||
@ -532,9 +506,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
>
|
||||
<LuRotateCw className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>menu.restart</Trans>
|
||||
</span>
|
||||
<span>{t("menu.restart")}</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -44,7 +44,7 @@ import {
|
||||
useNotifications,
|
||||
useNotificationSuspend,
|
||||
} from "@/api/ws";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { t } from "i18next";
|
||||
|
||||
type LiveContextMenuProps = {
|
||||
@ -362,9 +362,7 @@ export default function LiveContextMenu({
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={isEnabled ? resetPreferredLiveMode : undefined}
|
||||
>
|
||||
<div className="text-primary">
|
||||
<Trans>button</Trans>
|
||||
</div>
|
||||
<div className="text-primary">{t("button")}</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
|
||||
@ -39,8 +39,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SearchResultActionsProps = {
|
||||
searchResult: SearchResult;
|
||||
@ -61,6 +61,8 @@ export default function SearchResultActions({
|
||||
isContextMenu = false,
|
||||
children,
|
||||
}: SearchResultActionsProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@ -92,61 +94,45 @@ export default function SearchResultActions({
|
||||
const menuItems = (
|
||||
<>
|
||||
{searchResult.has_clip && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.downloadVideo.aria", { ns: "views/explore" })}
|
||||
>
|
||||
<MenuItem aria-label={t("itemMenu.downloadVideo.aria")}>
|
||||
<a
|
||||
className="flex items-center"
|
||||
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
||||
download={`${searchResult.camera}_${searchResult.label}.mp4`}
|
||||
>
|
||||
<LuDownload className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans ns="views/explore">itemMenu.downloadVideo</Trans>
|
||||
</span>
|
||||
<span>{t("itemMenu.downloadVideo")}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
{searchResult.has_snapshot && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.downloadSnapshot.aria", {
|
||||
ns: "views/explore",
|
||||
})}
|
||||
>
|
||||
<MenuItem aria-label={t("itemMenu.downloadSnapshot.aria")}>
|
||||
<a
|
||||
className="flex items-center"
|
||||
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
|
||||
download={`${searchResult.camera}_${searchResult.label}.jpg`}
|
||||
>
|
||||
<LuCamera className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans ns="views/explore">itemMenu.downloadSnapshot.label</Trans>
|
||||
</span>
|
||||
<span>{t("itemMenu.downloadSnapshot.label")}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
{searchResult.data.type == "object" && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.viewObjectLifecycle.aria", {
|
||||
ns: "views/explore",
|
||||
})}
|
||||
aria-label={t("itemMenu.viewObjectLifecycle.aria")}
|
||||
onClick={showObjectLifecycle}
|
||||
>
|
||||
<FaArrowsRotate className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans ns="views/explore">itemMenu.viewObjectLifecycle.label</Trans>
|
||||
</span>
|
||||
<span>{t("itemMenu.viewObjectLifecycle.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{config?.semantic_search?.enabled && isContextMenu && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.findSimilar.aria", { ns: "views/explore" })}
|
||||
aria-label={t("itemMenu.findSimilar.aria")}
|
||||
onClick={findSimilar}
|
||||
>
|
||||
<MdImageSearch className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans ns="views/explore">itemMenu.findSimilar.label</Trans>
|
||||
</span>
|
||||
<span>{t("itemMenu.findSimilar.label")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMobileOnly &&
|
||||
@ -156,15 +142,11 @@ export default function SearchResultActions({
|
||||
searchResult.data.type == "object" &&
|
||||
!searchResult.plus_id && (
|
||||
<MenuItem
|
||||
aria-label={t("itemMenu.submitToPlus.aria", {
|
||||
ns: "views/explore",
|
||||
})}
|
||||
aria-label={t("itemMenu.submitToPlus.aria")}
|
||||
onClick={showSnapshot}
|
||||
>
|
||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||
<span>
|
||||
<Trans ns="views/explore">itemMenu.submitToPlus</Trans>
|
||||
</span>
|
||||
<span>{t("itemMenu.submitToPlus")}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
@ -172,9 +154,7 @@ export default function SearchResultActions({
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<LuTrash2 className="mr-2 size-4" />
|
||||
<span>
|
||||
<Trans>button.delete</Trans>
|
||||
</span>
|
||||
<span>{t("button.delete")}</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
@ -187,22 +167,18 @@ export default function SearchResultActions({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans ns="views/explore">dialog.confirmDelete</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("dialog.confirmDelete")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
<Trans ns="views/explore">dialog.confirmDelete.desc</Trans>
|
||||
{t("dialog.confirmDelete.desc")}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>button.cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trans>button.delete</Trans>
|
||||
{t("button.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -224,7 +200,7 @@ export default function SearchResultActions({
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans ns="views/explore">itemMenu.findSimilar.label</Trans>
|
||||
{t("itemMenu.findSimilar.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -243,7 +219,7 @@ export default function SearchResultActions({
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans ns="views/explore">itemMenu.submitToPlus.label</Trans>
|
||||
{t("itemMenu.submitToPlus.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -9,7 +9,7 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { NavData } from "@/types/navigation";
|
||||
import { IconType } from "react-icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
const variants = {
|
||||
primary: {
|
||||
@ -61,9 +61,7 @@ export default function NavItem({
|
||||
<TooltipTrigger>{content}</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
<Trans>{item.title}</Trans>
|
||||
</p>
|
||||
<p>{t("{item.title}")}</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
|
||||
@ -15,8 +15,7 @@ import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "../ui/sonner";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraInfoDialogProps = {
|
||||
camera: CameraConfig;
|
||||
@ -28,6 +27,7 @@ export default function CameraInfoDialog({
|
||||
showCameraInfoDialog,
|
||||
setShowCameraInfoDialog,
|
||||
}: CameraInfoDialogProps) {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const [ffprobeInfo, setFfprobeInfo] = useState<Ffprobe[]>();
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,12 +76,11 @@ export default function CameraInfoDialog({
|
||||
<DialogTitle className="capitalize">
|
||||
{t("cameras.info.cameraProbeInfo", {
|
||||
camera: camera.name.replaceAll("_", " "),
|
||||
ns: "views/system",
|
||||
})}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
<Trans ns="views/system">cameras.info.streamDataFromFFPROBE</Trans>
|
||||
{t("cameras.info.streamDataFromFFPROBE")}
|
||||
</DialogDescription>
|
||||
|
||||
<div className="mb-2 p-4">
|
||||
@ -92,7 +91,6 @@ export default function CameraInfoDialog({
|
||||
<div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary">
|
||||
{t("cameras.info.stream", {
|
||||
idx: idx + 1,
|
||||
ns: "views/system",
|
||||
})}
|
||||
</div>
|
||||
{stream.return_code == 0 ? (
|
||||
@ -102,15 +100,11 @@ export default function CameraInfoDialog({
|
||||
{codec.width ? (
|
||||
<div className="text-muted-foreground">
|
||||
<div className="ml-2">
|
||||
<Trans ns="views/system">
|
||||
cameras.info.video
|
||||
</Trans>
|
||||
{t("cameras.info.video")}
|
||||
</div>
|
||||
<div className="ml-5">
|
||||
<div>
|
||||
<Trans ns="views/system">
|
||||
cameras.info.codec
|
||||
</Trans>
|
||||
{t("cameras.info.codec")}
|
||||
<span className="text-primary">
|
||||
{" "}
|
||||
{codec.codec_long_name}
|
||||
@ -119,9 +113,7 @@ export default function CameraInfoDialog({
|
||||
<div>
|
||||
{codec.width && codec.height ? (
|
||||
<>
|
||||
<Trans ns="views/system">
|
||||
cameras.info.resolution
|
||||
</Trans>{" "}
|
||||
{t("cameras.info.resolution")}{" "}
|
||||
<span className="text-primary">
|
||||
{" "}
|
||||
{codec.width}x{codec.height} (
|
||||
@ -135,9 +127,7 @@ export default function CameraInfoDialog({
|
||||
</>
|
||||
) : (
|
||||
<span>
|
||||
<Trans ns="views/system">
|
||||
cameras.info.resolution
|
||||
</Trans>{" "}
|
||||
{t("cameras.info.resolution")}{" "}
|
||||
<span className="text-primary">
|
||||
Unknown
|
||||
</span>
|
||||
@ -145,14 +135,10 @@ export default function CameraInfoDialog({
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Trans ns="views/system">
|
||||
cameras.info.fps
|
||||
</Trans>{" "}
|
||||
{t("cameras.info.fps")}{" "}
|
||||
<span className="text-primary">
|
||||
{codec.avg_frame_rate == "0/0"
|
||||
? t("cameras.info.unknown", {
|
||||
ns: "views/system",
|
||||
})
|
||||
? t("cameras.info.unknown")
|
||||
: codec.avg_frame_rate}
|
||||
</span>
|
||||
</div>
|
||||
@ -162,9 +148,7 @@ export default function CameraInfoDialog({
|
||||
<div className="text-muted-foreground">
|
||||
<div className="ml-2 mt-1">Audio:</div>
|
||||
<div className="ml-4">
|
||||
<Trans ns="views/system">
|
||||
cameras.info.codec
|
||||
</Trans>{" "}
|
||||
{t("cameras.info.codec")}{" "}
|
||||
<span className="text-primary">
|
||||
{codec.codec_long_name}
|
||||
</span>
|
||||
@ -179,7 +163,6 @@ export default function CameraInfoDialog({
|
||||
<div>
|
||||
{t("cameras.info.error", {
|
||||
error: stream.stderr,
|
||||
ns: "views/system",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@ -190,9 +173,7 @@ export default function CameraInfoDialog({
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
<ActivityIndicator />
|
||||
<div className="mt-2">
|
||||
<Trans ns="views/system">cameras.info.fetching</Trans>
|
||||
</div>
|
||||
<div className="mt-2">{t("cameras.info.fetching")}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -203,7 +184,7 @@ export default function CameraInfoDialog({
|
||||
aria-label="Copy"
|
||||
onClick={() => onCopyFfprobe()}
|
||||
>
|
||||
<Trans>button.copy</Trans>
|
||||
{t("button.copy")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -22,8 +22,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -33,6 +32,7 @@ import {
|
||||
} from "../ui/select";
|
||||
import { Shield, User } from "lucide-react";
|
||||
import { LuCheck, LuX } from "react-icons/lu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CreateUserOverlayProps = {
|
||||
show: boolean;
|
||||
@ -45,31 +45,23 @@ export default function CreateUserDialog({
|
||||
onCreate,
|
||||
onCancel,
|
||||
}: CreateUserOverlayProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
user: z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
t("users.dialog.form.usernameIsRequired", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
)
|
||||
.min(1, t("users.dialog.form.usernameIsRequired"))
|
||||
.regex(/^[A-Za-z0-9._]+$/, {
|
||||
message: t("users.dialog.createUser.usernameOnlyInclude", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("users.dialog.createUser.usernameOnlyInclude"),
|
||||
}),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
confirmPassword: z.string().min(1, "Please confirm your password"),
|
||||
role: z.enum(["admin", "viewer"]),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: t("users.dialog.form.password.notMatch", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("users.dialog.form.password.notMatch"),
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
@ -120,11 +112,9 @@ export default function CreateUserDialog({
|
||||
<Dialog open={show} onOpenChange={onCancel}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans ns="views/settings">users.dialog.createUser.title</Trans>
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("users.dialog.createUser.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans ns="views/settings">users.dialog.createUser.desc</Trans>
|
||||
{t("users.dialog.createUser.desc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -138,21 +128,17 @@ export default function CreateUserDialog({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium">
|
||||
<Trans ns="views/settings">users.dialog.form.user</Trans>
|
||||
{t("users.dialog.form.user")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("users.dialog.form.user.placeholder", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
placeholder={t("users.dialog.form.user.placeholder")}
|
||||
className="h-10"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-xs text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.user.desc
|
||||
</Trans>
|
||||
{t("users.dialog.form.user.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -164,15 +150,11 @@ export default function CreateUserDialog({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password
|
||||
</Trans>
|
||||
{t("users.dialog.form.password")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("users.dialog.form.password.placeholder", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
placeholder={t("users.dialog.form.password.placeholder")}
|
||||
type="password"
|
||||
className="h-10"
|
||||
{...field}
|
||||
@ -188,15 +170,12 @@ export default function CreateUserDialog({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.confirm
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.confirm")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t(
|
||||
"users.dialog.form.password.confirm.placeholder",
|
||||
{ ns: "views/settings" },
|
||||
)}
|
||||
type="password"
|
||||
className="h-10"
|
||||
@ -209,18 +188,14 @@ export default function CreateUserDialog({
|
||||
<>
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
<span className="text-green-600">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.match
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.match")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
<span className="text-red-600">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.notMatch
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.notMatch")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@ -236,7 +211,7 @@ export default function CreateUserDialog({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium">
|
||||
<Trans>role.title</Trans>
|
||||
{t("role.title")}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
@ -254,9 +229,7 @@ export default function CreateUserDialog({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
<span>
|
||||
<Trans>role.admin</Trans>
|
||||
</span>
|
||||
<span>{t("role.admin")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
@ -265,15 +238,13 @@ export default function CreateUserDialog({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span>
|
||||
<Trans>role.viewer</Trans>
|
||||
</span>
|
||||
<span>{t("role.viewer")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription className="text-xs text-muted-foreground">
|
||||
<Trans>role.desc</Trans>
|
||||
{t("role.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -290,7 +261,7 @@ export default function CreateUserDialog({
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -302,12 +273,10 @@ export default function CreateUserDialog({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@ -20,23 +20,22 @@ export default function DeleteUserDialog({
|
||||
onDelete,
|
||||
onCancel,
|
||||
}: DeleteUserDialogProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
return (
|
||||
<Dialog open={show} onOpenChange={onCancel}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader className="flex flex-col items-center gap-2 sm:items-start">
|
||||
<div className="space-y-1 text-center sm:text-left">
|
||||
<Trans ns="views/settings">users.dialog.deleteUser.title</Trans>
|
||||
{t("users.dialog.deleteUser.title")}
|
||||
<DialogDescription>
|
||||
<Trans ns="views/settings">users.dialog.deleteUser.desc</Trans>
|
||||
{t("users.dialog.deleteUser.desc")}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="my-4 rounded-md border border-destructive/20 bg-destructive/5 p-4 text-center text-sm">
|
||||
<p className="font-medium text-destructive">
|
||||
<Trans ns="views/settings" values={{ username }}>
|
||||
users.dialog.deleteUser.warn
|
||||
</Trans>
|
||||
{t("users.dialog.deleteUser.warn", { username })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -49,7 +48,7 @@ export default function DeleteUserDialog({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@ -57,7 +56,7 @@ export default function DeleteUserDialog({
|
||||
className="flex flex-1"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trans>button.delete</Trans>
|
||||
{t("button.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -30,8 +30,7 @@ import { getUTCOffset } from "@/utils/dateUtil";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const EXPORT_OPTIONS = [
|
||||
"1",
|
||||
@ -66,30 +65,21 @@ export default function ExportDialog({
|
||||
setMode,
|
||||
setShowPreview,
|
||||
}: ExportDialogProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const onStartExport = useCallback(() => {
|
||||
if (!range) {
|
||||
toast.error(
|
||||
t("export.toast.error.noVaildTimeSelected", {
|
||||
ns: "components/dialog",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.error(t("export.toast.error.noVaildTimeSelected"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (range.before < range.after) {
|
||||
toast.error(
|
||||
t("export.toast.error.endTimeMustAfterStartTime", {
|
||||
ns: "components/dialog",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.error(t("export.toast.error.endTimeMustAfterStartTime"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -103,12 +93,9 @@ export default function ExportDialog({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
t("export.toast.success", { ns: "components/dialog" }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.success(t("export.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setName("");
|
||||
setRange(undefined);
|
||||
setMode("none");
|
||||
@ -122,12 +109,11 @@ export default function ExportDialog({
|
||||
toast.error(
|
||||
t("export.toast.error.failed", {
|
||||
error: errorMessage,
|
||||
ns: "components/dialog",
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
});
|
||||
}, [camera, name, range, setRange, setName, setMode]);
|
||||
}, [camera, name, range, setRange, setName, setMode, t]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setName("");
|
||||
@ -181,9 +167,7 @@ export default function ExportDialog({
|
||||
>
|
||||
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>menu.export</Trans>
|
||||
</div>
|
||||
<div className="text-primary">{t("menu.export")}</div>
|
||||
)}
|
||||
</Button>
|
||||
</Trigger>
|
||||
@ -233,6 +217,7 @@ export function ExportContent({
|
||||
setMode,
|
||||
onCancel,
|
||||
}: ExportContentProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
||||
|
||||
const onSelectTime = useCallback(
|
||||
@ -280,9 +265,7 @@ export function ExportContent({
|
||||
{isDesktop && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>menu.export</Trans>
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("menu.export")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SelectSeparator className="my-4 bg-secondary" />
|
||||
</>
|
||||
@ -306,11 +289,10 @@ export function ExportContent({
|
||||
<Label className="cursor-pointer capitalize" htmlFor={opt}>
|
||||
{isNaN(parseInt(opt))
|
||||
? opt == "timeline"
|
||||
? t("export.time.fromTimeline", { ns: "components/dialog" })
|
||||
: t("export.time." + opt, { ns: "components/dialog" })
|
||||
? t("export.time.fromTimeline")
|
||||
: t("export.time." + opt)
|
||||
: t("export.time.lastHour", {
|
||||
count: parseInt(opt),
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</Label>
|
||||
</div>
|
||||
@ -327,7 +309,7 @@ export function ExportContent({
|
||||
<Input
|
||||
className="text-md my-6"
|
||||
type="search"
|
||||
placeholder={t("export.name.placeholder", { ns: "components/dialog" })}
|
||||
placeholder={t("export.name.placeholder")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
@ -339,7 +321,7 @@ export function ExportContent({
|
||||
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</div>
|
||||
<Button
|
||||
className={isDesktop ? "" : "w-full"}
|
||||
@ -358,8 +340,8 @@ export function ExportContent({
|
||||
}}
|
||||
>
|
||||
{selectedOption == "timeline"
|
||||
? t("export.select", { ns: "components/dialog" })
|
||||
: t("export.export", { ns: "components/dialog" })}
|
||||
? t("export.select")
|
||||
: t("export.export")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
@ -376,6 +358,7 @@ function CustomTimeSelector({
|
||||
range,
|
||||
setRange,
|
||||
}: CustomTimeSelectorProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// times
|
||||
@ -596,6 +579,7 @@ export function ExportPreviewDialog({
|
||||
showPreview,
|
||||
setShowPreview,
|
||||
}: ExportPreviewDialogProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
@ -613,15 +597,9 @@ export function ExportPreviewDialog({
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans ns="components/dialog">
|
||||
export.fromTimeline.previewExport
|
||||
</Trans>
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("export.fromTimeline.previewExport")}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
<Trans ns="components/dialog">
|
||||
export.fromTimeline.previewExport
|
||||
</Trans>
|
||||
{t("export.fromTimeline.previewExport")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<GenericVideoPlayer source={source} />
|
||||
|
||||
@ -19,7 +19,7 @@ import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
import SaveExportOverlay from "./SaveExportOverlay";
|
||||
import { isIOS, isMobile } from "react-device-detect";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { t } from "i18next";
|
||||
|
||||
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
||||
@ -247,7 +247,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@ -33,6 +33,7 @@ export default function RoleChangeDialog({
|
||||
onSave,
|
||||
onCancel,
|
||||
}: RoleChangeDialogProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const [selectedRole, setSelectedRole] = useState<"admin" | "viewer">(
|
||||
currentRole,
|
||||
);
|
||||
@ -42,18 +43,16 @@ export default function RoleChangeDialog({
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
<Trans ns="views/settings">users.dialog.changeRole.title</Trans>
|
||||
{t("users.dialog.changeRole.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans ns="views/settings" values={{ username }}>
|
||||
users.dialog.changeRole.desc
|
||||
</Trans>
|
||||
{t("users.dialog.changeRole.desc", { username })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="mb-4 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">users.dialog.changeRole.roleInfo</Trans>
|
||||
{t("users.dialog.changeRole.roleInfo")}
|
||||
</div>
|
||||
|
||||
<Select
|
||||
@ -69,17 +68,13 @@ export default function RoleChangeDialog({
|
||||
<SelectItem value="admin" className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<LuShield className="size-4 text-primary" />
|
||||
<span>
|
||||
<Trans>role.admin</Trans>
|
||||
</span>
|
||||
<span>{t("role.admin")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="viewer" className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<LuUser className="size-4 text-primary" />
|
||||
<span>
|
||||
<Trans>role.viewer</Trans>
|
||||
</span>
|
||||
<span>{t("role.viewer")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@ -95,7 +90,7 @@ export default function RoleChangeDialog({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -104,7 +99,7 @@ export default function RoleChangeDialog({
|
||||
onClick={() => onSave(selectedRole)}
|
||||
disabled={selectedRole === currentRole}
|
||||
>
|
||||
<Trans>button.save</Trans>
|
||||
{t("button.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@ import { LuVideo, LuX } from "react-icons/lu";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaCompactDisc } from "react-icons/fa";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SaveExportOverlayProps = {
|
||||
className: string;
|
||||
@ -18,6 +18,7 @@ export default function SaveExportOverlay({
|
||||
onSave,
|
||||
onCancel,
|
||||
}: SaveExportOverlayProps) {
|
||||
const { t } = useTranslation("components/dialog");
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
@ -34,7 +35,7 @@ export default function SaveExportOverlay({
|
||||
onClick={onCancel}
|
||||
>
|
||||
<LuX />
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
@ -43,9 +44,7 @@ export default function SaveExportOverlay({
|
||||
onClick={onPreview}
|
||||
>
|
||||
<LuVideo />
|
||||
<Trans ns="components/dialog">
|
||||
export.fromTimeline.previewExport
|
||||
</Trans>
|
||||
{t("export.fromTimeline.previewExport")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
@ -55,7 +54,7 @@ export default function SaveExportOverlay({
|
||||
onClick={onSave}
|
||||
>
|
||||
<FaCompactDisc />
|
||||
<Trans ns="components/dialog">export.fromTimeline.saveExport</Trans>
|
||||
{t("export.fromTimeline.saveExport", { ns: "components/dialog" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -11,10 +11,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { Label } from "../ui/label";
|
||||
import { LuCheck, LuX } from "react-icons/lu";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SetPasswordProps = {
|
||||
show: boolean;
|
||||
@ -29,6 +29,7 @@ export default function SetPasswordDialog({
|
||||
onCancel,
|
||||
username,
|
||||
}: SetPasswordProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||
const [passwordStrength, setPasswordStrength] = useState<number>(0);
|
||||
@ -80,20 +81,12 @@ export default function SetPasswordDialog({
|
||||
const getStrengthLabel = () => {
|
||||
if (!password) return "";
|
||||
if (passwordStrength <= 1)
|
||||
return t("users.dialog.form.password.strength.weak", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
return t("users.dialog.form.password.strength.weak");
|
||||
if (passwordStrength === 2)
|
||||
return t("users.dialog.form.password.strength.medium", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
return t("users.dialog.form.password.strength.medium");
|
||||
if (passwordStrength === 3)
|
||||
return t("users.dialog.form.password.strength.strong", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
return t("users.dialog.form.password.strength.veryStrong", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
return t("users.dialog.form.password.strength.strong");
|
||||
return t("users.dialog.form.password.strength.veryStrong");
|
||||
};
|
||||
|
||||
const getStrengthColor = () => {
|
||||
@ -114,19 +107,17 @@ export default function SetPasswordDialog({
|
||||
username,
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("users.dialog.passwordSetting.setPassword", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
: t("users.dialog.passwordSetting.setPassword")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans ns="views/settings">users.dialog.passwordSetting.desc</Trans>
|
||||
{t("users.dialog.passwordSetting.desc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">
|
||||
<Trans ns="views/settings">users.dialog.form.newPassword</Trans>
|
||||
{t("users.dialog.form.newPassword")}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
@ -137,9 +128,7 @@ export default function SetPasswordDialog({
|
||||
setPassword(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder={t("users.dialog.form.newPassword.placeholder", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
placeholder={t("users.dialog.form.newPassword.placeholder")}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -153,9 +142,7 @@ export default function SetPasswordDialog({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.strength
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.strength")}
|
||||
<span className="font-medium">{getStrengthLabel()}</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -164,9 +151,7 @@ export default function SetPasswordDialog({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.confirm
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.confirm")}
|
||||
</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
@ -179,7 +164,6 @@ export default function SetPasswordDialog({
|
||||
}}
|
||||
placeholder={t(
|
||||
"users.dialog.form.newPassword.confirm.placeholder",
|
||||
{ ns: "views/settings" },
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -190,18 +174,14 @@ export default function SetPasswordDialog({
|
||||
<>
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
<span className="text-green-600">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.match
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.match")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
<span className="text-red-600">
|
||||
<Trans ns="views/settings">
|
||||
users.dialog.form.password.notMatch
|
||||
</Trans>
|
||||
{t("users.dialog.form.password.notMatch")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@ -225,7 +205,7 @@ export default function SetPasswordDialog({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -234,7 +214,7 @@ export default function SetPasswordDialog({
|
||||
onClick={handleSave}
|
||||
disabled={!password || password !== confirmPassword}
|
||||
>
|
||||
<Trans>button.save</Trans>
|
||||
{t("button.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -73,8 +73,7 @@ import { LuInfo } from "react-icons/lu";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const SEARCH_TABS = [
|
||||
"details",
|
||||
@ -100,6 +99,7 @@ export default function SearchDetailDialog({
|
||||
setSimilarity,
|
||||
setInputFocused,
|
||||
}: SearchDetailDialogProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
@ -194,12 +194,8 @@ export default function SearchDetailDialog({
|
||||
)}
|
||||
>
|
||||
<Header>
|
||||
<Title>
|
||||
<Trans ns="views/explore">trackedObjectDetails</Trans>
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
<Trans ns="views/explore">details</Trans>
|
||||
</Description>
|
||||
<Title>{t("trackedObjectDetails")}</Title>
|
||||
<Description className="sr-only">{t("details")}</Description>
|
||||
</Header>
|
||||
<ScrollArea
|
||||
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
||||
@ -230,9 +226,7 @@ export default function SearchDetailDialog({
|
||||
{item == "object_lifecycle" && (
|
||||
<FaRotate className="size-4" />
|
||||
)}
|
||||
<div className="capitalize">
|
||||
<Trans ns="views/explore">type.{item}</Trans>
|
||||
</div>
|
||||
<div className="capitalize">{t("type.{item}")}</div>
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
@ -289,6 +283,8 @@ function ObjectDetailsTab({
|
||||
setSimilarity,
|
||||
setInputFocused,
|
||||
}: ObjectDetailsTabProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
|
||||
const apiHost = useApiHost();
|
||||
|
||||
// mutation / revalidation
|
||||
@ -374,12 +370,9 @@ function ObjectDetailsTab({
|
||||
.post(`events/${search.id}/description`, { description: desc })
|
||||
.then((resp) => {
|
||||
if (resp.status == 200) {
|
||||
toast.success(
|
||||
t("details.tips.descriptionSaved", { ns: "views/explore" }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.success(t("details.tips.descriptionSaved"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
mutate(
|
||||
(key) =>
|
||||
@ -412,7 +405,6 @@ function ObjectDetailsTab({
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("details.tips.saveDescriptionFailed", {
|
||||
ns: "views/explore",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
@ -421,7 +413,7 @@ function ObjectDetailsTab({
|
||||
);
|
||||
setDesc(search.data.description);
|
||||
});
|
||||
}, [desc, search, mutate]);
|
||||
}, [desc, search, mutate, t]);
|
||||
|
||||
const regenerateDescription = useCallback(
|
||||
(source: "snapshot" | "thumbnails") => {
|
||||
@ -533,12 +525,10 @@ function ObjectDetailsTab({
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">
|
||||
<Trans ns="views/explore">details.label</Trans>
|
||||
</div>
|
||||
<div className="text-sm text-primary/40">{t("details.label")}</div>
|
||||
<div className="flex flex-row items-center gap-2 text-sm capitalize">
|
||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||
<Trans ns="objects">{search.label}</Trans>
|
||||
{t("{search.label}", { ns: "objects" })}
|
||||
{search.sub_label && ` (${search.sub_label})`}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@ -552,9 +542,7 @@ function ObjectDetailsTab({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
<Trans ns="views/explore">details.editSubLable</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("details.editSubLable")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -562,7 +550,7 @@ function ObjectDetailsTab({
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<Trans ns="views/explore">details.topScore</Trans>
|
||||
{t("details.topScore")}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
@ -571,7 +559,7 @@ function ObjectDetailsTab({
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<Trans ns="views/explore">details.topScore.info</Trans>
|
||||
{t("details.topScore.info")}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
@ -583,7 +571,7 @@ function ObjectDetailsTab({
|
||||
{averageEstimatedSpeed && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">
|
||||
<Trans ns="views/explore">details.estimatedSpeed</Trans>
|
||||
{t("details.estimatedSpeed")}
|
||||
</div>
|
||||
<div className="flex flex-col space-y-0.5 text-sm">
|
||||
{averageEstimatedSpeed && (
|
||||
@ -608,16 +596,14 @@ function ObjectDetailsTab({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">
|
||||
<Trans ns="views/explore">details.camera</Trans>
|
||||
</div>
|
||||
<div className="text-sm text-primary/40">{t("details.camera")}</div>
|
||||
<div className="text-sm capitalize">
|
||||
{search.camera.replaceAll("_", " ")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">
|
||||
<Trans ns="views/explore">details.timestamp</Trans>
|
||||
{t("details.timestamp")}
|
||||
</div>
|
||||
<div className="text-sm">{formattedDate}</div>
|
||||
</div>
|
||||
@ -669,9 +655,7 @@ function ObjectDetailsTab({
|
||||
<div className="flex">
|
||||
<ActivityIndicator />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Trans ns="views/explore">details.description.aiTips</Trans>
|
||||
</div>
|
||||
<div className="flex">{t("details.description.aiTips")}</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@ -679,9 +663,7 @@ function ObjectDetailsTab({
|
||||
<div className="text-sm text-primary/40"></div>
|
||||
<Textarea
|
||||
className="h-64"
|
||||
placeholder={t("details.description.placeholder", {
|
||||
ns: "views/explore",
|
||||
})}
|
||||
placeholder={t("details.description.placeholder")}
|
||||
value={desc}
|
||||
onChange={(e) => setDesc(e.target.value)}
|
||||
onFocus={handleDescriptionFocus}
|
||||
@ -698,7 +680,7 @@ function ObjectDetailsTab({
|
||||
aria-label="Regenerate tracked object description"
|
||||
onClick={() => regenerateDescription("thumbnails")}
|
||||
>
|
||||
<Trans ns="views/explore">details.button.regenerate</Trans>
|
||||
{t("details.button.regenerate")}
|
||||
</Button>
|
||||
{search.has_snapshot && (
|
||||
<DropdownMenu>
|
||||
@ -716,18 +698,14 @@ function ObjectDetailsTab({
|
||||
aria-label="Regenerate from snapshot"
|
||||
onClick={() => regenerateDescription("snapshot")}
|
||||
>
|
||||
<Trans ns="views/explore">
|
||||
details.regenerateFromSnapshot
|
||||
</Trans>
|
||||
{t("details.regenerateFromSnapshot")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
aria-label="Regenerate from thumbnails"
|
||||
onClick={() => regenerateDescription("thumbnails")}
|
||||
>
|
||||
<Trans ns="views/explore">
|
||||
details.regenerateFromThumbnails
|
||||
</Trans>
|
||||
{t("details.regenerateFromThumbnails")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -741,22 +719,19 @@ function ObjectDetailsTab({
|
||||
aria-label="Save"
|
||||
onClick={updateDescription}
|
||||
>
|
||||
<Trans>button.save</Trans>
|
||||
{t("button.save")}
|
||||
</Button>
|
||||
)}
|
||||
<TextEntryDialog
|
||||
open={isSubLabelDialogOpen}
|
||||
setOpen={setIsSubLabelDialogOpen}
|
||||
title={t("details.editSubLable", { ns: "views/explore" })}
|
||||
title={t("details.editSubLable")}
|
||||
description={
|
||||
search.label
|
||||
? t("details.editSubLable.desc", {
|
||||
label: t(search.label, { ns: "objects" }),
|
||||
ns: "views/explore",
|
||||
})
|
||||
: t("details.editSubLable.desc.noLabel", {
|
||||
ns: "views/explore",
|
||||
})
|
||||
: t("details.editSubLable.desc.noLabel")
|
||||
}
|
||||
onSave={handleSubLabelSave}
|
||||
defaultValue={search?.sub_label || ""}
|
||||
@ -776,6 +751,7 @@ export function ObjectSnapshotTab({
|
||||
search,
|
||||
onEventUploaded,
|
||||
}: ObjectSnapshotTabProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
type SubmissionState = "reviewing" | "uploading" | "submitted";
|
||||
|
||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||
@ -858,9 +834,7 @@ export function ObjectSnapshotTab({
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
<Trans>button.download</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("button.download")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@ -18,8 +18,8 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type RestartDialogProps = {
|
||||
isOpen: boolean;
|
||||
@ -32,6 +32,7 @@ export default function RestartDialog({
|
||||
onClose,
|
||||
onRestart,
|
||||
}: RestartDialogProps) {
|
||||
const { t } = useTranslation("components/dialog");
|
||||
const [restartDialogOpen, setRestartDialogOpen] = useState(isOpen);
|
||||
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
@ -80,16 +81,12 @@ export default function RestartDialog({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans ns="components/dialog">restart.title</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("restart.title")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>button.cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleRestart}>
|
||||
<Trans ns="components/dialog">restart.button</Trans>
|
||||
{t("restart.button")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -104,13 +101,12 @@ export default function RestartDialog({
|
||||
<ActivityIndicator />
|
||||
<SheetHeader className="mt-5 text-center">
|
||||
<SheetTitle className="text-center">
|
||||
<Trans ns="components/dialog">restart.restarting.title</Trans>
|
||||
{t("restart.restarting.title")}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="text-center">
|
||||
<div>
|
||||
{t("restart.restarting.content", {
|
||||
countdown,
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</div>
|
||||
</SheetDescription>
|
||||
@ -121,7 +117,7 @@ export default function RestartDialog({
|
||||
aria-label="Force reload now"
|
||||
onClick={handleForceReload}
|
||||
>
|
||||
<Trans ns="components/dialog">restart.restarting.button</Trans>
|
||||
{t("restart.restarting.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@ -33,8 +33,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SearchFilterDialogProps = {
|
||||
config?: FrigateConfig;
|
||||
@ -54,7 +53,7 @@ export default function SearchFilterDialog({
|
||||
onUpdateFilter,
|
||||
}: SearchFilterDialogProps) {
|
||||
// data
|
||||
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [currentFilter, setCurrentFilter] = useState(filter ?? {});
|
||||
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
|
||||
|
||||
@ -95,7 +94,7 @@ export default function SearchFilterDialog({
|
||||
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
|
||||
)}
|
||||
/>
|
||||
<Trans ns="components/filter">more</Trans>
|
||||
{t("more")}
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
@ -177,7 +176,7 @@ export default function SearchFilterDialog({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trans>button.apply</Trans>
|
||||
{t("button.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset filters to default values"
|
||||
@ -197,7 +196,7 @@ export default function SearchFilterDialog({
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -233,6 +232,7 @@ function TimeRangeFilterContent({
|
||||
timeRange,
|
||||
updateTimeRange,
|
||||
}: TimeRangeFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [startOpen, setStartOpen] = useState(false);
|
||||
const [endOpen, setEndOpen] = useState(false);
|
||||
|
||||
@ -274,9 +274,7 @@ function TimeRangeFilterContent({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<div className="text-lg">
|
||||
<Trans ns="components/filter">timeRange</Trans>
|
||||
</div>
|
||||
<div className="text-lg">{t("timeRange")}</div>
|
||||
<div className="mt-3 flex flex-row items-center justify-center gap-2">
|
||||
<Popover
|
||||
open={startOpen}
|
||||
@ -370,13 +368,12 @@ export function ZoneFilterContent({
|
||||
zones,
|
||||
updateZones,
|
||||
}: ZoneFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="text-lg">
|
||||
<Trans ns="components/filter">zones.label</Trans>
|
||||
</div>
|
||||
<div className="text-lg">{t("zones.label")}</div>
|
||||
{allZones && (
|
||||
<>
|
||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||
@ -384,7 +381,7 @@ export function ZoneFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allZones"
|
||||
>
|
||||
<Trans ns="components/filter">zones.all</Trans>
|
||||
{t("zones.all")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -439,15 +436,14 @@ export function SubFilterContent({
|
||||
subLabels,
|
||||
setSubLabels,
|
||||
}: SubFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="text-lg">
|
||||
<Trans ns="components/filter">subLabels.label</Trans>
|
||||
</div>
|
||||
<div className="text-lg">{t("subLabels.label")}</div>
|
||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||
<Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels">
|
||||
<Trans ns="components/filter">subLabels.all</Trans>
|
||||
{t("subLabels.all")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -499,12 +495,11 @@ export function ScoreFilterContent({
|
||||
maxScore,
|
||||
setScoreRange,
|
||||
}: ScoreFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="mb-3 text-lg">
|
||||
<Trans ns="components/filter">score</Trans>
|
||||
</div>
|
||||
<div className="mb-3 text-lg">{t("score")}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
className="w-14 text-center"
|
||||
@ -555,21 +550,17 @@ export function SpeedFilterContent({
|
||||
maxSpeed,
|
||||
setSpeedRange,
|
||||
}: SpeedFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="mb-3 text-lg">
|
||||
<Trans
|
||||
values={{
|
||||
unit:
|
||||
config?.ui.unit_system == "metric"
|
||||
? t("unit.speed.kph")
|
||||
: t("unit.speed.mph"),
|
||||
}}
|
||||
ns="components/filter"
|
||||
>
|
||||
filter.estimatedSpeed
|
||||
</Trans>
|
||||
{t("filter.estimatedSpeed", {
|
||||
unit:
|
||||
config?.ui.unit_system == "metric"
|
||||
? t("unit.speed.kph")
|
||||
: t("unit.speed.mph"),
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
@ -628,6 +619,7 @@ export function SnapshotClipFilterContent({
|
||||
submittedToFrigatePlus,
|
||||
setSnapshotClip,
|
||||
}: SnapshotClipContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [isSnapshotFilterActive, setIsSnapshotFilterActive] = useState(
|
||||
hasSnapshot !== undefined,
|
||||
);
|
||||
@ -656,9 +648,7 @@ export function SnapshotClipFilterContent({
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="mb-3 text-lg">
|
||||
<Trans ns="components/filter">features.label</Trans>
|
||||
</div>
|
||||
<div className="mb-3 text-lg">{t("features.label")}</div>
|
||||
|
||||
<div className="my-2.5 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -680,7 +670,7 @@ export function SnapshotClipFilterContent({
|
||||
htmlFor="snapshot-filter"
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
>
|
||||
<Trans ns="components/filter">features.hasSnapshot</Trans>
|
||||
{t("features.hasSnapshot")}
|
||||
</Label>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
@ -701,14 +691,14 @@ export function SnapshotClipFilterContent({
|
||||
aria-label="Yes"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
<Trans>button.yes</Trans>
|
||||
{t("button.yes")}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
<Trans>button.no</Trans>
|
||||
{t("button.no")}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
@ -742,9 +732,7 @@ export function SnapshotClipFilterContent({
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
>
|
||||
<Trans ns="components/filter">
|
||||
features.submittedToFrigatePlus.tips
|
||||
</Trans>
|
||||
{t("features.submittedToFrigatePlus.tips")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
@ -753,9 +741,7 @@ export function SnapshotClipFilterContent({
|
||||
htmlFor="plus-filter"
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
>
|
||||
<Trans ns="components/filter">
|
||||
features.submittedToFrigatePlus.label
|
||||
</Trans>
|
||||
{t("features.submittedToFrigatePlus.label")}
|
||||
</Label>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
@ -781,14 +767,14 @@ export function SnapshotClipFilterContent({
|
||||
aria-label="Yes"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
<Trans>button.yes</Trans>
|
||||
{t("button.yes")}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
<Trans>button.no</Trans>
|
||||
{t("button.no")}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
@ -817,7 +803,7 @@ export function SnapshotClipFilterContent({
|
||||
htmlFor="clip-filter"
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
>
|
||||
<Trans ns="components/filter">features.hasVideoClip</Trans>
|
||||
{t("features.hasVideoClip")}
|
||||
</Label>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
@ -836,14 +822,14 @@ export function SnapshotClipFilterContent({
|
||||
aria-label="Yes"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
<Trans>button.yes</Trans>
|
||||
{t("button.yes")}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
<Trans>button.no</Trans>
|
||||
{t("button.no")}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
@ -12,7 +12,8 @@ import { Input } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
type TextEntryDialogProps = {
|
||||
@ -38,6 +39,8 @@ export default function TextEntryDialog({
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
const { t } = useTranslation("components/dialog");
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { text: defaultValue },
|
||||
@ -88,10 +91,10 @@ export default function TextEntryDialog({
|
||||
/>
|
||||
<DialogFooter className="pt-4">
|
||||
<Button type="button" onClick={() => setOpen(false)}>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button variant="select" type="submit">
|
||||
<Trans>button.save</Trans>
|
||||
{t("button.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
getPreviewForTimeRange,
|
||||
usePreviewForTimeRange,
|
||||
} from "@/hooks/use-camera-previews";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PreviewPlayerProps = {
|
||||
className?: string;
|
||||
@ -43,6 +43,7 @@ export default function PreviewPlayer({
|
||||
onControllerReady,
|
||||
onClick,
|
||||
}: PreviewPlayerProps) {
|
||||
const { t } = useTranslation(["components/player"]);
|
||||
const [currentHourFrame, setCurrentHourFrame] = useState<string>();
|
||||
const currentPreview = usePreviewForTimeRange(
|
||||
cameraPreviews,
|
||||
@ -89,7 +90,7 @@ export default function PreviewPlayer({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Trans ns="components/player">noPreviewFound</Trans>
|
||||
{t("noPreviewFound")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -134,6 +135,7 @@ function PreviewVideoPlayer({
|
||||
onClick,
|
||||
setCurrentHourFrame,
|
||||
}: PreviewVideoPlayerProps) {
|
||||
const { t } = useTranslation(["components/player"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// controlling playback
|
||||
@ -325,12 +327,7 @@ function PreviewVideoPlayer({
|
||||
</video>
|
||||
{cameraPreviews && !currentPreview && (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
|
||||
<Trans
|
||||
ns="components/player"
|
||||
value={{ camera: camera.replaceAll("_", " ") }}
|
||||
>
|
||||
noPreviewFoundFor
|
||||
</Trans>
|
||||
{t("noPreviewFoundFor", { camera: camera.replaceAll("_", " ") })}
|
||||
</div>
|
||||
)}
|
||||
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
||||
@ -450,6 +447,8 @@ function PreviewFramesPlayer({
|
||||
onControllerReady,
|
||||
onClick,
|
||||
}: PreviewFramesPlayerProps) {
|
||||
const { t } = useTranslation(["components/player"]);
|
||||
|
||||
// frames data
|
||||
|
||||
const { data: previewFrames } = useSWR<string[]>(
|
||||
@ -550,12 +549,7 @@ function PreviewFramesPlayer({
|
||||
/>
|
||||
{previewFrames?.length === 0 && (
|
||||
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
|
||||
<Trans
|
||||
ns="components/player"
|
||||
values={{ cameraName: camera.replaceAll("_", " ") }}
|
||||
>
|
||||
noPreviewFoundFor
|
||||
</Trans>
|
||||
{t("noPreviewFoundFor", { cameraName: camera.replaceAll("_", " ") })}
|
||||
</div>
|
||||
)}
|
||||
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
||||
|
||||
@ -12,7 +12,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { VideoResolutionType } from "@/types/live";
|
||||
import axios from "axios";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Dynamically switches between video playback and scrubbing preview player.
|
||||
@ -51,6 +51,7 @@ export default function DynamicVideoPlayer({
|
||||
toggleFullscreen,
|
||||
containerRef,
|
||||
}: DynamicVideoPlayerProps) {
|
||||
const { t } = useTranslation(["components/player"]);
|
||||
const apiHost = useApiHost();
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -248,7 +249,7 @@ export default function DynamicVideoPlayer({
|
||||
)}
|
||||
{!isScrubbing && !isLoading && noRecording && (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Trans ns="components/player">noRecordingsFoundForThisTime</Trans>
|
||||
{t("noRecordingsFoundForThisTime")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -31,7 +31,7 @@ import useSWR from "swr";
|
||||
import { LuCheck, LuExternalLink, LuInfo, LuX } from "react-icons/lu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LiveStreamMetadata } from "@/types/live";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraStreamingDialogProps = {
|
||||
camera: string;
|
||||
@ -50,6 +50,7 @@ export function CameraStreamingDialog({
|
||||
setIsDialogOpen,
|
||||
onSave,
|
||||
}: CameraStreamingDialogProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -168,16 +169,11 @@ export function CameraStreamingDialog({
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle className="capitalize">
|
||||
<Trans
|
||||
ns="components/camera"
|
||||
values={{ cameraName: camera.replaceAll("_", " ") }}
|
||||
>
|
||||
group.camera.setting.title
|
||||
</Trans>
|
||||
{t("group.camera.setting.title", {
|
||||
cameraName: camera.replaceAll("_", " "),
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans ns="components/camera">group.camera.setting.desc</Trans>
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t("group.camera.setting.desc")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col space-y-8">
|
||||
{!isRestreamed && (
|
||||
@ -186,23 +182,19 @@ export function CameraStreamingDialog({
|
||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.disabled
|
||||
</Trans>
|
||||
{t("streaming.restreaming.disabled", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">
|
||||
<Trans>button.info</Trans>
|
||||
</span>
|
||||
<span className="sr-only">{t("button.info")}</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.desc
|
||||
</Trans>
|
||||
{t("streaming.restreaming.desc", { ns: "components/dialog" })}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@ -210,9 +202,9 @@ export function CameraStreamingDialog({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.readTheDocumentation
|
||||
</Trans>
|
||||
{t("streaming.restreaming.readTheDocumentation", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -245,33 +237,21 @@ export function CameraStreamingDialog({
|
||||
{supportsAudioOutput ? (
|
||||
<>
|
||||
<LuCheck className="size-4 text-success" />
|
||||
<div>
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.audioIsAvailable
|
||||
</Trans>
|
||||
</div>
|
||||
<div>{t("group.camera.setting.audioIsAvailable")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.audioIsUnavailable
|
||||
</Trans>
|
||||
</div>
|
||||
<div>{t("group.camera.setting.audioIsUnavailable")}</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">
|
||||
<Trans>button.info</Trans>
|
||||
</span>
|
||||
<span className="sr-only">{t("button.info")}</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.audio.desc
|
||||
</Trans>
|
||||
{t("group.camera.setting.audio.desc")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@ -279,9 +259,7 @@ export function CameraStreamingDialog({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.audio.desc.document
|
||||
</Trans>
|
||||
{t("group.camera.setting.audio.desc.document")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -295,9 +273,7 @@ export function CameraStreamingDialog({
|
||||
)}
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Label htmlFor="streaming-method" className="text-right">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod
|
||||
</Trans>
|
||||
{t("group.camera.setting.streamMethod")}
|
||||
</Label>
|
||||
<Select
|
||||
value={streamType}
|
||||
@ -308,49 +284,43 @@ export function CameraStreamingDialog({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="no-streaming">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.noStreaming
|
||||
</Trans>
|
||||
{t("group.camera.setting.streamMethod.method.noStreaming")}
|
||||
</SelectItem>
|
||||
<SelectItem value="smart">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.smartStreaming
|
||||
</Trans>
|
||||
{t("group.camera.setting.streamMethod.method.smartStreaming")}
|
||||
</SelectItem>
|
||||
<SelectItem value="continuous">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.continuousStreaming
|
||||
</Trans>
|
||||
{t(
|
||||
"group.camera.setting.streamMethod.method.continuousStreaming",
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{streamType === "no-streaming" && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.noStreaming.desc
|
||||
</Trans>
|
||||
{t("group.camera.setting.streamMethod.method.noStreaming.desc")}
|
||||
</p>
|
||||
)}
|
||||
{streamType === "smart" && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.smartStreaming.desc
|
||||
</Trans>
|
||||
{t(
|
||||
"group.camera.setting.streamMethod.method.smartStreaming.desc",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{streamType === "continuous" && (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.continuousStreaming.desc
|
||||
</Trans>
|
||||
{t(
|
||||
"group.camera.setting.streamMethod.method.continuousStreaming.desc",
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<IoIosWarning className="mr-2 size-5 text-danger" />
|
||||
<div className="max-w-[85%] text-sm">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.streamMethod.method.continuousStreaming.desc.warning
|
||||
</Trans>
|
||||
{t(
|
||||
"group.camera.setting.streamMethod.method.continuousStreaming.desc.warning",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -368,16 +338,12 @@ export function CameraStreamingDialog({
|
||||
htmlFor="compatibility"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.compatibilityMode
|
||||
</Trans>
|
||||
{t("group.camera.setting.compatibilityMode")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 leading-none">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans ns="components/camera">
|
||||
group.camera.setting.compatibilityMode.desc
|
||||
</Trans>
|
||||
{t("group.camera.setting.compatibilityMode.desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -389,7 +355,7 @@ export function CameraStreamingDialog({
|
||||
aria-label="Cancel"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -401,12 +367,10 @@ export function CameraStreamingDialog({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -22,8 +22,7 @@ import { Toaster } from "../ui/sonner";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MotionMaskEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
@ -52,6 +51,7 @@ export default function MotionMaskEditPane({
|
||||
snapPoints,
|
||||
setSnapPoints,
|
||||
}: MotionMaskEditPaneProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -107,9 +107,7 @@ export default function MotionMaskEditPane({
|
||||
polygon: z.object({ name: z.string(), isFinished: z.boolean() }),
|
||||
})
|
||||
.refine(() => polygon?.isFinished === true, {
|
||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
|
||||
path: ["polygon.isFinished"],
|
||||
});
|
||||
|
||||
@ -170,11 +168,8 @@ export default function MotionMaskEditPane({
|
||||
polygon.name
|
||||
? t("masksAndZones.motionMasks.toast.success", {
|
||||
polygonName: polygon.name,
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("masksAndZones.motionMasks.toast.success.noName", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
: t("masksAndZones.motionMasks.toast.success.noName"),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
@ -205,6 +200,7 @@ export default function MotionMaskEditPane({
|
||||
scaledHeight,
|
||||
setIsLoading,
|
||||
cameraConfig,
|
||||
t,
|
||||
]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
@ -220,10 +216,8 @@ export default function MotionMaskEditPane({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("masksAndZones.motionMasks.documentTitle", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
}, []);
|
||||
document.title = t("masksAndZones.motionMasks.documentTitle");
|
||||
}, [t]);
|
||||
|
||||
if (!polygon) {
|
||||
return;
|
||||
@ -234,13 +228,11 @@ export default function MotionMaskEditPane({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length
|
||||
? t("masksAndZones.motionMasks.edit", { ns: "views/settings" })
|
||||
: t("masksAndZones.motionMasks.add", { ns: "views/settings" })}
|
||||
? t("masksAndZones.motionMasks.edit")
|
||||
: t("masksAndZones.motionMasks.add")}
|
||||
</Heading>
|
||||
<div className="my-3 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">masksAndZones.motionMasks.context</Trans>
|
||||
</p>
|
||||
<p>{t("masksAndZones.motionMasks.context")}</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -249,9 +241,7 @@ export default function MotionMaskEditPane({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.context.documentation
|
||||
</Trans>{" "}
|
||||
{t("masksAndZones.motionMasks.context.documentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -262,7 +252,6 @@ export default function MotionMaskEditPane({
|
||||
<div className="my-1 inline-flex">
|
||||
{t("masksAndZones.motionMasks.point", {
|
||||
count: polygons[activePolygonIndex].points.length,
|
||||
ns: "views/settings",
|
||||
})}
|
||||
{polygons[activePolygonIndex].isFinished && (
|
||||
<FaCheckCircle className="ml-2 size-5" />
|
||||
@ -278,9 +267,7 @@ export default function MotionMaskEditPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.clickDrawPolygon
|
||||
</Trans>
|
||||
{t("masksAndZones.motionMasks.clickDrawPolygon")}
|
||||
</div>
|
||||
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
@ -290,22 +277,17 @@ export default function MotionMaskEditPane({
|
||||
<div className="mb-3 text-sm text-danger">
|
||||
{t("masksAndZones.motionMasks.polygonAreaTooLarge", {
|
||||
polygonArea: Math.round(polygonArea * 100),
|
||||
ns: "views/settings",
|
||||
})}
|
||||
</div>
|
||||
<div className="mb-3 text-sm text-primary">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.polygonAreaTooLarge.tips
|
||||
</Trans>
|
||||
{t("masksAndZones.motionMasks.polygonAreaTooLarge.tips")}
|
||||
<Link
|
||||
to="https://github.com/blakeblackshear/frigate/discussions/13040"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="my-3 block"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.polygonAreaTooLarge.documentation
|
||||
</Trans>{" "}
|
||||
{t("masksAndZones.motionMasks.polygonAreaTooLarge.documentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -342,7 +324,7 @@ export default function MotionMaskEditPane({
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -354,12 +336,10 @@ export default function MotionMaskEditPane({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -38,8 +38,7 @@ import { toast } from "sonner";
|
||||
import { Toaster } from "../ui/sonner";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { getAttributeLabels } from "@/utils/iconUtil";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ObjectMaskEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
@ -68,6 +67,7 @@ export default function ObjectMaskEditPane({
|
||||
snapPoints,
|
||||
setSnapPoints,
|
||||
}: ObjectMaskEditPaneProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -109,9 +109,7 @@ export default function ObjectMaskEditPane({
|
||||
polygon: z.object({ isFinished: z.boolean(), name: z.string() }),
|
||||
})
|
||||
.refine(() => polygon?.isFinished === true, {
|
||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
|
||||
path: ["polygon.isFinished"],
|
||||
});
|
||||
|
||||
@ -202,11 +200,8 @@ export default function ObjectMaskEditPane({
|
||||
polygon.name
|
||||
? t("masksAndZones.objectMasks.toast.success", {
|
||||
polygonName: polygon.name,
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("masksAndZones.objectMasks.toast.success.noName", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
: t("masksAndZones.objectMasks.toast.success.noName"),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
@ -248,6 +243,7 @@ export default function ObjectMaskEditPane({
|
||||
scaledHeight,
|
||||
setIsLoading,
|
||||
cameraConfig,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
@ -264,10 +260,8 @@ export default function ObjectMaskEditPane({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("masksAndZones.objectMasks.documentTitle", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
}, []);
|
||||
document.title = t("masksAndZones.objectMasks.documentTitle");
|
||||
}, [t]);
|
||||
|
||||
if (!polygon) {
|
||||
return;
|
||||
@ -278,17 +272,11 @@ export default function ObjectMaskEditPane({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length
|
||||
? t("masksAndZones.objectMasks.edit", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("masksAndZones.objectMasks.add", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
? t("masksAndZones.objectMasks.edit")
|
||||
: t("masksAndZones.objectMasks.add")}
|
||||
</Heading>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">masksAndZones.objectMasks.context</Trans>
|
||||
</p>
|
||||
<p>{t("masksAndZones.objectMasks.context")}</p>
|
||||
</div>
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
{polygons && activePolygonIndex !== undefined && (
|
||||
@ -296,7 +284,6 @@ export default function ObjectMaskEditPane({
|
||||
<div className="my-1 inline-flex">
|
||||
{t("masksAndZones.objectMasks.point", {
|
||||
count: polygons[activePolygonIndex].points.length,
|
||||
ns: "views/settings",
|
||||
})}
|
||||
{polygons[activePolygonIndex].isFinished && (
|
||||
<FaCheckCircle className="ml-2 size-5" />
|
||||
@ -312,9 +299,7 @@ export default function ObjectMaskEditPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.clickDrawPolygon
|
||||
</Trans>
|
||||
{t("masksAndZones.objectMasks.clickDrawPolygon")}
|
||||
</div>
|
||||
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
@ -340,9 +325,7 @@ export default function ObjectMaskEditPane({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.objects
|
||||
</Trans>
|
||||
{t("masksAndZones.objectMasks.objects")}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
@ -359,9 +342,7 @@ export default function ObjectMaskEditPane({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.objects.desc
|
||||
</Trans>
|
||||
{t("masksAndZones.objectMasks.objects.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -384,7 +365,7 @@ export default function ObjectMaskEditPane({
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -396,12 +377,10 @@ export default function ObjectMaskEditPane({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@ -417,6 +396,7 @@ type ZoneObjectSelectorProps = {
|
||||
};
|
||||
|
||||
export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const attributeLabels = useMemo(() => {
|
||||
@ -461,9 +441,7 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
|
||||
<>
|
||||
<SelectGroup>
|
||||
<SelectItem value="all_labels">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.objects.allObjectTypes
|
||||
</Trans>
|
||||
{t("masksAndZones.objectMasks.objects.allObjectTypes")}
|
||||
</SelectItem>
|
||||
<SelectSeparator className="bg-secondary" />
|
||||
{allLabels.map((item) => (
|
||||
|
||||
@ -36,7 +36,7 @@ import { reviewQueries } from "@/utils/zoneEdutUtil";
|
||||
import IconWrapper from "../ui/icon-wrapper";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { buttonVariants } from "../ui/button";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PolygonItemProps = {
|
||||
polygon: Polygon;
|
||||
@ -57,6 +57,7 @@ export default function PolygonItem({
|
||||
setEditPane,
|
||||
handleCopyCoordinates,
|
||||
}: PolygonItemProps) {
|
||||
const { t } = useTranslation("views/settings");
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@ -318,9 +319,7 @@ export default function PolygonItem({
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>button.edit</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("button.edit")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
@ -333,9 +332,7 @@ export default function PolygonItem({
|
||||
onClick={() => handleCopyCoordinates(index)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>button.copyCoordinates</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("button.copyCoordinates")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
@ -349,9 +346,7 @@ export default function PolygonItem({
|
||||
onClick={() => !isLoading && setDeleteDialogOpen(true)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>button.delete</Trans>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("button.delete")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -17,8 +17,7 @@ import FilterSwitch from "../filter/FilterSwitch";
|
||||
import { SearchFilter, SearchSource } from "@/types/search";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ExploreSettingsProps = {
|
||||
className?: string;
|
||||
@ -38,6 +37,7 @@ export default function ExploreSettings({
|
||||
setDefaultView,
|
||||
onUpdateFilter,
|
||||
}: ExploreSettingsProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@ -52,20 +52,16 @@ export default function ExploreSettings({
|
||||
size="sm"
|
||||
>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
<Trans ns="components/filter">explore.settings.title</Trans>
|
||||
{t("explore.settings.title")}
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="components/filter">explore.settings.defaultView</Trans>
|
||||
</div>
|
||||
<div className="text-md">{t("explore.settings.defaultView")}</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<Trans ns="components/filter">
|
||||
explore.settings.defaultView.desc
|
||||
</Trans>
|
||||
{t("explore.settings.defaultView.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
@ -74,12 +70,8 @@ export default function ExploreSettings({
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{defaultView == "summary"
|
||||
? t("explore.settings.defaultView.summary", {
|
||||
ns: "components/filter",
|
||||
})
|
||||
: t("explore.settings.defaultView.unfilteredGrid", {
|
||||
ns: "components/filter",
|
||||
})}
|
||||
? t("explore.settings.defaultView.summary")
|
||||
: t("explore.settings.defaultView.unfilteredGrid")}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@ -90,12 +82,8 @@ export default function ExploreSettings({
|
||||
value={value}
|
||||
>
|
||||
{value == "summary"
|
||||
? t("explore.settings.defaultView.summary", {
|
||||
ns: "components/filter",
|
||||
})
|
||||
: t("explore.settings.defaultView.unfilteredGrid", {
|
||||
ns: "components/filter",
|
||||
})}
|
||||
? t("explore.settings.defaultView.summary")
|
||||
: t("explore.settings.defaultView.unfilteredGrid")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@ -107,15 +95,9 @@ export default function ExploreSettings({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="components/filter">
|
||||
explore.settings.gridColumns
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="text-md">{t("explore.settings.gridColumns")}</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<Trans ns="components/filter">
|
||||
explore.settings.gridColumns.desc
|
||||
</Trans>
|
||||
{t("explore.settings.gridColumns.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
@ -171,18 +153,15 @@ export function SearchTypeContent({
|
||||
searchSources,
|
||||
setSearchSources,
|
||||
}: SearchTypeContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="components/filter">explore.settings.searchSource</Trans>
|
||||
</div>
|
||||
<div className="text-md">{t("explore.settings.searchSource")}</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<Trans ns="components/filter">
|
||||
explore.settings.searchSource.desc
|
||||
</Trans>
|
||||
{t("explore.settings.searchSource.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2.5 flex flex-col gap-2.5">
|
||||
|
||||
@ -29,8 +29,7 @@ import { toast } from "sonner";
|
||||
import { flattenPoints, interpolatePoints } from "@/utils/canvasUtil";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { getAttributeLabels } from "@/utils/iconUtil";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ZoneEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
@ -61,6 +60,7 @@ export default function ZoneEditPane({
|
||||
snapPoints,
|
||||
setSnapPoints,
|
||||
}: ZoneEditPaneProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -106,7 +106,6 @@ export default function ZoneEditPane({
|
||||
.min(2, {
|
||||
message: t(
|
||||
"masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters",
|
||||
{ ns: "views/settings" },
|
||||
),
|
||||
})
|
||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||
@ -117,7 +116,6 @@ export default function ZoneEditPane({
|
||||
{
|
||||
message: t(
|
||||
"masksAndZones.form.zoneName.error.mustNotBeSameWithCamera",
|
||||
{ ns: "views/settings" },
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -131,9 +129,7 @@ export default function ZoneEditPane({
|
||||
return !otherPolygonNames.includes(value);
|
||||
},
|
||||
{
|
||||
message: t("masksAndZones.form.zoneName.error.alreadyExists", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.zoneName.error.alreadyExists"),
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
@ -143,21 +139,16 @@ export default function ZoneEditPane({
|
||||
{
|
||||
message: t(
|
||||
"masksAndZones.form.zoneName.error.mustNotContainPeriod",
|
||||
{ ns: "views/settings" },
|
||||
),
|
||||
},
|
||||
)
|
||||
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
|
||||
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter"),
|
||||
}),
|
||||
inertia: z.coerce
|
||||
.number()
|
||||
.min(1, {
|
||||
message: t("masksAndZones.form.inertia.error.mustBeAboveZero", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.inertia.error.mustBeAboveZero"),
|
||||
})
|
||||
.or(z.literal("")),
|
||||
loitering_time: z.coerce
|
||||
@ -165,15 +156,12 @@ export default function ZoneEditPane({
|
||||
.min(0, {
|
||||
message: t(
|
||||
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
|
||||
{ ns: "views/settings" },
|
||||
),
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
|
||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished"),
|
||||
}),
|
||||
objects: z.array(z.string()).optional(),
|
||||
review_alerts: z.boolean().default(false).optional(),
|
||||
@ -182,36 +170,28 @@ export default function ZoneEditPane({
|
||||
lineA: z.coerce
|
||||
.number()
|
||||
.min(0.1, {
|
||||
message: t("masksAndZones.form.distance.error", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.distance.error"),
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
lineB: z.coerce
|
||||
.number()
|
||||
.min(0.1, {
|
||||
message: t("masksAndZones.form.distance.error", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.distance.error"),
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
lineC: z.coerce
|
||||
.number()
|
||||
.min(0.1, {
|
||||
message: t("masksAndZones.form.distance.error", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.distance.error"),
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
lineD: z.coerce
|
||||
.number()
|
||||
.min(0.1, {
|
||||
message: t("masksAndZones.form.distance.error", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.distance.error"),
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
@ -231,9 +211,7 @@ export default function ZoneEditPane({
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: t("masksAndZones.form.distance.error.mustBeFilled", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
message: t("masksAndZones.form.distance.error.mustBeFilled"),
|
||||
path: ["speedEstimation"],
|
||||
},
|
||||
)
|
||||
@ -249,9 +227,6 @@ export default function ZoneEditPane({
|
||||
{
|
||||
message: t(
|
||||
"masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError",
|
||||
{
|
||||
ns: "views/settings",
|
||||
},
|
||||
),
|
||||
path: ["loitering_time"],
|
||||
},
|
||||
@ -291,13 +266,11 @@ export default function ZoneEditPane({
|
||||
polygon.points.length !== 4
|
||||
) {
|
||||
toast.error(
|
||||
t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError", {
|
||||
ns: "views/settings",
|
||||
}),
|
||||
t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError"),
|
||||
);
|
||||
form.setValue("speedEstimation", false);
|
||||
}
|
||||
}, [polygon, form]);
|
||||
}, [polygon, form, t]);
|
||||
|
||||
const saveToConfig = useCallback(
|
||||
async (
|
||||
@ -441,7 +414,6 @@ export default function ZoneEditPane({
|
||||
toast.success(
|
||||
t("masksAndZones.zones.toast.success", {
|
||||
zoneName,
|
||||
ns: "views/settings",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
@ -483,6 +455,7 @@ export default function ZoneEditPane({
|
||||
scaledHeight,
|
||||
setIsLoading,
|
||||
cameraConfig,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
@ -503,10 +476,8 @@ export default function ZoneEditPane({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("masksAndZones.zones.documentTitle", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
}, []);
|
||||
document.title = t("masksAndZones.zones.documentTitle");
|
||||
}, [t]);
|
||||
|
||||
if (!polygon) {
|
||||
return;
|
||||
@ -517,17 +488,11 @@ export default function ZoneEditPane({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length
|
||||
? t("masksAndZones.zones.edit", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("masksAndZones.zones.add", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
? t("masksAndZones.zones.edit")
|
||||
: t("masksAndZones.zones.add")}
|
||||
</Heading>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">masksAndZones.zones.desc</Trans>
|
||||
</p>
|
||||
<p>{t("masksAndZones.zones.desc")}</p>
|
||||
</div>
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
{polygons && activePolygonIndex !== undefined && (
|
||||
@ -535,7 +500,6 @@ export default function ZoneEditPane({
|
||||
<div className="my-1 inline-flex">
|
||||
{t("masksAndZones.zones.point", {
|
||||
count: polygons[activePolygonIndex].points.length,
|
||||
ns: "views/settings",
|
||||
})}
|
||||
|
||||
{polygons[activePolygonIndex].isFinished && (
|
||||
@ -552,7 +516,7 @@ export default function ZoneEditPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">masksAndZones.zones.clickDrawPolygon</Trans>
|
||||
{t("masksAndZones.zones.clickDrawPolygon")}
|
||||
</div>
|
||||
|
||||
<Separator className="my-3 bg-secondary" />
|
||||
@ -564,25 +528,16 @@ export default function ZoneEditPane({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="views/settings">masksAndZones.zones.name</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("masksAndZones.zones.name")}</FormLabel>
|
||||
<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={t(
|
||||
"masksAndZones.zones.name.inputPlaceHolder",
|
||||
{
|
||||
ns: "views/settings",
|
||||
},
|
||||
)}
|
||||
placeholder={t("masksAndZones.zones.name.inputPlaceHolder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.name.tips
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.name.tips")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -594,9 +549,7 @@ export default function ZoneEditPane({
|
||||
name="inertia"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="views/settings">masksAndZones.zones.inertia</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("masksAndZones.zones.inertia")}</FormLabel>
|
||||
<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]"
|
||||
@ -605,9 +558,7 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.inertia.desc
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.inertia.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -619,11 +570,7 @@ export default function ZoneEditPane({
|
||||
name="loitering_time"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.loiteringTime
|
||||
</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("masksAndZones.zones.loiteringTime")}</FormLabel>
|
||||
<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]"
|
||||
@ -632,9 +579,7 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.loiteringTime.desc
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.loiteringTime.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -642,13 +587,9 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="views/settings">masksAndZones.zones.objects</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("masksAndZones.zones.objects")}</FormLabel>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.objects.desc
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.objects.desc")}
|
||||
</FormDescription>
|
||||
<ZoneObjectSelector
|
||||
camera={polygon.camera}
|
||||
@ -682,9 +623,7 @@ export default function ZoneEditPane({
|
||||
className="cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.speedEstimation
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.speedEstimation")}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
@ -698,9 +637,6 @@ export default function ZoneEditPane({
|
||||
toast.error(
|
||||
t(
|
||||
"masksAndZones.zones.speedEstimation.pointLengthError",
|
||||
{
|
||||
ns: "views/settings",
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
@ -712,9 +648,6 @@ export default function ZoneEditPane({
|
||||
toast.error(
|
||||
t(
|
||||
"masksAndZones.zones.speedEstimation.loiteringTimeError",
|
||||
{
|
||||
ns: "views/settings",
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -725,9 +658,7 @@ export default function ZoneEditPane({
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.speedEstimation.desc
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.speedEstimation.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -839,17 +770,12 @@ export default function ZoneEditPane({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans
|
||||
values={{
|
||||
unit:
|
||||
config?.ui.unit_system == "imperial"
|
||||
? t("unit.speed.mph")
|
||||
: t("unit.speed.kph"),
|
||||
}}
|
||||
ns="views/settings"
|
||||
>
|
||||
masksAndZones.zones.speedThreshold
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.speedThreshold", {
|
||||
unit:
|
||||
config?.ui.unit_system == "imperial"
|
||||
? t("unit.speed.mph")
|
||||
: t("unit.speed.kph"),
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@ -858,9 +784,7 @@ export default function ZoneEditPane({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.speedThreshold.desc
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.speedThreshold.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -884,7 +808,7 @@ export default function ZoneEditPane({
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -896,12 +820,10 @@ export default function ZoneEditPane({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@ -924,6 +846,7 @@ export function ZoneObjectSelector({
|
||||
selectedLabels,
|
||||
updateLabelFilter,
|
||||
}: ZoneObjectSelectorProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const attributeLabels = useMemo(() => {
|
||||
@ -981,7 +904,7 @@ export function ZoneObjectSelector({
|
||||
<div className="scrollbar-container h-auto overflow-y-auto overflow-x-hidden">
|
||||
<div className="my-2.5 flex items-center justify-between">
|
||||
<Label className="cursor-pointer text-primary" htmlFor="allLabels">
|
||||
<Trans ns="views/settings">masksAndZones.zones.allObjects</Trans>
|
||||
{t("masksAndZones.zones.allObjects")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
|
||||
@ -13,7 +13,7 @@ import { Switch } from "./switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LuCheck } from "react-icons/lu";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
|
||||
export interface DateRangePickerProps {
|
||||
/** Click handler for applying the updates from DateRangePicker. */
|
||||
@ -431,7 +431,7 @@ export function DateRangePicker({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trans>button.apply</Trans>
|
||||
{t("button.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -442,7 +442,7 @@ export function DateRangePicker({
|
||||
variant="ghost"
|
||||
aria-label="Reset"
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,9 +10,11 @@ import useSWR from "swr";
|
||||
import useDeepMemo from "./use-deep-memo";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { useFrigateStats } from "@/api/ws";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function useStats(stats: FrigateStats | undefined) {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const memoizedStats = useDeepMemo(stats);
|
||||
@ -76,7 +78,6 @@ export default function useStats(stats: FrigateStats | undefined) {
|
||||
text: t("stats.ffmpegHighCpuUsage", {
|
||||
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
|
||||
ffmpegAvg,
|
||||
ns: "views/system",
|
||||
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
||||
color: "text-danger",
|
||||
relevantLink: "/system#cameras",
|
||||
@ -88,7 +89,6 @@ export default function useStats(stats: FrigateStats | undefined) {
|
||||
text: t("stats.detectHighCpuUsage", {
|
||||
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
|
||||
detectAvg,
|
||||
ns: "views/system",
|
||||
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
|
||||
color: "text-danger",
|
||||
relevantLink: "/system#cameras",
|
||||
@ -97,7 +97,7 @@ export default function useStats(stats: FrigateStats | undefined) {
|
||||
});
|
||||
|
||||
return problems;
|
||||
}, [config, memoizedStats]);
|
||||
}, [config, memoizedStats, t]);
|
||||
|
||||
return { potentialProblems };
|
||||
}
|
||||
|
||||
@ -14,11 +14,12 @@ import { toast } from "sonner";
|
||||
import { LuCopy, LuSave } from "react-icons/lu";
|
||||
import { MdOutlineRestartAlt } from "react-icons/md";
|
||||
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SaveOptions = "saveonly" | "restart";
|
||||
|
||||
function ConfigEditor() {
|
||||
const { t } = useTranslation(["views/configEditor"]);
|
||||
const apiHost = useApiHost();
|
||||
|
||||
useEffect(() => {
|
||||
@ -191,7 +192,7 @@ function ConfigEditor() {
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<div className="mr-1 flex items-center justify-between">
|
||||
<Heading as="h2" className="mb-0 ml-1 md:ml-0">
|
||||
<Trans ns="views/configEditor">configEditor</Trans>
|
||||
{t("configEditor")}
|
||||
</Heading>
|
||||
<div className="flex flex-row gap-1">
|
||||
<Button
|
||||
@ -201,9 +202,7 @@ function ConfigEditor() {
|
||||
onClick={() => handleCopyConfig()}
|
||||
>
|
||||
<LuCopy className="text-secondary-foreground" />
|
||||
<span className="hidden md:block">
|
||||
<Trans ns="views/configEditor">copyConfig</Trans>
|
||||
</span>
|
||||
<span className="hidden md:block">{t("copyConfig")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -215,9 +214,7 @@ function ConfigEditor() {
|
||||
<LuSave className="absolute left-0 top-0 size-3 text-secondary-foreground" />
|
||||
<MdOutlineRestartAlt className="absolute size-4 translate-x-1 translate-y-1/2 text-secondary-foreground" />
|
||||
</div>
|
||||
<span className="hidden md:block">
|
||||
<Trans ns="views/configEditor">saveAndRestart</Trans>
|
||||
</span>
|
||||
<span className="hidden md:block">{t("saveAndRestart")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -226,9 +223,7 @@ function ConfigEditor() {
|
||||
onClick={() => onHandleSaveConfig("saveonly")}
|
||||
>
|
||||
<LuSave className="text-secondary-foreground" />
|
||||
<span className="hidden md:block">
|
||||
<Trans ns="views/configEditor">saveOnly</Trans>
|
||||
</span>
|
||||
<span className="hidden md:block">{t("saveOnly")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,20 +17,22 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DeleteClipType, Export } from "@/types/export";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { LuFolderX } from "react-icons/lu";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
|
||||
function Exports() {
|
||||
const { t } = useTranslation(["views/exports"]);
|
||||
const { data: exports, mutate } = useSWR<Export[]>("exports");
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("documentTitle", { ns: "views/exports" });
|
||||
}, []);
|
||||
document.title = t("documentTitle");
|
||||
}, [t]);
|
||||
|
||||
// Search
|
||||
|
||||
@ -117,29 +119,20 @@ function Exports() {
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans ns="views/exports">deleteExport</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("deleteExport")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
ns="views/exports"
|
||||
values={{ exportName: deleteClip?.exportName }}
|
||||
>
|
||||
deleteExport.desc
|
||||
</Trans>
|
||||
{t("deleteExport.desc", { exportName: deleteClip?.exportName })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>button.cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("button.cancel")}</AlertDialogCancel>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Delete Export"
|
||||
variant="destructive"
|
||||
onClick={() => onHandleDelete()}
|
||||
>
|
||||
<Trans>button.delete</Trans>
|
||||
{t("button.delete")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -187,7 +180,7 @@ function Exports() {
|
||||
<div className="flex w-full items-center justify-center p-2">
|
||||
<Input
|
||||
className="text-md w-full bg-muted md:w-1/3"
|
||||
placeholder={t("search", { ns: "views/exports" })}
|
||||
placeholder={t("search")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
@ -215,7 +208,7 @@ function Exports() {
|
||||
) : (
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||
<LuFolderX className="size-16" />
|
||||
<Trans ns="views/exports" i18nKey={"noExports"}></Trans>
|
||||
{t("noExports")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,11 +9,13 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
|
||||
import LiveCameraView from "@/views/live/LiveCameraView";
|
||||
import LiveDashboardView from "@/views/live/LiveDashboardView";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
function Live() {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// selection
|
||||
@ -67,17 +69,15 @@ function Live() {
|
||||
.map((text) => text[0].toUpperCase() + text.substring(1));
|
||||
document.title = t("documentTitle.withCamera", {
|
||||
camera: capitalized.join(" "),
|
||||
ns: "views/live",
|
||||
});
|
||||
} else if (cameraGroup && cameraGroup != "default") {
|
||||
document.title = t("documentTitle.withCamera", {
|
||||
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
||||
ns: "views/live",
|
||||
});
|
||||
} else {
|
||||
document.title = t("documentTitle", { ns: "views/live" });
|
||||
}
|
||||
}, [cameraGroup, selectedCameraName]);
|
||||
}, [cameraGroup, selectedCameraName, t]);
|
||||
|
||||
// settings
|
||||
|
||||
|
||||
@ -14,15 +14,15 @@ import CameraMetrics from "@/views/system/CameraMetrics";
|
||||
import { useHashState } from "@/hooks/use-overlay-state";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import FeatureMetrics from "@/views/system/FeatureMetrics";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const allMetrics = ["general", "features", "storage", "cameras"] as const;
|
||||
type SystemMetric = (typeof allMetrics)[number];
|
||||
|
||||
function System() {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
@ -94,9 +94,7 @@ function System() {
|
||||
{item == "storage" && <LuHardDrive className="size-4" />}
|
||||
{item == "cameras" && <FaVideo className="size-4" />}
|
||||
{isDesktop && (
|
||||
<div className="capitalize">
|
||||
{t(item + ".title", { ns: "views/system" })}
|
||||
</div>
|
||||
<div className="capitalize">{t(item + ".title")}</div>
|
||||
)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
@ -105,16 +103,14 @@ function System() {
|
||||
<div className="flex h-full items-center">
|
||||
{lastUpdated && (
|
||||
<div className="h-full content-center text-sm text-muted-foreground">
|
||||
<Trans ns="views/system">lastRefreshed</Trans>
|
||||
{t("lastRefreshed")}
|
||||
<TimeAgo time={lastUpdated * 1000} dense />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-end gap-2">
|
||||
<div className="h-full content-center font-medium">
|
||||
<Trans ns="views/system">title</Trans>
|
||||
</div>
|
||||
<div className="h-full content-center font-medium">{t("title")}</div>
|
||||
{statsSnapshot && (
|
||||
<div className="h-full content-center text-sm text-muted-foreground">
|
||||
{statsSnapshot.service.version}
|
||||
|
||||
@ -54,9 +54,9 @@ import { FilterList, LAST_24_HOURS_KEY } from "@/types/filter";
|
||||
import { GiSoundWaves } from "react-icons/gi";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type EventViewProps = {
|
||||
reviewItems?: SegmentedReviewData;
|
||||
@ -96,6 +96,7 @@ export default function EventView({
|
||||
pullLatestData,
|
||||
updateFilter,
|
||||
}: EventViewProps) {
|
||||
const { t } = useTranslation(["views/event"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@ -223,7 +224,7 @@ export default function EventView({
|
||||
);
|
||||
});
|
||||
},
|
||||
[reviewItems],
|
||||
[reviewItems, t],
|
||||
);
|
||||
|
||||
const [motionOnly, setMotionOnly] = useState(false);
|
||||
@ -295,7 +296,7 @@ export default function EventView({
|
||||
<>
|
||||
<MdCircle className="size-2 text-severity_alert md:mr-[10px]" />
|
||||
<div className="hidden md:flex md:flex-row md:items-center">
|
||||
<Trans ns="views/events">alerts</Trans>
|
||||
{t("alerts")}
|
||||
{reviewCounts.alert > -1 ? (
|
||||
` ∙ ${reviewCounts.alert}`
|
||||
) : (
|
||||
@ -331,7 +332,7 @@ export default function EventView({
|
||||
<>
|
||||
<MdCircle className="size-2 text-severity_detection md:mr-[10px]" />
|
||||
<div className="hidden md:flex md:flex-row md:items-center">
|
||||
<Trans ns="views/events">detections</Trans>
|
||||
{t("detections")}
|
||||
{reviewCounts.detection > -1 ? (
|
||||
` ∙ ${reviewCounts.detection}`
|
||||
) : (
|
||||
@ -354,9 +355,7 @@ export default function EventView({
|
||||
) : (
|
||||
<>
|
||||
<MdCircle className="size-2 text-severity_significant_motion md:mr-[10px]" />
|
||||
<div className="hidden md:block">
|
||||
<Trans ns="views/events">motion.label</Trans>
|
||||
</div>
|
||||
<div className="hidden md:block">{t("motion.label")}</div>
|
||||
</>
|
||||
)}
|
||||
</ToggleGroupItem>
|
||||
@ -473,6 +472,8 @@ function DetectionReview({
|
||||
setSelectedReviews,
|
||||
pullLatestData,
|
||||
}: DetectionReviewProps) {
|
||||
const { t } = useTranslation(["views/event"]);
|
||||
|
||||
const reviewTimelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// detail
|
||||
@ -724,7 +725,7 @@ function DetectionReview({
|
||||
{!loading && currentItems?.length === 0 && (
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||
<LuFolderCheck className="size-16" />
|
||||
<Trans ns="views/events">empty.{severity.replace(/_/g, " ")}</Trans>
|
||||
{t("empty." + severity.replace(/_/g, " "))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -869,6 +870,7 @@ function MotionReview({
|
||||
motionOnly = false,
|
||||
onOpenRecording,
|
||||
}: MotionReviewProps) {
|
||||
const { t } = useTranslation(["views/event"]);
|
||||
const segmentDuration = 30;
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -1058,7 +1060,7 @@ function MotionReview({
|
||||
return (
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||
<LuFolderX className="size-16" />
|
||||
<Trans ns="views/events">empty.motion</Trans>
|
||||
{t("empty.motion")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -103,8 +103,7 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
||||
import useSWR from "swr";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSessionPersistence } from "@/hooks/use-session-persistence";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -119,6 +118,7 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type LiveCameraViewProps = {
|
||||
config?: FrigateConfig;
|
||||
@ -134,6 +134,7 @@ export default function LiveCameraView({
|
||||
fullscreen,
|
||||
toggleFullscreen,
|
||||
}: LiveCameraViewProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const navigate = useNavigate();
|
||||
const { isPortrait } = useMobileOrientation();
|
||||
const mainRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -435,9 +436,7 @@ export default function LiveCameraView({
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>button.back</Trans>
|
||||
</div>
|
||||
<div className="text-primary">{t("button.back")}</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
@ -459,9 +458,7 @@ export default function LiveCameraView({
|
||||
>
|
||||
<LuHistory className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>button.history</Trans>
|
||||
</div>
|
||||
<div className="text-primary">{t("button.history")}</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@ -482,7 +479,7 @@ export default function LiveCameraView({
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-secondary-foreground">
|
||||
<Trans>button.back</Trans>
|
||||
{t("button.back")}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
@ -681,6 +678,7 @@ function PtzControlPanel({
|
||||
clickOverlay: boolean;
|
||||
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
|
||||
|
||||
const { send: sendPtz } = usePtzCommand(camera);
|
||||
@ -766,7 +764,7 @@ function PtzControlPanel({
|
||||
{ptz?.features?.includes("pt") && (
|
||||
<>
|
||||
<TooltipButton
|
||||
label={t("ptz.move.left.label", { ns: "views/live" })}
|
||||
label={t("ptz.move.left.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_LEFT");
|
||||
@ -781,7 +779,7 @@ function PtzControlPanel({
|
||||
<FaAngleLeft />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label={t("ptz.move.up.label", { ns: "views/live" })}
|
||||
label={t("ptz.move.up.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_UP");
|
||||
@ -796,7 +794,7 @@ function PtzControlPanel({
|
||||
<FaAngleUp />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label={t("ptz.move.down.label", { ns: "views/live" })}
|
||||
label={t("ptz.move.down.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_DOWN");
|
||||
@ -811,7 +809,7 @@ function PtzControlPanel({
|
||||
<FaAngleDown />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label={t("ptz.move.right.label", { ns: "views/live" })}
|
||||
label={t("ptz.move.right.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_RIGHT");
|
||||
@ -830,7 +828,7 @@ function PtzControlPanel({
|
||||
{ptz?.features?.includes("zoom") && (
|
||||
<>
|
||||
<TooltipButton
|
||||
label={t("ptz.zoom.in.label", { ns: "views/live" })}
|
||||
label={t("ptz.zoom.in.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("ZOOM_IN");
|
||||
@ -845,7 +843,7 @@ function PtzControlPanel({
|
||||
<MdZoomIn />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label={t("ptz.zoom.out.label", { ns: "views/live" })}
|
||||
label={t("ptz.zoom.out.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("ZOOM_OUT");
|
||||
@ -981,6 +979,8 @@ function FrigateCameraFeatures({
|
||||
supports2WayTalk,
|
||||
cameraEnabled,
|
||||
}: FrigateCameraFeaturesProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
|
||||
const { payload: detectState, send: sendDetect } = useDetectState(
|
||||
camera.name,
|
||||
);
|
||||
@ -1024,15 +1024,9 @@ function FrigateCameraFeatures({
|
||||
setIsRecording(true);
|
||||
const toastId = toast.success(
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="font-semibold">
|
||||
<Trans ns="views/live">manualRecording.started</Trans>
|
||||
</div>
|
||||
<div className="font-semibold">{t("manualRecording.started")}</div>
|
||||
{!camera.record.enabled || camera.record.retain.days == 0 ? (
|
||||
<div>
|
||||
<Trans ns="views/live">
|
||||
manualRecording.recordDisabledTips
|
||||
</Trans>
|
||||
</div>
|
||||
<div>{t("manualRecording.recordDisabledTips")}</div>
|
||||
) : (
|
||||
<OnDemandRetentionMessage camera={camera} />
|
||||
)}
|
||||
@ -1045,11 +1039,11 @@ function FrigateCameraFeatures({
|
||||
setActiveToastId(toastId);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("manualRecording.failedToStart", { ns: "views/live" }), {
|
||||
toast.error(t("manualRecording.failedToStart"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
}, [camera]);
|
||||
}, [camera, t]);
|
||||
|
||||
const endEvent = useCallback(() => {
|
||||
if (activeToastId) {
|
||||
@ -1062,16 +1056,16 @@ function FrigateCameraFeatures({
|
||||
});
|
||||
recordingEventIdRef.current = null;
|
||||
setIsRecording(false);
|
||||
toast.success(t("manualRecording.ended", { ns: "views/live" }), {
|
||||
toast.success(t("manualRecording.ended"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("manualRecording.failedToEnd", { ns: "views/live" }), {
|
||||
toast.error(t("manualRecording.failedToEnd"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
}, [activeToastId]);
|
||||
}, [activeToastId, t]);
|
||||
|
||||
const handleEventButtonClick = useCallback(() => {
|
||||
if (isRecording) {
|
||||
@ -1174,9 +1168,7 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
|
||||
isActive={isRecording}
|
||||
title={t("manualRecording." + (isRecording ? "stop" : "start"), {
|
||||
ns: "views/live",
|
||||
})}
|
||||
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
|
||||
onClick={handleEventButtonClick}
|
||||
disabled={!cameraEnabled}
|
||||
/>
|
||||
@ -1197,15 +1189,13 @@ function FrigateCameraFeatures({
|
||||
<div className="flex flex-col gap-5 p-4">
|
||||
{!isRestreamed && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>
|
||||
<Trans ns="components/dialog">streaming</Trans>
|
||||
</Label>
|
||||
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
|
||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.disabled
|
||||
</Trans>
|
||||
{t("streaming.restreaming.disabled", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@ -1215,9 +1205,9 @@ function FrigateCameraFeatures({
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.desc
|
||||
</Trans>
|
||||
{t("streaming.restreaming.desc", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@ -1225,9 +1215,9 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.readTheDocumentation
|
||||
</Trans>
|
||||
{t("streaming.restreaming.readTheDocumentation", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -1408,7 +1398,7 @@ function FrigateCameraFeatures({
|
||||
className="mx-0 cursor-pointer text-primary"
|
||||
htmlFor="showstats"
|
||||
>
|
||||
<Trans ns="components/dialog">streaming.showStats</Trans>
|
||||
{t("streaming.showStats", { ns: "components/dialog" })}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@ -1418,12 +1408,12 @@ function FrigateCameraFeatures({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans ns="components/dialog">streaming.showStats.desc</Trans>
|
||||
{t("streaming.showStats.desc", { ns: "components/dialog" })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
||||
<Trans ns="components/dialog">streaming.debugView</Trans>
|
||||
{t("streaming.debugView", { ns: "components/dialog" })}
|
||||
<LuExternalLink
|
||||
onClick={() =>
|
||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
||||
@ -1514,15 +1504,13 @@ function FrigateCameraFeatures({
|
||||
<div className="mt-3 flex flex-col gap-5">
|
||||
{!isRestreamed && (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
<Label>
|
||||
<Trans ns="components/dialog">streaming</Trans>
|
||||
</Label>
|
||||
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
|
||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.disabled
|
||||
</Trans>
|
||||
{t("streaming.restreaming.disabled", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@ -1532,9 +1520,9 @@ function FrigateCameraFeatures({
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.desc
|
||||
</Trans>
|
||||
{t("streaming.restreaming.desc", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@ -1542,9 +1530,9 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="components/dialog">
|
||||
streaming.restreaming.readTheDocumentation
|
||||
</Trans>
|
||||
{t("streaming.restreaming.readTheDocumentation", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -1697,9 +1685,7 @@ function FrigateCameraFeatures({
|
||||
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
|
||||
)}
|
||||
>
|
||||
<Trans ns="views/live">
|
||||
manualRecording.{isRecording ? "end" : "start"}
|
||||
</Trans>
|
||||
{t("manualRecording." + isRecording ? "end" : "start")}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Start a manual event based on this camera's recording retention
|
||||
|
||||
@ -47,9 +47,9 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFullscreen } from "@/hooks/use-fullscreen";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTimezone } from "@/hooks/use-date-utils";
|
||||
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type RecordingViewProps = {
|
||||
startCamera: string;
|
||||
@ -73,6 +73,7 @@ export function RecordingView({
|
||||
filter,
|
||||
updateFilter,
|
||||
}: RecordingViewProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const navigate = useNavigate();
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -400,9 +401,7 @@ export function RecordingView({
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>button.back</Trans>
|
||||
</div>
|
||||
<div className="text-primary">{t("button.back")}</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
@ -414,11 +413,7 @@ export function RecordingView({
|
||||
}}
|
||||
>
|
||||
<FaVideo className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
<Trans>menu.live</Trans>
|
||||
</div>
|
||||
)}
|
||||
{isDesktop && <div className="text-primary">{t("menu.live")}</div>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@ -493,18 +488,14 @@ export function RecordingView({
|
||||
value="timeline"
|
||||
aria-label="Select timeline"
|
||||
>
|
||||
<div className="">
|
||||
<Trans ns="views/events">timeline</Trans>
|
||||
</div>
|
||||
<div className="">{t("timeline")}</div>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
|
||||
value="events"
|
||||
aria-label="Select events"
|
||||
>
|
||||
<div className="">
|
||||
<Trans ns="views/events">events.label</Trans>
|
||||
</div>
|
||||
<div className="">{t("events.label")}</div>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
) : (
|
||||
@ -702,6 +693,7 @@ function Timeline({
|
||||
setScrubbing,
|
||||
setExportRange,
|
||||
}: TimelineProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
||||
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||
|
||||
@ -811,7 +803,7 @@ function Timeline({
|
||||
>
|
||||
{mainCameraReviewItems.length === 0 ? (
|
||||
<div className="mt-5 text-center text-primary">
|
||||
<Trans ns="views/events">events.noFoundForTimePeriod</Trans>
|
||||
{t("events.noFoundForTimePeriod")}
|
||||
</div>
|
||||
) : (
|
||||
mainCameraReviewItems.map((review) => {
|
||||
|
||||
@ -31,7 +31,7 @@ import {
|
||||
import Chip from "@/components/indicators/Chip";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import SearchActionGroup from "@/components/filter/SearchActionGroup";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SearchViewProps = {
|
||||
search: string;
|
||||
@ -71,6 +71,7 @@ export default function SearchView({
|
||||
setColumns,
|
||||
setDefaultView,
|
||||
}: SearchViewProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
@ -521,7 +522,7 @@ export default function SearchView({
|
||||
{uniqueResults?.length == 0 && !isLoading && (
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||
<LuSearchX className="size-16" />
|
||||
<Trans ns="views/explore">noTrackedObjects</Trans>
|
||||
{t("noTrackedObjects")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -13,8 +13,7 @@ import { toast } from "sonner";
|
||||
import DeleteUserDialog from "@/components/overlay/DeleteUserDialog";
|
||||
import { HiTrash } from "react-icons/hi";
|
||||
import { FaUserEdit } from "react-icons/fa";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { LuPlus, LuShield, LuUserCog } from "react-icons/lu";
|
||||
import {
|
||||
Table,
|
||||
@ -32,8 +31,10 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import RoleChangeDialog from "@/components/overlay/RoleChangeDialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AuthenticationView() {
|
||||
const { t } = useTranslation("views/settings");
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const { data: users, mutate: mutateUsers } = useSWR<User[]>("users");
|
||||
|
||||
@ -51,33 +52,35 @@ export default function AuthenticationView() {
|
||||
document.title = "Authentication Settings - Frigate";
|
||||
}, []);
|
||||
|
||||
const onSavePassword = useCallback((user: string, password: string) => {
|
||||
axios
|
||||
.put(`users/${user}/password`, { password })
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setShowSetPassword(false);
|
||||
toast.success("Password updated successfully", {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("users.toast.error.setPasswordFailed", {
|
||||
ns: "views/settings",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
const onSavePassword = useCallback(
|
||||
(user: string, password: string) => {
|
||||
axios
|
||||
.put(`users/${user}/password`, { password })
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setShowSetPassword(false);
|
||||
toast.success("Password updated successfully", {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("users.toast.error.setPasswordFailed", {
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const onCreate = (
|
||||
user: string,
|
||||
@ -93,12 +96,9 @@ export default function AuthenticationView() {
|
||||
users?.push({ username: user, role: role });
|
||||
return users;
|
||||
}, false);
|
||||
toast.success(
|
||||
t("users.toast.success.createUser", { ns: "views/settings", user }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.success(t("users.toast.success.createUser", { user }), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -108,7 +108,6 @@ export default function AuthenticationView() {
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("users.toast.error.createUserFailed", {
|
||||
ns: "views/settings",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
@ -128,12 +127,9 @@ export default function AuthenticationView() {
|
||||
(users) => users?.filter((u) => u.username !== user),
|
||||
false,
|
||||
);
|
||||
toast.success(
|
||||
t("users.toast.success.deleteUser", { ns: "views/settings", user }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.success(t("users.toast.success.deleteUser", { user }), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -143,7 +139,6 @@ export default function AuthenticationView() {
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("users.toast.error.deleteUserFailed", {
|
||||
ns: "views/settings",
|
||||
errorMessage,
|
||||
}),
|
||||
{
|
||||
@ -199,10 +194,10 @@ export default function AuthenticationView() {
|
||||
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
||||
<div className="flex flex-col items-start">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">users.management</Trans>
|
||||
{t("users.management")}
|
||||
</Heading>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">users.management.desc</Trans>
|
||||
{t("users.management.desc")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@ -212,7 +207,7 @@ export default function AuthenticationView() {
|
||||
onClick={() => setShowCreate(true)}
|
||||
>
|
||||
<LuPlus className="size-4" />
|
||||
<Trans ns="views/settings">users.addUser</Trans>
|
||||
{t("users.addUser")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
@ -222,13 +217,11 @@ export default function AuthenticationView() {
|
||||
<TableHeader className="sticky top-0 bg-muted/50">
|
||||
<TableRow>
|
||||
<TableHead className="w-[250px]">
|
||||
<Trans ns="views/settings">users.table.username</Trans>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans ns="views/settings">users.table.role</Trans>
|
||||
{t("users.table.username")}
|
||||
</TableHead>
|
||||
<TableHead>{t("users.table.role")}</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<Trans ns="views/settings">users.table.actions</Trans>
|
||||
{t("users.table.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@ -236,7 +229,7 @@ export default function AuthenticationView() {
|
||||
{users.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="h-24 text-center">
|
||||
<Trans ns="views/settings">users.table.noUsers</Trans>
|
||||
{t("users.table.noUsers")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@ -263,7 +256,7 @@ export default function AuthenticationView() {
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<Trans>role.{user.role || "viewer"}</Trans>
|
||||
{t("role." + user.role || "viewer")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
@ -287,16 +280,12 @@ export default function AuthenticationView() {
|
||||
>
|
||||
<LuUserCog className="size-3.5" />
|
||||
<span className="ml-1.5 hidden sm:inline-block">
|
||||
<Trans>role.title</Trans>
|
||||
{t("role.title")}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
users.table.changeRole
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("users.table.changeRole")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -314,18 +303,12 @@ export default function AuthenticationView() {
|
||||
>
|
||||
<FaUserEdit className="size-3.5" />
|
||||
<span className="ml-1.5 hidden sm:inline-block">
|
||||
<Trans ns="views/settings">
|
||||
users.table.password
|
||||
</Trans>
|
||||
{t("users.table.password")}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
users.updatePassword
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("users.updatePassword")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@ -343,16 +326,12 @@ export default function AuthenticationView() {
|
||||
>
|
||||
<HiTrash className="size-3.5" />
|
||||
<span className="ml-1.5 hidden sm:inline-block">
|
||||
<Trans>button.delete</Trans>
|
||||
{t("button.delete")}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
users.table.deleteUser
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("users.table.deleteUser")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -27,11 +27,11 @@ import { LuExternalLink } from "react-icons/lu";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { MdCircle } from "react-icons/md";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useAlertsState, useDetectionsState, useEnabledState } from "@/api/ws";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraSettingsViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -47,6 +47,7 @@ export default function CameraSettingsView({
|
||||
selectedCamera,
|
||||
setUnsavedChanges,
|
||||
}: CameraSettingsViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -81,7 +82,7 @@ export default function CameraSettingsView({
|
||||
.map((label) => t(label, { ns: "objects" }))
|
||||
.join(", ")
|
||||
: "";
|
||||
}, [cameraConfig]);
|
||||
}, [cameraConfig, t]);
|
||||
|
||||
const detectionsLabels = useMemo(() => {
|
||||
return cameraConfig?.review.detections.labels
|
||||
@ -89,7 +90,7 @@ export default function CameraSettingsView({
|
||||
.map((label) => t(label, { ns: "objects" }))
|
||||
.join(", ")
|
||||
: "";
|
||||
}, [cameraConfig]);
|
||||
}, [cameraConfig, t]);
|
||||
|
||||
// form
|
||||
|
||||
@ -190,7 +191,7 @@ export default function CameraSettingsView({
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[updateConfig, setIsLoading, selectedCamera, cameraConfig],
|
||||
[updateConfig, setIsLoading, selectedCamera, cameraConfig, t],
|
||||
);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
@ -259,13 +260,13 @@ export default function CameraSettingsView({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">camera.title</Trans>
|
||||
{t("camera.title")}
|
||||
</Heading>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">camera.streams.title</Trans>
|
||||
{t("camera.streams.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
@ -278,18 +279,16 @@ export default function CameraSettingsView({
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="camera-enabled">
|
||||
<Trans>button.enabled</Trans>
|
||||
</Label>
|
||||
<Label htmlFor="camera-enabled">{t("button.enabled")}</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">camera.streams.desc</Trans>
|
||||
{t("camera.streams.desc")}
|
||||
</div>
|
||||
<Separator className="mb-2 mt-4 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">camera.review.title</Trans>
|
||||
{t("camera.review.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 space-y-3 text-sm text-primary-variant">
|
||||
@ -304,7 +303,7 @@ export default function CameraSettingsView({
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="alerts-enabled">
|
||||
<Trans ns="views/settings">camera.review.alerts</Trans>
|
||||
{t("camera.review.alerts")}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
@ -320,12 +319,12 @@ export default function CameraSettingsView({
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="detections-enabled">
|
||||
<Trans ns="views/settings">camera.review.detections</Trans>
|
||||
{t("camera.review.detections")}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">camera.review.desc</Trans>
|
||||
{t("camera.review.desc")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -333,16 +332,12 @@ export default function CameraSettingsView({
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">camera.reviewClassification.title</Trans>
|
||||
{t("camera.reviewClassification.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="max-w-6xl">
|
||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
camera.reviewClassification.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("camera.reviewClassification.desc")}</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/review"
|
||||
@ -350,9 +345,7 @@ export default function CameraSettingsView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
camera.reviewClassification.readTheDocumentation
|
||||
</Trans>{" "}
|
||||
{t("camera.reviewClassification.readTheDocumentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -381,15 +374,13 @@ export default function CameraSettingsView({
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<FormLabel className="flex flex-row items-center text-base">
|
||||
<Trans ns="views/settings">
|
||||
camera.review.alerts
|
||||
</Trans>
|
||||
{t("camera.review.alerts")}
|
||||
<MdCircle className="ml-3 size-2 text-severity_alert" />
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
camera.reviewClassification.selectAlertsZones
|
||||
</Trans>
|
||||
{t(
|
||||
"camera.reviewClassification.selectAlertsZones",
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
|
||||
@ -438,9 +429,7 @@ export default function CameraSettingsView({
|
||||
</>
|
||||
) : (
|
||||
<div className="font-normal text-destructive">
|
||||
<Trans ns="views/settings">
|
||||
camera.reviewClassification.noDefinedZones
|
||||
</Trans>
|
||||
{t("camera.reviewClassification.noDefinedZones")}
|
||||
</div>
|
||||
)}
|
||||
<FormMessage />
|
||||
@ -461,7 +450,6 @@ export default function CameraSettingsView({
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
ns: "views/settings",
|
||||
},
|
||||
)
|
||||
: t("camera.reviewClassification.objectAlertsTips", {
|
||||
@ -469,7 +457,6 @@ export default function CameraSettingsView({
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
ns: "views/settings",
|
||||
})}
|
||||
</div>
|
||||
</FormItem>
|
||||
@ -485,16 +472,14 @@ export default function CameraSettingsView({
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<FormLabel className="flex flex-row items-center text-base">
|
||||
<Trans ns="views/settings">
|
||||
camera.review.detections
|
||||
</Trans>
|
||||
{t("camera.review.detections")}
|
||||
<MdCircle className="ml-3 size-2 text-severity_detection" />
|
||||
</FormLabel>
|
||||
{selectDetections && (
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
camera.reviewClassification.selectDetectionsZones
|
||||
</Trans>
|
||||
{t(
|
||||
"camera.reviewClassification.selectDetectionsZones",
|
||||
)}
|
||||
</FormDescription>
|
||||
)}
|
||||
</div>
|
||||
@ -557,9 +542,9 @@ export default function CameraSettingsView({
|
||||
htmlFor="select-detections"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
camera.reviewClassification.limitDetections
|
||||
</Trans>
|
||||
{t(
|
||||
"camera.reviewClassification.limitDetections",
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -568,58 +553,51 @@ export default function CameraSettingsView({
|
||||
|
||||
<div className="text-sm">
|
||||
{watchedDetectionsZones &&
|
||||
watchedDetectionsZones.length > 0 ? (
|
||||
!selectDetections ? (
|
||||
<Trans
|
||||
i18nKey="camera.reviewClassification.zoneObjectDetectionsTips"
|
||||
values={{
|
||||
watchedDetectionsZones.length > 0
|
||||
? !selectDetections
|
||||
? t(
|
||||
"camera.reviewClassification.zoneObjectDetectionsTips",
|
||||
{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
},
|
||||
)
|
||||
: t(
|
||||
"camera.reviewClassification.zoneObjectDetectionsTips.notSelectDetections",
|
||||
{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
},
|
||||
)
|
||||
: t(
|
||||
"camera.reviewClassification.objectDetectionsTips",
|
||||
{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
}}
|
||||
ns="views/settings"
|
||||
></Trans>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="camera.reviewClassification.zoneObjectDetectionsTips.notSelectDetections"
|
||||
values={{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
}}
|
||||
ns="views/settings"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="camera.reviewClassification.objectDetectionsTips"
|
||||
values={{
|
||||
detectionsLabels,
|
||||
cameraName: capitalizeFirstLetter(
|
||||
cameraConfig?.name ?? "",
|
||||
).replaceAll("_", " "),
|
||||
}}
|
||||
ns="views/settings"
|
||||
/>
|
||||
)}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -634,7 +612,7 @@ export default function CameraSettingsView({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -646,12 +624,10 @@ export default function CameraSettingsView({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -37,8 +37,9 @@ import PolygonItem from "@/components/settings/PolygonItem";
|
||||
import { Link } from "react-router-dom";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MasksAndZoneViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -51,6 +52,7 @@ export default function MasksAndZonesView({
|
||||
selectedZoneMask,
|
||||
setUnsavedChanges,
|
||||
}: MasksAndZoneViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [allPolygons, setAllPolygons] = useState<Polygon[]>([]);
|
||||
const [editingPolygons, setEditingPolygons] = useState<Polygon[]>([]);
|
||||
@ -481,7 +483,7 @@ export default function MasksAndZonesView({
|
||||
{editPane === undefined && (
|
||||
<>
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">menu.masksAndZones</Trans>
|
||||
{t("menu.masksAndZones")}
|
||||
</Heading>
|
||||
<div className="flex w-full flex-col">
|
||||
{(selectedZoneMask === undefined ||
|
||||
@ -491,18 +493,12 @@ export default function MasksAndZonesView({
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="text-md cursor-default">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.label
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.label")}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("masksAndZones.zones.desc")}</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/zones"
|
||||
@ -510,9 +506,7 @@ export default function MasksAndZonesView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.desc.documentation
|
||||
</Trans>{" "}
|
||||
{t("masksAndZones.zones.desc.documentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -534,9 +528,7 @@ export default function MasksAndZonesView({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.zones.add
|
||||
</Trans>
|
||||
{t("masksAndZones.zones.add")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -567,18 +559,12 @@ export default function MasksAndZonesView({
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="text-md cursor-default">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.label
|
||||
</Trans>
|
||||
{t("masksAndZones.motionMasks.label")}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("masksAndZones.motionMasks.desc")}</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/masks#motion-masks"
|
||||
@ -586,9 +572,9 @@ export default function MasksAndZonesView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.desc.documentation
|
||||
</Trans>{" "}
|
||||
{t(
|
||||
"masksAndZones.motionMasks.desc.documentation",
|
||||
)}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -610,9 +596,7 @@ export default function MasksAndZonesView({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.motionMasks.add
|
||||
</Trans>
|
||||
{t("masksAndZones.motionMasks.add")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -645,18 +629,12 @@ export default function MasksAndZonesView({
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="text-md cursor-default">
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.label
|
||||
</Trans>
|
||||
{t("masksAndZones.objectMasks.label")}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="my-2 flex flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("masksAndZones.objectMasks.desc")}</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/masks#object-filter-masks"
|
||||
@ -664,9 +642,7 @@ export default function MasksAndZonesView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.documentation
|
||||
</Trans>{" "}
|
||||
{t("masksAndZones.objectMasks.documentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -688,9 +664,7 @@ export default function MasksAndZonesView({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans ns="views/settings">
|
||||
masksAndZones.objectMasks.add
|
||||
</Trans>
|
||||
{t("masksAndZones.objectMasks.add")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@ -21,8 +21,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MotionTunerViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -39,6 +38,7 @@ export default function MotionTunerView({
|
||||
selectedCamera,
|
||||
setUnsavedChanges,
|
||||
}: MotionTunerViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
const [changedValue, setChangedValue] = useState(false);
|
||||
@ -119,12 +119,9 @@ export default function MotionTunerView({
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
toast.success(
|
||||
t("motionDetectionTuner.toast.success", { ns: "views/settings" }),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
toast.success(t("motionDetectionTuner.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setChangedValue(false);
|
||||
updateConfig();
|
||||
} else {
|
||||
@ -150,6 +147,7 @@ export default function MotionTunerView({
|
||||
motionSettings.contour_area,
|
||||
motionSettings.improve_contrast,
|
||||
selectedCamera,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
@ -186,12 +184,10 @@ export default function MotionTunerView({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">motionDetectionTuner.title</Trans>
|
||||
{t("motionDetectionTuner.title")}
|
||||
</Heading>
|
||||
<div className="my-3 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">motionDetectionTuner.desc</Trans>
|
||||
</p>
|
||||
<p>{t("motionDetectionTuner.desc")}</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -200,9 +196,7 @@ export default function MotionTunerView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.desc.documentation
|
||||
</Trans>{" "}
|
||||
{t("motionDetectionTuner.desc.documentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -212,16 +206,10 @@ export default function MotionTunerView({
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="motion-threshold" className="text-md">
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.Threshold
|
||||
</Trans>
|
||||
{t("motionDetectionTuner.Threshold")}
|
||||
</Label>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.Threshold.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("motionDetectionTuner.Threshold.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
@ -245,16 +233,10 @@ export default function MotionTunerView({
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="motion-threshold" className="text-md">
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.contourArea
|
||||
</Trans>
|
||||
{t("motionDetectionTuner.contourArea")}
|
||||
</Label>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.contourArea.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("motionDetectionTuner.contourArea.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
@ -279,14 +261,10 @@ export default function MotionTunerView({
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="improve-contrast">
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.improveContrast
|
||||
</Trans>
|
||||
{t("motionDetectionTuner.improveContrast")}
|
||||
</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
motionDetectionTuner.improveContrast.desc
|
||||
</Trans>
|
||||
{t("motionDetectionTuner.improveContrast.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -307,7 +285,7 @@ export default function MotionTunerView({
|
||||
aria-label="Reset"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -319,12 +297,10 @@ export default function MotionTunerView({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -18,10 +18,10 @@ import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { LuCheck, LuExternalLink, LuX } from "react-icons/lu";
|
||||
import { CiCircleAlert } from "react-icons/ci";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -43,6 +43,7 @@ import {
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import FilterSwitch from "@/components/filter/FilterSwitch";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const NOTIFICATION_SERVICE_WORKER = "notifications-worker.js";
|
||||
|
||||
@ -58,6 +59,8 @@ type NotificationsSettingsViewProps = {
|
||||
export default function NotificationView({
|
||||
setUnsavedChanges,
|
||||
}: NotificationsSettingsViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
|
||||
const { data: config, mutate: updateConfig } = useSWR<FrigateConfig>(
|
||||
"config",
|
||||
{
|
||||
@ -351,16 +354,12 @@ export default function NotificationView({
|
||||
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">
|
||||
notification.notificationSettings
|
||||
</Trans>
|
||||
{t("notification.notificationSettings")}
|
||||
</Heading>
|
||||
|
||||
<div className="max-w-6xl">
|
||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans ns="views/settings">notification.desc</Trans>
|
||||
</p>
|
||||
<p>{t("notification.desc")}</p>
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/notifications"
|
||||
@ -368,9 +367,7 @@ export default function NotificationView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
notification.documentation
|
||||
</Trans>{" "}
|
||||
{t("notification.documentation")}{" "}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -387,22 +384,16 @@ export default function NotificationView({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans ns="views/settings">notification.email</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel>{t("notification.email")}</FormLabel>
|
||||
<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] md:w-72"
|
||||
placeholder={t("notification.email.placeholder", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
placeholder={t("notification.email.placeholder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
notification.email.desc
|
||||
</Trans>
|
||||
{t("notification.email.desc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -418,9 +409,7 @@ export default function NotificationView({
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<FormLabel className="flex flex-row items-center text-base">
|
||||
<Trans ns="views/settings">
|
||||
notification.cameras
|
||||
</Trans>
|
||||
{t("notification.cameras")}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
||||
@ -470,17 +459,13 @@ export default function NotificationView({
|
||||
</>
|
||||
) : (
|
||||
<div className="font-normal text-destructive">
|
||||
<Trans ns="views/settings">
|
||||
notification.cameras.noCameras
|
||||
</Trans>
|
||||
{t("notification.cameras.noCameras")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
<Trans ns="views/settings">
|
||||
notification.cameras.desc
|
||||
</Trans>
|
||||
{t("notification.cameras.desc")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -493,7 +478,7 @@ export default function NotificationView({
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<Trans>button.cancel</Trans>
|
||||
{t("button.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -505,12 +490,10 @@ export default function NotificationView({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@ -523,9 +506,7 @@ export default function NotificationView({
|
||||
<div className="flex flex-col gap-2 md:max-w-[50%]">
|
||||
<Separator className="my-2 flex bg-secondary md:hidden" />
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">
|
||||
notification.deviceSpecific
|
||||
</Trans>
|
||||
{t("notification.deviceSpecific")}
|
||||
</Heading>
|
||||
<Button
|
||||
aria-label="Register or unregister notifications for this device"
|
||||
@ -569,12 +550,8 @@ export default function NotificationView({
|
||||
}}
|
||||
>
|
||||
{registration != null
|
||||
? t("notification.unregisterDevice", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("notification.registerDevice", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
? t("notification.unregisterDevice")
|
||||
: t("notification.registerDevice")}
|
||||
</Button>
|
||||
{registration != null && registration.active && (
|
||||
<Button
|
||||
@ -634,6 +611,7 @@ export function CameraNotificationSwitch({
|
||||
config,
|
||||
camera,
|
||||
}: CameraNotificationSwitchProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { payload: notificationState, send: sendNotification } =
|
||||
useNotifications(camera);
|
||||
const { payload: notificationSuspendUntil, send: sendNotificationSuspend } =
|
||||
|
||||
@ -23,11 +23,11 @@ import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { LuExternalLink, LuInfo } from "react-icons/lu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ObjectSettingsViewProps = {
|
||||
selectedCamera?: string;
|
||||
@ -40,6 +40,8 @@ const emptyObject = Object.freeze({});
|
||||
export default function ObjectSettingsView({
|
||||
selectedCamera,
|
||||
}: ObjectSettingsViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -47,55 +49,45 @@ export default function ObjectSettingsView({
|
||||
const DEBUG_OPTIONS = [
|
||||
{
|
||||
param: "bbox",
|
||||
title: t("debug.boundingBoxes.title", { ns: "views/settings" }),
|
||||
description: t("debug.boundingBoxes.desc", { ns: "views/settings" }),
|
||||
title: t("debug.boundingBoxes.title"),
|
||||
description: t("debug.boundingBoxes.desc"),
|
||||
info: (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
<strong>
|
||||
<Trans ns="views/settings">debug.boundingBoxes.colors</Trans>
|
||||
</strong>
|
||||
<strong>{t("debug.boundingBoxes.colors")}</strong>
|
||||
</p>
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
<Trans ns="views/settings">debug.boundingBoxes.colors.info</Trans>
|
||||
{t("debug.boundingBoxes.colors.info")}
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
param: "timestamp",
|
||||
title: t("debug.timestamp.title", { ns: "views/settings" }),
|
||||
description: t("debug.timestamp.desc", { ns: "views/settings" }),
|
||||
title: t("debug.timestamp.title"),
|
||||
description: t("debug.timestamp.desc"),
|
||||
},
|
||||
{
|
||||
param: "zones",
|
||||
title: t("debug.zones.title", { ns: "views/settings" }),
|
||||
description: t("debug.zones.desc", { ns: "views/settings" }),
|
||||
title: t("debug.zones.title"),
|
||||
description: t("debug.zones.desc"),
|
||||
},
|
||||
{
|
||||
param: "mask",
|
||||
title: t("debug.mask.title", { ns: "views/settings" }),
|
||||
description: t("debug.mask.desc", { ns: "views/settings" }),
|
||||
title: t("debug.mask.title"),
|
||||
description: t("debug.mask.desc"),
|
||||
},
|
||||
{
|
||||
param: "motion",
|
||||
title: t("debug.motion.title", { ns: "views/settings" }),
|
||||
description: t("debug.motion.desc", { ns: "views/settings" }),
|
||||
info: (
|
||||
<>
|
||||
<Trans ns="views/settings">debug.motion.tips</Trans>
|
||||
</>
|
||||
),
|
||||
title: t("debug.motion.title"),
|
||||
description: t("debug.motion.desc"),
|
||||
info: <>{t("debug.motion.tips")}</>,
|
||||
},
|
||||
{
|
||||
param: "regions",
|
||||
title: t("debug.regions.title", { ns: "views/settings" }),
|
||||
description: t("debug.regions.desc", { ns: "views/settings" }),
|
||||
info: (
|
||||
<>
|
||||
<Trans ns="views/settings">debug.regions.tips</Trans>
|
||||
</>
|
||||
),
|
||||
title: t("debug.regions.title"),
|
||||
description: t("debug.regions.desc"),
|
||||
info: <>{t("debug.regions.tips")}</>,
|
||||
},
|
||||
];
|
||||
|
||||
@ -156,7 +148,7 @@ export default function ObjectSettingsView({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">debug.title</Trans>
|
||||
{t("debug.title")}
|
||||
</Heading>
|
||||
<div className="mb-5 space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
@ -166,12 +158,9 @@ export default function ObjectSettingsView({
|
||||
.map((detector) => capitalizeFirstLetter(detector))
|
||||
.join(",")
|
||||
: "",
|
||||
ns: "views/settings",
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
<Trans ns="views/settings">debug.desc</Trans>
|
||||
</p>
|
||||
<p>{t("debug.desc")}</p>
|
||||
</div>
|
||||
{config?.cameras[cameraConfig.name]?.webui_url && (
|
||||
<div className="mb-5 text-sm text-muted-foreground">
|
||||
@ -191,11 +180,9 @@ export default function ObjectSettingsView({
|
||||
|
||||
<Tabs defaultValue="debug" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="debug">
|
||||
<Trans ns="views/settings">debug.debugging</Trans>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="debug">{t("debug.debugging")}</TabsTrigger>
|
||||
<TabsTrigger value="objectlist">
|
||||
<Trans ns="views/settings">debug.objectList</Trans>
|
||||
{t("debug.objectList")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="debug">
|
||||
@ -255,9 +242,7 @@ export default function ObjectSettingsView({
|
||||
className="mb-0 cursor-pointer capitalize text-primary"
|
||||
htmlFor="debugdraw"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
debug.objectShapeFilterDrawing.title
|
||||
</Trans>
|
||||
{t("debug.objectShapeFilterDrawing.title")}
|
||||
</Label>
|
||||
|
||||
<Popover>
|
||||
@ -265,14 +250,12 @@ export default function ObjectSettingsView({
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">
|
||||
<Trans>button.info</Trans>
|
||||
{t("button.info")}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-sm">
|
||||
<Trans ns="views/settings">
|
||||
debug.objectShapeFilterDrawing.tips
|
||||
</Trans>
|
||||
{t("debug.objectShapeFilterDrawing.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/object_filters#object-shape"
|
||||
@ -280,9 +263,7 @@ export default function ObjectSettingsView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
debug.objectShapeFilterDrawing.document
|
||||
</Trans>
|
||||
{t("debug.objectShapeFilterDrawing.document")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -290,9 +271,7 @@ export default function ObjectSettingsView({
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
debug.objectShapeFilterDrawing.desc
|
||||
</Trans>
|
||||
{t("debug.objectShapeFilterDrawing.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -348,6 +327,7 @@ type ObjectListProps = {
|
||||
};
|
||||
|
||||
function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const colormap = useMemo(() => {
|
||||
@ -441,9 +421,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-3 text-center">
|
||||
<Trans ns="views/settings">debug.noObjects</Trans>
|
||||
</div>
|
||||
<div className="p-3 text-center">{t("debug.noObjects")}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -20,8 +20,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ExploreSettingsViewProps = {
|
||||
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -36,6 +35,7 @@ type ExploreSettings = {
|
||||
export default function ExploreSettingsView({
|
||||
setUnsavedChanges,
|
||||
}: ExploreSettingsViewProps) {
|
||||
const { t } = useTranslation("views/settings");
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
const [changedValue, setChangedValue] = useState(false);
|
||||
@ -127,6 +127,7 @@ export default function ExploreSettingsView({
|
||||
ExploreSettings.enabled,
|
||||
ExploreSettings.reindex,
|
||||
ExploreSettings.model_size,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
@ -163,17 +164,15 @@ export default function ExploreSettingsView({
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">explore.title</Trans>
|
||||
{t("explore.title")}
|
||||
</Heading>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">explore.semanticSearch.title</Trans>
|
||||
{t("explore.semanticSearch.title")}
|
||||
</Heading>
|
||||
<div className="max-w-6xl">
|
||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
||||
<p>
|
||||
<Trans ns="views/settings">explore.semanticSearch.desc</Trans>
|
||||
</p>
|
||||
<p>{t("explore.semanticSearch.desc")}</p>
|
||||
|
||||
<div className="flex items-center text-primary">
|
||||
<Link
|
||||
@ -182,9 +181,7 @@ export default function ExploreSettingsView({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.readTheDocumentation
|
||||
</Trans>
|
||||
{t("explore.semanticSearch.readTheDocumentation")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -203,9 +200,7 @@ export default function ExploreSettingsView({
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="enabled">
|
||||
<Trans>button.enabled</Trans>
|
||||
</Label>
|
||||
<Label htmlFor="enabled">{t("button.enabled")}</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@ -221,42 +216,24 @@ export default function ExploreSettingsView({
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="reindex">
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.reindexOnStartup.label
|
||||
</Trans>
|
||||
{t("explore.semanticSearch.reindexOnStartup.label")}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.reindexOnStartup.desc
|
||||
</Trans>
|
||||
{t("explore.semanticSearch.reindexOnStartup.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.modelSize.label
|
||||
</Trans>
|
||||
{t("explore.semanticSearch.modelSize.label")}
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.modelSize.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("explore.semanticSearch.modelSize.desc")}</p>
|
||||
<ul className="list-disc pl-5 text-sm">
|
||||
<li>
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.modelSize.small.desc
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans ns="views/settings">
|
||||
explore.semanticSearch.modelSize.large.desc
|
||||
</Trans>
|
||||
</li>
|
||||
<li>{t("explore.semanticSearch.modelSize.small.desc")}</li>
|
||||
<li>{t("explore.semanticSearch.modelSize.large.desc")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -272,7 +249,6 @@ export default function ExploreSettingsView({
|
||||
{t(
|
||||
"explore.semanticSearch.modelSize." +
|
||||
ExploreSettings.model_size,
|
||||
{ ns: "views/settings" },
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -283,9 +259,7 @@ export default function ExploreSettingsView({
|
||||
className="cursor-pointer"
|
||||
value={size}
|
||||
>
|
||||
{t("explore.semanticSearch.modelSize." + size, {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
{t("explore.semanticSearch.modelSize." + size)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@ -297,7 +271,7 @@ export default function ExploreSettingsView({
|
||||
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
|
||||
<Trans>button.reset</Trans>
|
||||
{t("button.reset")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
@ -309,12 +283,10 @@ export default function ExploreSettingsView({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>
|
||||
<Trans>button.saving</Trans>
|
||||
</span>
|
||||
<span>{t("button.saving")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>button.save</Trans>
|
||||
t("button.save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -18,15 +18,14 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "../../components/ui/select";
|
||||
import { Trans } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
||||
const WEEK_STARTS_ON = ["Sunday", "Monday"];
|
||||
|
||||
export default function UiSettingsView() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const { t } = useTranslation("views/settings");
|
||||
const clearStoredLayouts = useCallback(() => {
|
||||
if (!config) {
|
||||
return [];
|
||||
@ -90,13 +89,13 @@ export default function UiSettingsView() {
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||
<Heading as="h3" className="my-2">
|
||||
<Trans ns="views/settings">general.title</Trans>
|
||||
{t("general.title")}
|
||||
</Heading>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">general.liveDashboard.title</Trans>
|
||||
{t("general.liveDashboard.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
@ -108,17 +107,11 @@ export default function UiSettingsView() {
|
||||
onCheckedChange={setAutoLive}
|
||||
/>
|
||||
<Label className="cursor-pointer" htmlFor="auto-live">
|
||||
<Trans ns="views/settings">
|
||||
general.liveDashboard.automaticLiveView.label
|
||||
</Trans>
|
||||
{t("general.liveDashboard.automaticLiveView.label")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
general.liveDashboard.automaticLiveView.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("general.liveDashboard.automaticLiveView.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
@ -129,17 +122,11 @@ export default function UiSettingsView() {
|
||||
onCheckedChange={setAlertVideos}
|
||||
/>
|
||||
<Label className="cursor-pointer" htmlFor="images-only">
|
||||
<Trans ns="views/settings">
|
||||
general.liveDashboard.playAlertVideos.label
|
||||
</Trans>
|
||||
{t("general.liveDashboard.playAlertVideos.label")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
general.liveDashboard.playAlertVideos.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("general.liveDashboard.playAlertVideos.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -148,69 +135,51 @@ export default function UiSettingsView() {
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="views/settings">general.storedLayouts.title</Trans>
|
||||
{t("general.storedLayouts.title")}
|
||||
</div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
general.storedLayouts.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("general.storedLayouts.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Clear all saved layouts"
|
||||
onClick={clearStoredLayouts}
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
general.storedLayouts.clearAll
|
||||
</Trans>
|
||||
{t("general.storedLayouts.clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="views/settings">
|
||||
general.cameraGroupStreaming.title
|
||||
</Trans>
|
||||
{t("general.cameraGroupStreaming.title")}
|
||||
</div>
|
||||
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
general.cameraGroupStreaming.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("general.cameraGroupStreaming.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Clear all group streaming settings"
|
||||
onClick={clearStreamingSettings}
|
||||
>
|
||||
<Trans ns="views/settings">
|
||||
general.cameraGroupStreaming.clearAll
|
||||
</Trans>
|
||||
{t("general.cameraGroupStreaming.clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">general.recordingsViewer.title</Trans>
|
||||
{t("general.recordingsViewer.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="views/settings">
|
||||
general.recordingsViewer.defaultPlaybackRate.label
|
||||
</Trans>
|
||||
{t("general.recordingsViewer.defaultPlaybackRate.label")}
|
||||
</div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
general.recordingsViewer.defaultPlaybackRate.desc
|
||||
</Trans>
|
||||
{t("general.recordingsViewer.defaultPlaybackRate.desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -239,22 +208,16 @@ export default function UiSettingsView() {
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">general.calendar.title</Trans>
|
||||
{t("general.calendar.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">
|
||||
<Trans ns="views/settings">
|
||||
general.calendar.firstWeekday.label
|
||||
</Trans>
|
||||
{t("general.calendar.firstWeekday.label")}
|
||||
</div>
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
general.calendar.firstWeekday.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("general.calendar.firstWeekday.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -266,7 +229,6 @@ export default function UiSettingsView() {
|
||||
{t(
|
||||
"general.calendar.firstWeekday." +
|
||||
WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
|
||||
{ ns: "views/settings" },
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -277,9 +239,7 @@ export default function UiSettingsView() {
|
||||
className="cursor-pointer"
|
||||
value={index.toString()}
|
||||
>
|
||||
{t("general.calendar.firstWeekday." + day.toLowerCase(), {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
{t("general.calendar.firstWeekday." + day.toLowerCase())}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraMetricsProps = {
|
||||
lastUpdated: number;
|
||||
@ -23,7 +23,7 @@ export default function CameraMetrics({
|
||||
setLastUpdated,
|
||||
}: CameraMetricsProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
// camera info dialog
|
||||
|
||||
const [showCameraInfoDialog, setShowCameraInfoDialog] = useState(false);
|
||||
@ -225,14 +225,12 @@ export default function CameraMetrics({
|
||||
return (
|
||||
<div className="scrollbar-container mt-4 flex size-full flex-col gap-3 overflow-y-auto">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
<Trans ns="views/system">cameras.overview</Trans>
|
||||
{t("cameras.overview")}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3">
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">cameras.framesAndDetections</Trans>
|
||||
</div>
|
||||
<div className="mb-5">{t("cameras.framesAndDetections")}</div>
|
||||
<CameraLineGraph
|
||||
graphId="overall-stats"
|
||||
unit=""
|
||||
@ -300,9 +298,7 @@ export default function CameraMetrics({
|
||||
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
cameras.framesAndDetections
|
||||
</Trans>
|
||||
{t("cameras.framesAndDetections")}
|
||||
</div>
|
||||
<CameraLineGraph
|
||||
graphId={`${camera.name}-dps`}
|
||||
|
||||
@ -15,7 +15,7 @@ import GPUInfoDialog from "@/components/overlay/GPUInfoDialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ThresholdBarGraph } from "@/components/graph/SystemGraph";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type GeneralMetricsProps = {
|
||||
lastUpdated: number;
|
||||
@ -26,7 +26,7 @@ export default function GeneralMetrics({
|
||||
setLastUpdated,
|
||||
}: GeneralMetricsProps) {
|
||||
// extra info
|
||||
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const [showVainfo, setShowVainfo] = useState(false);
|
||||
|
||||
// stats
|
||||
@ -449,7 +449,7 @@ export default function GeneralMetrics({
|
||||
|
||||
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
<Trans ns="views/system">general.detector.title</Trans>
|
||||
{t("general.detector.title")}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@ -459,9 +459,7 @@ export default function GeneralMetrics({
|
||||
>
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">general.detector.inferenceSpeed</Trans>
|
||||
</div>
|
||||
<div className="mb-5">{t("general.detector.inferenceSpeed")}</div>
|
||||
{detInferenceTimeSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -499,9 +497,7 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">general.detector.cpuUsage</Trans>
|
||||
</div>
|
||||
<div className="mb-5">{t("general.detector.cpuUsage")}</div>
|
||||
{detCpuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -519,9 +515,7 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">general.detector.memoryUsage</Trans>
|
||||
</div>
|
||||
<div className="mb-5">{t("general.detector.memoryUsage")}</div>
|
||||
{detMemSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
@ -552,7 +546,7 @@ export default function GeneralMetrics({
|
||||
size="sm"
|
||||
onClick={() => setShowVainfo(true)}
|
||||
>
|
||||
<Trans ns="views/system">general.hardwareInfo.title</Trans>
|
||||
{t("general.hardwareInfo.title")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -565,9 +559,7 @@ export default function GeneralMetrics({
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
general.hardwareInfo.gpuUsage
|
||||
</Trans>
|
||||
{t("general.hardwareInfo.gpuUsage")}
|
||||
</div>
|
||||
{gpuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
@ -589,9 +581,7 @@ export default function GeneralMetrics({
|
||||
{gpuMemSeries && (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
general.hardwareInfo.gpuMemroy
|
||||
</Trans>
|
||||
{t("general.hardwareInfo.gpuMemroy")}
|
||||
</div>
|
||||
{gpuMemSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
@ -615,9 +605,7 @@ export default function GeneralMetrics({
|
||||
{gpuEncSeries && gpuEncSeries?.length != 0 && (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
general.hardwareInfo.gpuEncoder
|
||||
</Trans>
|
||||
{t("general.hardwareInfo.gpuEncoder")}
|
||||
</div>
|
||||
{gpuEncSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
@ -641,9 +629,7 @@ export default function GeneralMetrics({
|
||||
{gpuDecSeries && gpuDecSeries?.length != 0 && (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
general.hardwareInfo.gpuDecoder
|
||||
</Trans>
|
||||
{t("general.hardwareInfo.gpuDecoder")}
|
||||
</div>
|
||||
{gpuDecSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
@ -667,15 +653,13 @@ export default function GeneralMetrics({
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||
<Trans ns="views/system">general.otherProcesses.title</Trans>
|
||||
{t("general.otherProcesses.title")}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
general.otherProcesses.processCpuUsage
|
||||
</Trans>
|
||||
{t("general.otherProcesses.processCpuUsage")}
|
||||
</div>
|
||||
{otherProcessCpuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
@ -695,9 +679,7 @@ export default function GeneralMetrics({
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
<Trans ns="views/system">
|
||||
general.otherProcesses.processMemoryUsage
|
||||
</Trans>
|
||||
{t("general.otherProcesses.processMemoryUsage")}
|
||||
</div>
|
||||
{otherProcessMemSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
|
||||
@ -8,12 +8,12 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import useSWR from "swr";
|
||||
import { Trans } from "react-i18next";
|
||||
import { CiCircleAlert } from "react-icons/ci";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { useTimezone } from "@/hooks/use-date-utils";
|
||||
import { RecordingsSummary } from "@/types/review";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CameraStorage = {
|
||||
[key: string]: {
|
||||
@ -34,7 +34,7 @@ export default function StorageMetrics({
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const timezone = useTimezone(config);
|
||||
|
||||
const totalStorage = useMemo(() => {
|
||||
@ -77,12 +77,12 @@ export default function StorageMetrics({
|
||||
return (
|
||||
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
<Trans ns="views/system">storage.overview</Trans>
|
||||
{t("storage.overview")}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5 flex flex-row items-center justify-between">
|
||||
<Trans ns="views/system">storage.recordings.title</Trans>
|
||||
{t("storage.recordings.title")}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
@ -96,9 +96,7 @@ export default function StorageMetrics({
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-2">
|
||||
<Trans ns="views/system">storage.recordings.tips</Trans>
|
||||
</div>
|
||||
<div className="space-y-2">{t("storage.recordings.tips")}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
@ -136,7 +134,7 @@ export default function StorageMetrics({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||
<Trans ns="views/system">storage.cameraStorage.title</Trans>
|
||||
{t("storage.cameraStorage.title")}
|
||||
</div>
|
||||
<div className="mt-4 bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<CombinedStorageGraph
|
||||
|
||||
Loading…
Reference in New Issue
Block a user