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"
},
"cameraInputs": {
"itemTitle": "Stream {{index}}: {{path}}"
"itemTitle": "Stream {{index}}"
},
"restartRequiredField": "Restart required",
"restartRequiredFooter": "Configuration changed - Restart required",

View File

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

View File

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

View File

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

View File

@ -64,6 +64,10 @@ const resolveMode = (
return "inherit";
}
if (allowInherit && Array.isArray(value) && value.length === 0) {
return "inherit";
}
if (Array.isArray(value)) {
return "manual";
}
@ -116,6 +120,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
const presetField = options?.ffmpegPresetField as PresetField | undefined;
const allowInherit = options?.allowInherit === true;
const hideDescription = options?.hideDescription === true;
const useSplitLayout = options?.splitLayout !== false;
const { data } = useSWR<FfmpegPresetResponse>("ffmpeg/presets");
@ -148,7 +153,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
setMode(nextMode);
if (nextMode === "inherit") {
onChange(null);
onChange(undefined);
return;
}
@ -164,10 +169,15 @@ export function FfmpegArgsWidget(props: WidgetProps) {
return;
}
if (mode === "preset") {
onChange("");
return;
}
const manualText = normalizeManualText(value);
onChange(manualText);
},
[onChange, presetOptions, value],
[mode, onChange, presetOptions, value],
);
const handlePresetChange = useCallback(
@ -237,6 +247,23 @@ export function FfmpegArgsWidget(props: WidgetProps) {
onValueChange={(next) => handleModeChange(next as FfmpegArgsMode)}
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">
<RadioGroupItem
value="preset"
@ -267,23 +294,6 @@ export function FfmpegArgsWidget(props: WidgetProps) {
{t("configForm.ffmpegArgs.manual", { ns: "views/settings" })}
</label>
</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>
{mode === "inherit" ? null : mode === "preset" && canUsePresets ? (
@ -292,7 +302,7 @@ export function FfmpegArgsWidget(props: WidgetProps) {
onValueChange={handlePresetChange}
disabled={disabled || readonly}
>
<SelectTrigger id={id} className="w-full">
<SelectTrigger id={id} className="w-full md:max-w-md">
<SelectValue
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>
) : null}
</div>

View File

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