diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index b0073d1d3..ecf5102b4 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -30,10 +30,22 @@ const detect: SectionConfigOverrides = { ], }, global: { - restartRequired: ["width", "height", "min_initialized", "max_disappeared"], + restartRequired: [ + "fps", + "width", + "height", + "min_initialized", + "max_disappeared", + ], }, camera: { - restartRequired: ["width", "height", "min_initialized", "max_disappeared"], + restartRequired: [ + "fps", + "width", + "height", + "min_initialized", + "max_disappeared", + ], }, }; diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index bab142d92..38755ee20 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -3,6 +3,12 @@ import type { SectionConfigOverrides } from "./types"; const motion: SectionConfigOverrides = { base: { sectionDocs: "/configuration/motion_detection", + fieldDocs: { + lightning_threshold: + "/configuration/motion_detection#lightning_threshold", + skip_motion_threshold: + "/configuration/motion_detection#skip_motion_on_large_scene_changes", + }, restartRequired: [], fieldOrder: [ "enabled", @@ -20,6 +26,16 @@ const motion: SectionConfigOverrides = { sensitivity: ["enabled", "threshold", "contour_area"], algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], }, + uiSchema: { + skip_motion_threshold: { + "ui:widget": "optionalField", + "ui:options": { + innerWidget: "range", + step: 0.05, + suppressMultiSchema: true, + }, + }, + }, hiddenFields: ["enabled_in_config", "mask", "raw_mask"], advancedFields: [ "lightning_threshold", @@ -58,7 +74,7 @@ const motion: SectionConfigOverrides = { "frame_alpha", "frame_height", ], - advancedFields: ["lightning_threshold"], + advancedFields: ["lightning_threshold", "skip_motion_threshold"], }, }; diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index 3baa2f3ad..79bc14b84 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -26,6 +26,7 @@ import { FfmpegArgsWidget } from "./widgets/FfmpegArgsWidget"; import { InputRolesWidget } from "./widgets/InputRolesWidget"; import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget"; import { CameraPathWidget } from "./widgets/CameraPathWidget"; +import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget"; import { FieldTemplate } from "./templates/FieldTemplate"; import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate"; @@ -73,6 +74,7 @@ export const frigateTheme: FrigateTheme = { audioLabels: AudioLabelSwitchesWidget, zoneNames: ZoneSwitchesWidget, timezoneSelect: TimezoneSelectWidget, + optionalField: OptionalFieldWidget, }, templates: { FieldTemplate: FieldTemplate as React.ComponentType, diff --git a/web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx b/web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx new file mode 100644 index 000000000..7f05d6466 --- /dev/null +++ b/web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx @@ -0,0 +1,64 @@ +// Optional Field Widget - wraps any inner widget with an enable/disable switch +// Used for nullable fields where None means "disabled" (not the same as 0) + +import type { WidgetProps } from "@rjsf/utils"; +import { getWidget } from "@rjsf/utils"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { getNonNullSchema } from "../fields/nullableUtils"; + +export function OptionalFieldWidget(props: WidgetProps) { + const { id, value, disabled, readonly, onChange, schema, options, registry } = + props; + + const innerWidgetName = (options.innerWidget as string) || undefined; + const isEnabled = value !== undefined && value !== null; + + // Extract the non-null branch from anyOf [Type, null] + const innerSchema = getNonNullSchema(schema) ?? schema; + + const InnerWidget = getWidget(innerSchema, innerWidgetName, registry.widgets); + + const getDefaultValue = () => { + if (innerSchema.default !== undefined && innerSchema.default !== null) { + return innerSchema.default; + } + if (innerSchema.minimum !== undefined) { + return innerSchema.minimum; + } + if (innerSchema.type === "integer" || innerSchema.type === "number") { + return 0; + } + if (innerSchema.type === "string") { + return ""; + } + return 0; + }; + + const handleToggle = (checked: boolean) => { + onChange(checked ? getDefaultValue() : undefined); + }; + + const innerProps: WidgetProps = { + ...props, + schema: innerSchema, + disabled: disabled || readonly || !isEnabled, + value: isEnabled ? value : getDefaultValue(), + }; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index b8167b7dd..f66f45858 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -1284,7 +1284,7 @@ function MotionReview({ return ( <> - {motionPreviewsCamera && selectedMotionPreviewCamera ? ( + {selectedMotionPreviewCamera && ( <>