mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-19 06:38:21 +03:00
add audio label switches and input to filter list
This commit is contained in:
parent
95d9b01771
commit
ed5596d50d
@ -1,27 +1,42 @@
|
|||||||
// Object Label Switches Widget - For selecting objects via switches
|
// Audio Label Switches Widget - For selecting audio labels via switches
|
||||||
import type { WidgetProps } from "@rjsf/utils";
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
import { SwitchesWidget } from "./SwitchesWidget";
|
import { SwitchesWidget } from "./SwitchesWidget";
|
||||||
import type { FormContext } from "./SwitchesWidget";
|
import type { FormContext } from "./SwitchesWidget";
|
||||||
import { getTranslatedLabel } from "@/utils/i18n";
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { JsonObject } from "@/types/configForm";
|
||||||
|
|
||||||
function getAudioLabels(context: FormContext): string[] {
|
function getEnabledAudioLabels(context: FormContext): string[] {
|
||||||
let cameraLabels: string[] = [];
|
let cameraLabels: string[] = [];
|
||||||
let globalLabels: string[] = [];
|
let globalLabels: string[] = [];
|
||||||
|
|
||||||
if (context) {
|
if (context) {
|
||||||
// context.cameraValue and context.globalValue should be the entire objects section
|
// context.cameraValue and context.globalValue should be the entire audio section
|
||||||
const trackValue = context.cameraValue?.listen;
|
if (
|
||||||
if (Array.isArray(trackValue)) {
|
context.cameraValue &&
|
||||||
cameraLabels = trackValue.filter(
|
typeof context.cameraValue === "object" &&
|
||||||
(item): item is string => typeof item === "string",
|
!Array.isArray(context.cameraValue)
|
||||||
);
|
) {
|
||||||
|
const listenValue = (context.cameraValue as JsonObject).listen;
|
||||||
|
if (Array.isArray(listenValue)) {
|
||||||
|
cameraLabels = listenValue.filter(
|
||||||
|
(item): item is string => typeof item === "string",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalTrackValue = context.globalValue?.listen;
|
if (
|
||||||
if (Array.isArray(globalTrackValue)) {
|
context.globalValue &&
|
||||||
globalLabels = globalTrackValue.filter(
|
typeof context.globalValue === "object" &&
|
||||||
(item): item is string => typeof item === "string",
|
!Array.isArray(context.globalValue)
|
||||||
);
|
) {
|
||||||
|
const globalListenValue = (context.globalValue as JsonObject).listen;
|
||||||
|
if (Array.isArray(globalListenValue)) {
|
||||||
|
globalLabels = globalListenValue.filter(
|
||||||
|
(item): item is string => typeof item === "string",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,14 +49,51 @@ function getAudioLabelDisplayName(label: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AudioLabelSwitchesWidget(props: WidgetProps) {
|
export function AudioLabelSwitchesWidget(props: WidgetProps) {
|
||||||
|
const { data: audioLabels } = useSWR<Record<string, string>>("/audio_labels");
|
||||||
|
|
||||||
|
const allLabels = useMemo(() => {
|
||||||
|
if (!audioLabels) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelSet = new Set<string>();
|
||||||
|
Object.values(audioLabels).forEach((label) => {
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = label.trim();
|
||||||
|
if (normalized) {
|
||||||
|
labelSet.add(normalized);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...labelSet].sort();
|
||||||
|
}, [audioLabels]);
|
||||||
|
|
||||||
|
const getEntities = useCallback(
|
||||||
|
(context: FormContext) => {
|
||||||
|
const enabledLabels = getEnabledAudioLabels(context);
|
||||||
|
|
||||||
|
if (allLabels.length === 0) {
|
||||||
|
return enabledLabels;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedLabels = new Set([...allLabels, ...enabledLabels]);
|
||||||
|
return [...combinedLabels].sort();
|
||||||
|
},
|
||||||
|
[allLabels],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwitchesWidget
|
<SwitchesWidget
|
||||||
{...props}
|
{...props}
|
||||||
options={{
|
options={{
|
||||||
...props.options,
|
...props.options,
|
||||||
getEntities: getAudioLabels,
|
getEntities,
|
||||||
getDisplayLabel: getAudioLabelDisplayName,
|
getDisplayLabel: getAudioLabelDisplayName,
|
||||||
i18nKey: "audioLabels",
|
i18nKey: "audioLabels",
|
||||||
|
listClassName: "max-h-64 overflow-y-auto scrollbar-container",
|
||||||
|
enableSearch: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { WidgetProps } from "@rjsf/utils";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
@ -11,10 +12,16 @@ import {
|
|||||||
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
|
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
|
||||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { ConfigFormContext } from "@/types/configForm";
|
import { ConfigFormContext } from "@/types/configForm";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type FormContext = Pick<
|
type FormContext = Pick<
|
||||||
ConfigFormContext,
|
ConfigFormContext,
|
||||||
"cameraValue" | "globalValue" | "fullCameraConfig" | "fullConfig" | "t"
|
| "cameraValue"
|
||||||
|
| "globalValue"
|
||||||
|
| "fullCameraConfig"
|
||||||
|
| "fullConfig"
|
||||||
|
| "t"
|
||||||
|
| "level"
|
||||||
> & {
|
> & {
|
||||||
fullCameraConfig?: CameraConfig;
|
fullCameraConfig?: CameraConfig;
|
||||||
fullConfig?: FrigateConfig;
|
fullConfig?: FrigateConfig;
|
||||||
@ -31,6 +38,10 @@ export type SwitchesWidgetOptions = {
|
|||||||
i18nKey: string;
|
i18nKey: string;
|
||||||
/** Translation namespace (default: "views/settings") */
|
/** Translation namespace (default: "views/settings") */
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
|
/** Optional class name for the list container */
|
||||||
|
listClassName?: string;
|
||||||
|
/** Enable search input to filter the list */
|
||||||
|
enableSearch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeValue(value: unknown): string[] {
|
function normalizeValue(value: unknown): string[] {
|
||||||
@ -100,8 +111,30 @@ export function SwitchesWidget(props: WidgetProps) {
|
|||||||
[props.options],
|
[props.options],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const listClassName = useMemo(
|
||||||
|
() => props.options?.listClassName as string | undefined,
|
||||||
|
[props.options],
|
||||||
|
);
|
||||||
|
|
||||||
|
const enableSearch = useMemo(
|
||||||
|
() => props.options?.enableSearch as boolean | undefined,
|
||||||
|
[props.options],
|
||||||
|
);
|
||||||
|
|
||||||
const selectedEntities = useMemo(() => normalizeValue(value), [value]);
|
const selectedEntities = useMemo(() => normalizeValue(value), [value]);
|
||||||
const [isOpen, setIsOpen] = useState(selectedEntities.length > 0);
|
const [isOpen, setIsOpen] = useState(selectedEntities.length > 0);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
const filteredEntities = useMemo(() => {
|
||||||
|
if (!enableSearch || !searchTerm.trim()) {
|
||||||
|
return availableEntities;
|
||||||
|
}
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
return availableEntities.filter((entity) => {
|
||||||
|
const displayLabel = getDisplayLabel(entity, context);
|
||||||
|
return displayLabel.toLowerCase().includes(term);
|
||||||
|
});
|
||||||
|
}, [availableEntities, searchTerm, enableSearch, getDisplayLabel, context]);
|
||||||
|
|
||||||
const toggleEntity = (entity: string, enabled: boolean) => {
|
const toggleEntity = (entity: string, enabled: boolean) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@ -150,28 +183,41 @@ export function SwitchesWidget(props: WidgetProps) {
|
|||||||
{availableEntities.length === 0 ? (
|
{availableEntities.length === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground">{emptyMessage}</div>
|
<div className="text-sm text-muted-foreground">{emptyMessage}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-2">
|
<>
|
||||||
{availableEntities.map((entity) => {
|
{enableSearch && (
|
||||||
const checked = selectedEntities.includes(entity);
|
<Input
|
||||||
const displayLabel = getDisplayLabel(entity, context);
|
type="text"
|
||||||
return (
|
placeholder="Search..."
|
||||||
<div
|
value={searchTerm}
|
||||||
key={entity}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="flex items-center justify-between rounded-md px-3 py-0"
|
className="mb-2"
|
||||||
>
|
/>
|
||||||
<label htmlFor={`${id}-${entity}`} className="text-sm">
|
)}
|
||||||
{displayLabel}
|
<div className={cn("grid gap-2", listClassName)}>
|
||||||
</label>
|
{filteredEntities.map((entity) => {
|
||||||
<Switch
|
const checked = selectedEntities.includes(entity);
|
||||||
id={`${id}-${entity}`}
|
const displayLabel = getDisplayLabel(entity, context);
|
||||||
checked={checked}
|
return (
|
||||||
disabled={disabled || readonly}
|
<div
|
||||||
onCheckedChange={(value) => toggleEntity(entity, !!value)}
|
key={entity}
|
||||||
/>
|
className="flex items-center justify-between rounded-md px-3 py-0"
|
||||||
</div>
|
>
|
||||||
);
|
<label htmlFor={`${id}-${entity}`} className="text-sm">
|
||||||
})}
|
{displayLabel}
|
||||||
</div>
|
</label>
|
||||||
|
<Switch
|
||||||
|
id={`${id}-${entity}`}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
toggleEntity(entity, !!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user