mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +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 { 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<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 (
|
||||
<SwitchesWidget
|
||||
{...props}
|
||||
options={{
|
||||
...props.options,
|
||||
getEntities: getAudioLabels,
|
||||
getEntities,
|
||||
getDisplayLabel: getAudioLabelDisplayName,
|
||||
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 { 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 ? (
|
||||
<div className="text-sm text-muted-foreground">{emptyMessage}</div>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
{availableEntities.map((entity) => {
|
||||
const checked = selectedEntities.includes(entity);
|
||||
const displayLabel = getDisplayLabel(entity, context);
|
||||
return (
|
||||
<div
|
||||
key={entity}
|
||||
className="flex items-center justify-between rounded-md px-3 py-0"
|
||||
>
|
||||
<label htmlFor={`${id}-${entity}`} className="text-sm">
|
||||
{displayLabel}
|
||||
</label>
|
||||
<Switch
|
||||
id={`${id}-${entity}`}
|
||||
checked={checked}
|
||||
disabled={disabled || readonly}
|
||||
onCheckedChange={(value) => toggleEntity(entity, !!value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<>
|
||||
{enableSearch && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
<div className={cn("grid gap-2", listClassName)}>
|
||||
{filteredEntities.map((entity) => {
|
||||
const checked = selectedEntities.includes(entity);
|
||||
const displayLabel = getDisplayLabel(entity, context);
|
||||
return (
|
||||
<div
|
||||
key={entity}
|
||||
className="flex items-center justify-between rounded-md px-3 py-0"
|
||||
>
|
||||
<label htmlFor={`${id}-${entity}`} className="text-sm">
|
||||
{displayLabel}
|
||||
</label>
|
||||
<Switch
|
||||
id={`${id}-${entity}`}
|
||||
checked={checked}
|
||||
disabled={disabled || readonly}
|
||||
onCheckedChange={(value) =>
|
||||
toggleEntity(entity, !!value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user