clean up camera inputs fields

This commit is contained in:
Josh Hawkins 2026-02-16 08:45:39 -06:00
parent afeb03a32f
commit ef665a8c3d
6 changed files with 79 additions and 55 deletions

View File

@ -1252,7 +1252,7 @@
"manualPlaceholder": "Enter FFmpeg arguments" "manualPlaceholder": "Enter FFmpeg arguments"
}, },
"cameraInputs": { "cameraInputs": {
"itemTitle": "Stream {{index}}: {{path}}" "itemTitle": "Stream {{index}}"
}, },
"restartRequiredField": "Restart required", "restartRequiredField": "Restart required",
"restartRequiredFooter": "Configuration changed - Restart required", "restartRequiredFooter": "Configuration changed - Restart required",

View File

@ -30,6 +30,7 @@ const ffmpeg: SectionConfigOverrides = {
output_args: "/configuration/ffmpeg_presets#output-args-presets", output_args: "/configuration/ffmpeg_presets#output-args-presets",
"inputs.output_args": "/configuration/ffmpeg_presets#output-args-presets", "inputs.output_args": "/configuration/ffmpeg_presets#output-args-presets",
"output_args.record": "/configuration/ffmpeg_presets#output-args-presets", "output_args.record": "/configuration/ffmpeg_presets#output-args-presets",
"inputs.roles": "/configuration/cameras/#setting-up-camera-inputs",
}, },
restartRequired: [], restartRequired: [],
fieldOrder: [ fieldOrder: [
@ -85,6 +86,9 @@ const ffmpeg: SectionConfigOverrides = {
}, },
roles: { roles: {
"ui:widget": "inputRoles", "ui:widget": "inputRoles",
"ui:options": {
showArrayItemDescription: true,
},
}, },
global_args: { global_args: {
"ui:widget": "hidden", "ui:widget": "hidden",
@ -92,10 +96,13 @@ const ffmpeg: SectionConfigOverrides = {
hwaccel_args: ffmpegArgsWidget("hwaccel_args", { hwaccel_args: ffmpegArgsWidget("hwaccel_args", {
allowInherit: true, allowInherit: true,
hideDescription: true, hideDescription: true,
forceSplitLayout: true,
showArrayItemDescription: true, showArrayItemDescription: true,
}), }),
input_args: ffmpegArgsWidget("input_args", { input_args: ffmpegArgsWidget("input_args", {
allowInherit: true,
hideDescription: true, hideDescription: true,
forceSplitLayout: true,
showArrayItemDescription: true, showArrayItemDescription: true,
}), }),
output_args: { output_args: {

View File

@ -87,7 +87,7 @@ const normalizeNonDetectHwaccel = (inputs: FfmpegInput[]): FfmpegInput[] =>
return { return {
...input, ...input,
hwaccel_args: null, hwaccel_args: undefined,
}; };
}); });
@ -311,8 +311,9 @@ export function CameraInputsField(props: FieldProps) {
const itemTitle = t("configForm.cameraInputs.itemTitle", { const itemTitle = t("configForm.cameraInputs.itemTitle", {
ns: "views/settings", ns: "views/settings",
index: index + 1, index: index + 1,
path: typeof input.path === "string" ? input.path.trim() : "",
}); });
const itemPath =
typeof input.path === "string" ? input.path.trim() : "";
return ( return (
<Card key={`${baseId}-${index}`} className="w-full"> <Card key={`${baseId}-${index}`} className="w-full">
@ -328,7 +329,14 @@ export function CameraInputsField(props: FieldProps) {
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer p-4 transition-colors hover:bg-muted/50"> <CardHeader className="cursor-pointer p-4 transition-colors hover:bg-muted/50">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<CardTitle className="text-sm">{itemTitle}</CardTitle> <CardTitle className="text-sm">
<span>{itemTitle}</span>
{itemPath ? (
<span className="mt-1 block text-xs font-normal text-muted-foreground">
{itemPath}
</span>
) : null}
</CardTitle>
{open ? ( {open ? (
<LuChevronDown className="h-4 w-4" /> <LuChevronDown className="h-4 w-4" />
) : ( ) : (
@ -352,11 +360,7 @@ export function CameraInputsField(props: FieldProps) {
})} })}
</div> </div>
<div className="w-full"> <div className="w-full">{renderField(index, "roles")}</div>
{renderField(index, "roles", {
showSchemaDescription: true,
})}
</div>
{renderField(index, "input_args")} {renderField(index, "input_args")}

View File

@ -135,9 +135,10 @@ export function FieldTemplate(props: FieldTemplateProps) {
!isMultiSchemaWrapper && !isMultiSchemaWrapper &&
!isObjectField && !isObjectField &&
!isAdditionalProperty; !isAdditionalProperty;
const forceSplitLayout = uiOptionsFromSchema.forceSplitLayout === true;
const useSplitLayout = const useSplitLayout =
uiOptionsFromSchema.splitLayout !== false && uiOptionsFromSchema.splitLayout !== false &&
isScalarValueField && (isScalarValueField || forceSplitLayout) &&
!isBoolean && !isBoolean &&
!isMultiSchemaWrapper && !isMultiSchemaWrapper &&
!isObjectField && !isObjectField &&

View File

@ -64,6 +64,10 @@ const resolveMode = (
return "inherit"; return "inherit";
} }
if (allowInherit && Array.isArray(value) && value.length === 0) {
return "inherit";
}
if (Array.isArray(value)) { if (Array.isArray(value)) {
return "manual"; return "manual";
} }
@ -116,6 +120,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
const presetField = options?.ffmpegPresetField as PresetField | undefined; const presetField = options?.ffmpegPresetField as PresetField | undefined;
const allowInherit = options?.allowInherit === true; const allowInherit = options?.allowInherit === true;
const hideDescription = options?.hideDescription === true; const hideDescription = options?.hideDescription === true;
const useSplitLayout = options?.splitLayout !== false;
const { data } = useSWR<FfmpegPresetResponse>("ffmpeg/presets"); const { data } = useSWR<FfmpegPresetResponse>("ffmpeg/presets");
@ -148,7 +153,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
setMode(nextMode); setMode(nextMode);
if (nextMode === "inherit") { if (nextMode === "inherit") {
onChange(null); onChange(undefined);
return; return;
} }
@ -164,10 +169,15 @@ export function FfmpegArgsWidget(props: WidgetProps) {
return; return;
} }
if (mode === "preset") {
onChange("");
return;
}
const manualText = normalizeManualText(value); const manualText = normalizeManualText(value);
onChange(manualText); onChange(manualText);
}, },
[onChange, presetOptions, value], [mode, onChange, presetOptions, value],
); );
const handlePresetChange = useCallback( const handlePresetChange = useCallback(
@ -237,6 +247,23 @@ export function FfmpegArgsWidget(props: WidgetProps) {
onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)} onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)}
className="gap-3" className="gap-3"
> >
{allowInherit ? (
<div className="flex items-center space-x-2">
<RadioGroupItem
value="inherit"
id={`${id}-inherit`}
disabled={disabled || readonly}
className={
mode === "inherit"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/>
<label htmlFor={`${id}-inherit`} className="cursor-pointer text-sm">
{t("configForm.ffmpegArgs.inherit", { ns: "views/settings" })}
</label>
</div>
) : null}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem <RadioGroupItem
value="preset" value="preset"
@ -267,23 +294,6 @@ export function FfmpegArgsWidget(props: WidgetProps) {
{t("configForm.ffmpegArgs.manual", { ns: "views/settings" })} {t("configForm.ffmpegArgs.manual", { ns: "views/settings" })}
</label> </label>
</div> </div>
{allowInherit ? (
<div className="flex items-center space-x-2">
<RadioGroupItem
value="inherit"
id={`${id}-inherit`}
disabled={disabled || readonly}
className={
mode === "inherit"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/>
<label htmlFor={`${id}-inherit`} className="cursor-pointer text-sm">
{t("configForm.ffmpegArgs.inherit", { ns: "views/settings" })}
</label>
</div>
) : null}
</RadioGroup> </RadioGroup>
{mode === "inherit" ? null : mode === "preset" && canUsePresets ? ( {mode === "inherit" ? null : mode === "preset" && canUsePresets ? (
@ -292,7 +302,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
onValueChange={handlePresetChange} onValueChange={handlePresetChange}
disabled={disabled || readonly} disabled={disabled || readonly}
> >
<SelectTrigger id={id} className="w-full"> <SelectTrigger id={id} className="w-full md:max-w-md">
<SelectValue <SelectValue
placeholder={ placeholder={
placeholder || placeholder ||
@ -326,7 +336,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
/> />
)} )}
{!hideDescription && fieldDescription ? ( {!hideDescription && !useSplitLayout && fieldDescription ? (
<p className="text-xs text-muted-foreground">{fieldDescription}</p> <p className="text-xs text-muted-foreground">{fieldDescription}</p>
) : null} ) : null}
</div> </div>

View File

@ -35,31 +35,33 @@ export function InputRolesWidget(props: WidgetProps) {
}; };
return ( return (
<div className="space-y-2 rounded-lg bg-secondary p-2 pr-0 md:max-w-md"> <div className="rounded-lg border border-secondary-highlight bg-background_alt p-2 pr-0 md:max-w-md">
{INPUT_ROLES.map((role) => { <div className="grid gap-2">
const checked = selectedRoles.includes(role); {INPUT_ROLES.map((role) => {
const label = t(`configForm.inputRoles.options.${role}`, { const checked = selectedRoles.includes(role);
ns: "views/settings", const label = t(`configForm.inputRoles.options.${role}`, {
defaultValue: role, ns: "views/settings",
}); defaultValue: role,
});
return ( return (
<div <div
key={role} key={role}
className="flex items-center justify-between rounded-md px-3 py-0" className="flex items-center justify-between rounded-md px-3 py-0"
> >
<label htmlFor={`${id}-${role}`} className="text-sm"> <label htmlFor={`${id}-${role}`} className="text-sm">
{label} {label}
</label> </label>
<Switch <Switch
id={`${id}-${role}`} id={`${id}-${role}`}
checked={checked} checked={checked}
disabled={disabled || readonly} disabled={disabled || readonly}
onCheckedChange={(enabled) => toggleRole(role, !!enabled)} onCheckedChange={(enabled) => toggleRole(role, !!enabled)}
/> />
</div> </div>
); );
})} })}
</div>
</div> </div>
); );
} }