diff --git a/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx index 209233bed..eb6780c0f 100644 --- a/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/AudioLabelSwitchesWidget.tsx @@ -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 { useCallback, useMemo } from "react"; +import useSWR from "swr"; import { SwitchesWidget } from "./SwitchesWidget"; import type { FormContext } from "./SwitchesWidget"; import { getTranslatedLabel } from "@/utils/i18n"; +import { JsonObject } from "@/types/configForm"; -function getAudioLabels(context: FormContext): string[] { +function getEnabledAudioLabels(context: FormContext): string[] { let cameraLabels: string[] = []; let globalLabels: string[] = []; if (context) { - // context.cameraValue and context.globalValue should be the entire objects section - const trackValue = context.cameraValue?.listen; - if (Array.isArray(trackValue)) { - cameraLabels = trackValue.filter( - (item): item is string => typeof item === "string", - ); + // context.cameraValue and context.globalValue should be the entire audio section + if ( + context.cameraValue && + typeof context.cameraValue === "object" && + !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 (Array.isArray(globalTrackValue)) { - globalLabels = globalTrackValue.filter( - (item): item is string => typeof item === "string", - ); + if ( + context.globalValue && + typeof context.globalValue === "object" && + !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) { + const { data: audioLabels } = useSWR>("/audio_labels"); + + const allLabels = useMemo(() => { + if (!audioLabels) { + return []; + } + + const labelSet = new Set(); + 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 ( ); diff --git a/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx index 15e5accbc..eac5f2493 100644 --- a/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx @@ -3,6 +3,7 @@ import { WidgetProps } from "@rjsf/utils"; import { useMemo, useState } from "react"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Collapsible, CollapsibleContent, @@ -11,10 +12,16 @@ import { import { LuChevronDown, LuChevronRight } from "react-icons/lu"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import { ConfigFormContext } from "@/types/configForm"; +import { cn } from "@/lib/utils"; type FormContext = Pick< ConfigFormContext, - "cameraValue" | "globalValue" | "fullCameraConfig" | "fullConfig" | "t" + | "cameraValue" + | "globalValue" + | "fullCameraConfig" + | "fullConfig" + | "t" + | "level" > & { fullCameraConfig?: CameraConfig; fullConfig?: FrigateConfig; @@ -31,6 +38,10 @@ export type SwitchesWidgetOptions = { i18nKey: string; /** Translation namespace (default: "views/settings") */ 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[] { @@ -100,8 +111,30 @@ export function SwitchesWidget(props: WidgetProps) { [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 [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) => { if (enabled) { @@ -150,28 +183,41 @@ export function SwitchesWidget(props: WidgetProps) { {availableEntities.length === 0 ? (
{emptyMessage}
) : ( -
- {availableEntities.map((entity) => { - const checked = selectedEntities.includes(entity); - const displayLabel = getDisplayLabel(entity, context); - return ( -
- - toggleEntity(entity, !!value)} - /> -
- ); - })} -
+ <> + {enableSearch && ( + setSearchTerm(e.target.value)} + className="mb-2" + /> + )} +
+ {filteredEntities.map((entity) => { + const checked = selectedEntities.includes(entity); + const displayLabel = getDisplayLabel(entity, context); + return ( +
+ + + toggleEntity(entity, !!value) + } + /> +
+ ); + })} +
+ )}