mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-10 00:57:38 +03:00
* Translation module init
* Add more i18n keys
* fix: fix string wrong
* refactor: use namespace translation file
* chore: add more translation key
* fix: fix some page name error
* refactor: change Trans tag for t function
* chore: fix some key not work
* chore: fix SearchFilterDialog i18n key error
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* chore: fix en i18n file filter missing some keys
* chore: add some i18n keys
* chore: add more i18n keys again
* feat: add search page i18n
* feat: add explore model i18n keys
* Update web/src/components/menu/GeneralSettings.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/components/menu/GeneralSettings.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/components/menu/GeneralSettings.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* feat: add more live i18n keys
* feat: add more search setting i18n keys
* fix: remove some comment
* fix: fix some setting page url error
* Update web/src/views/settings/SearchSettingsView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* fix: add system missing keys
* fix: update password update i18n keys
* chore: remove outdate translation.json file
* fix: fix exploreSettings error
* chore: add object setting i18n keys
* Update web/src/views/recording/RecordingView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/public/locales/en/components/filter.json
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/components/overlay/ExportDialog.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* feat: add more i18n keys
* fix: fix motionDetectionTuner html node
* feat: add more page i18n keys
* fix: cameraStream i18n keys error
* feat: add Player i18n keys
* feat: add more toast i18n keys
* feat: change explore setting name
* feat: add more document title i18n keys
* feat: add more search i18n keys
* fix: fix accessDenied i18n keys error
* chore: add objectType i18n
* chore: add inputWithTags i18n
* chore: add SearchFilterDialog i18n
* Update web/src/views/settings/ObjectSettingsView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/views/settings/ObjectSettingsView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/views/settings/ObjectSettingsView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/views/settings/ObjectSettingsView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* Update web/src/views/settings/ObjectSettingsView.tsx
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* chore: add some missing i18n keys
* chore: remove most import { t } from "i18next";
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
import { IconType } from "react-icons";
|
|
import * as LuIcons from "react-icons/lu";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { IoClose } from "react-icons/io5";
|
|
import Heading from "../ui/heading";
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "../ui/button";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
export type IconName = keyof typeof LuIcons;
|
|
|
|
export type IconElement = {
|
|
name?: string;
|
|
Icon?: IconType;
|
|
};
|
|
|
|
type IconPickerProps = {
|
|
selectedIcon?: IconElement;
|
|
setSelectedIcon?: React.Dispatch<
|
|
React.SetStateAction<IconElement | undefined>
|
|
>;
|
|
};
|
|
|
|
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("");
|
|
|
|
const iconSets = useMemo(() => [...Object.entries(LuIcons)], []);
|
|
|
|
const icons = useMemo(
|
|
() =>
|
|
iconSets.filter(
|
|
([name]) =>
|
|
name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
searchTerm === "",
|
|
),
|
|
[iconSets, searchTerm],
|
|
);
|
|
|
|
const handleIconSelect = useCallback(
|
|
({ name, Icon }: IconElement) => {
|
|
if (setSelectedIcon) {
|
|
setSelectedIcon({ name, Icon });
|
|
}
|
|
setSearchTerm("");
|
|
},
|
|
[setSelectedIcon],
|
|
);
|
|
|
|
return (
|
|
<div ref={containerRef}>
|
|
<Popover
|
|
open={open}
|
|
onOpenChange={(open) => {
|
|
setOpen(open);
|
|
}}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
{!selectedIcon?.name || !selectedIcon?.Icon ? (
|
|
<Button
|
|
className="mt-2 w-full text-muted-foreground"
|
|
aria-label={t("iconPicker.selectIcon")}
|
|
>
|
|
{t("iconPicker.selectIcon")}
|
|
</Button>
|
|
) : (
|
|
<div className="hover:cursor-pointer">
|
|
<div className="my-3 flex w-full flex-row items-center justify-between gap-2">
|
|
<div className="flex flex-row items-center gap-2">
|
|
<selectedIcon.Icon size={15} />
|
|
<div className="text-sm">
|
|
{selectedIcon.name
|
|
.replace(/^Lu/, "")
|
|
.replace(/([A-Z])/g, " $1")}
|
|
</div>
|
|
</div>
|
|
|
|
<IoClose
|
|
className="mx-2 hover:cursor-pointer"
|
|
onClick={() => {
|
|
handleIconSelect({ name: undefined, Icon: undefined });
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
align="start"
|
|
side="top"
|
|
container={containerRef.current}
|
|
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">{t("iconPicker.selectIcon")}</Heading>
|
|
<span tabIndex={0} className="sr-only" />
|
|
<IoClose
|
|
size={15}
|
|
className="hover:cursor-pointer"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
<Input
|
|
type="text"
|
|
placeholder={t("iconPicker.search.placeholder", {
|
|
ns: "components/icons",
|
|
})}
|
|
className="text-md mb-3 md:text-sm"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
<div className="scrollbar-container flex h-full flex-col overflow-y-auto">
|
|
<div className="grid grid-cols-6 gap-2 pr-1">
|
|
{icons.map(([name, Icon]) => (
|
|
<div
|
|
key={name}
|
|
className={cn(
|
|
"flex flex-row items-center justify-center rounded-lg p-1 hover:cursor-pointer",
|
|
selectedIcon?.name === name
|
|
? "bg-selected text-white"
|
|
: "hover:bg-secondary-foreground",
|
|
)}
|
|
>
|
|
<Icon
|
|
className="size-6"
|
|
onClick={() => {
|
|
handleIconSelect({ name, Icon });
|
|
setOpen(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type IconRendererProps = {
|
|
icon: IconType;
|
|
size?: number;
|
|
className?: string;
|
|
};
|
|
|
|
export function IconRenderer({ icon, size, className }: IconRendererProps) {
|
|
return <>{React.createElement(icon, { size, className })}</>;
|
|
}
|