From c79ca9838fff97cd6197a489cce42db38ac94e00 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:56:52 -0500 Subject: [PATCH] UI tweaks (#23492) * slightly darken bg-card * change menu label * move snapshot retain out of advanced fields * add new ui options for collapsibles * backend title and description * remove unused snapshot retention field * update reference config * remove further references to snapshots retain.mode --- docs/docs/configuration/advanced/reference.md | 5 - docs/docs/configuration/snapshots.md | 2 - frigate/config/camera/camera.py | 4 +- frigate/config/camera/snapshots.py | 6 - web/public/locales/en/config/cameras.json | 8 +- web/public/locales/en/config/global.json | 4 - web/public/locales/en/views/settings.json | 2 +- .../config-form/section-configs/record.ts | 16 +- .../config-form/section-configs/snapshots.ts | 8 +- .../theme/templates/ObjectFieldTemplate.tsx | 137 ++++++++++-------- web/themes/theme-default.css | 4 +- 11 files changed, 99 insertions(+), 97 deletions(-) diff --git a/docs/docs/configuration/advanced/reference.md b/docs/docs/configuration/advanced/reference.md index 81feb17db7..660c003192 100644 --- a/docs/docs/configuration/advanced/reference.md +++ b/docs/docs/configuration/advanced/reference.md @@ -655,11 +655,6 @@ snapshots: retain: # Required: Default retention days (default: shown below) default: 10 - # Optional: Mode for retention. (default: shown below) - # all - save all snapshots regardless of activity - # motion - save snapshots for any detected motion - # active_objects - save snapshots for active/moving objects - mode: motion # Optional: Per object retention days objects: person: 15 diff --git a/docs/docs/configuration/snapshots.md b/docs/docs/configuration/snapshots.md index 84516ce685..0d674b61ac 100644 --- a/docs/docs/configuration/snapshots.md +++ b/docs/docs/configuration/snapshots.md @@ -111,7 +111,6 @@ Navigate to . | Field | Description | | -------------------------------------------------- | ----------------------------------------------------------------------------------- | | **Snapshot retention > Default retention** | Number of days to retain snapshots (default: 10) | -| **Snapshot retention > Retention mode** | Retention mode: `all`, `motion`, or `active_objects` | | **Snapshot retention > Object retention > Person** | Per-object overrides for retention days (e.g., keep `person` snapshots for 15 days) | @@ -122,7 +121,6 @@ snapshots: enabled: True retain: default: 10 - mode: motion objects: person: 15 ``` diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index 01092d4f18..247b1d1168 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -100,8 +100,8 @@ class CameraConfig(FrigateBaseModel): description="Settings for face detection and recognition for this camera.", ) ffmpeg: CameraFfmpegConfig = Field( - title="FFmpeg", - description="FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", + title="Streams (FFmpeg)", + description="Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", ) live: CameraLiveConfig = Field( default_factory=CameraLiveConfig, diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py index 63bcba2267..b6bccfc40c 100644 --- a/frigate/config/camera/snapshots.py +++ b/frigate/config/camera/snapshots.py @@ -3,7 +3,6 @@ from typing import Optional from pydantic import Field from ..base import FrigateBaseModel -from .record import RetainModeEnum __all__ = ["SnapshotsConfig", "RetainConfig"] @@ -14,11 +13,6 @@ class RetainConfig(FrigateBaseModel): title="Default retention", description="Default number of days to retain snapshots.", ) - mode: RetainModeEnum = Field( - default=RetainModeEnum.motion, - title="Retention mode", - description="Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).", - ) objects: dict[str, float] = Field( default_factory=dict, title="Object retention", diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index dd7c7e0a58..7a0651c50a 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -152,8 +152,8 @@ } }, "ffmpeg": { - "label": "FFmpeg", - "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", + "label": "Streams (FFmpeg)", + "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", "path": { "label": "FFmpeg path", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")." @@ -666,10 +666,6 @@ "label": "Default retention", "description": "Default number of days to retain snapshots." }, - "mode": { - "label": "Retention mode", - "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." - }, "objects": { "label": "Object retention", "description": "Per-object overrides for snapshot retention days." diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 09facf0da3..56ec0c3650 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1176,10 +1176,6 @@ "label": "Default retention", "description": "Default number of days to retain snapshots." }, - "mode": { - "label": "Retention mode", - "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." - }, "objects": { "label": "Object retention", "description": "Per-object overrides for snapshot retention days." diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index a40ce797dc..9680f774e2 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -85,7 +85,7 @@ "integrationObjectClassification": "Object classification", "integrationAudioTranscription": "Audio transcription", "cameraDetect": "Object detection", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Streams (FFmpeg)", "cameraRecording": "Recording", "cameraSnapshots": "Snapshots", "cameraMotion": "Motion detection", diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts index d1c2a94e64..d4dd481b86 100644 --- a/web/src/components/config-form/section-configs/record.ts +++ b/web/src/components/config-form/section-configs/record.ts @@ -44,7 +44,14 @@ const record: SectionConfigOverrides = { hiddenFields: ["enabled_in_config", "sync_recordings"], advancedFields: ["expire_interval", "preview", "export"], uiSchema: { + continuous: { + "ui:options": { defaultOpen: true, disableCollapsible: true }, + }, + motion: { + "ui:options": { defaultOpen: true, disableCollapsible: true }, + }, export: { + "ui:options": { defaultOpen: true, disableCollapsible: true }, hwaccel_args: { "ui:widget": "FfmpegArgsWidget", "ui:options": { @@ -59,9 +66,12 @@ const record: SectionConfigOverrides = { "detections.retain.mode": { "ui:options": { enumI18nPrefix: "retainMode" }, }, - "preview.quality": { - "ui:options": { - enumI18nPrefix: "previewQuality", + preview: { + "ui:options": { defaultOpen: true, disableCollapsible: true }, + quality: { + "ui:options": { + enumI18nPrefix: "previewQuality", + }, }, }, }, diff --git a/web/src/components/config-form/section-configs/snapshots.ts b/web/src/components/config-form/section-configs/snapshots.ts index 94bfd9dc8f..e5b5caa208 100644 --- a/web/src/components/config-form/section-configs/snapshots.ts +++ b/web/src/components/config-form/section-configs/snapshots.ts @@ -21,13 +21,14 @@ const snapshots: SectionConfigOverrides = { "crop", "quality", "timestamp", + "required_zones", "retain", ], fieldGroups: { display: ["bounding_box", "crop", "quality", "timestamp"], }, hiddenFields: ["enabled_in_config"], - advancedFields: ["height", "quality", "retain"], + advancedFields: ["height", "quality"], uiSchema: { required_zones: { "ui:widget": "zoneNames", @@ -35,11 +36,6 @@ const snapshots: SectionConfigOverrides = { suppressMultiSchema: true, }, }, - "retain.mode": { - "ui:options": { - enumI18nPrefix: "retainMode", - }, - }, }, }, global: { diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index 9d703b3c8d..009a53b588 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -156,7 +156,8 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { }; const hasModifiedDescendants = checkSubtreeModified(fieldPath); - const [isOpen, setIsOpen] = useState(hasModifiedDescendants); + const defaultOpen = uiSchema?.["ui:options"]?.defaultOpen === true; + const [isOpen, setIsOpen] = useState(hasModifiedDescendants || defaultOpen); const resetKey = `${formContext?.level ?? "global"}::${ formContext?.cameraName ?? "global" }`; @@ -192,6 +193,8 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { (uiSchema?.["ui:groups"] as Record | undefined) || {}; const disableNestedCard = uiSchema?.["ui:options"]?.disableNestedCard === true; + const disableCollapsible = + uiSchema?.["ui:options"]?.disableCollapsible === true; const isHiddenProp = (prop: (typeof properties)[number]) => (prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] === @@ -228,10 +231,10 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { useEffect(() => { if (lastResetKeyRef.current !== resetKey) { lastResetKeyRef.current = resetKey; - setIsOpen(hasModifiedDescendants); + setIsOpen(hasModifiedDescendants || defaultOpen); setShowAdvanced(hasModifiedAdvanced); } - }, [resetKey, hasModifiedDescendants, hasModifiedAdvanced]); + }, [resetKey, hasModifiedDescendants, hasModifiedAdvanced, defaultOpen]); const { children } = props as ObjectFieldTemplateProps & { children?: ReactNode; }; @@ -458,6 +461,75 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { ); } + // Label/description/docs header shared by the collapsible and static layouts. + const cardHeaderContent = ( +
+ + {inferredLabel} + {objectRequiresRestart && } + + {inferredDescription && ( +

+ {inferredDescription} +

+ )} + {fieldDocsUrl && ( +
+ e.stopPropagation()} + > + {t("readTheDocumentation", { ns: "common" })} + + +
+ )} +
+ ); + + // Body shared by the collapsible and static layouts. + const cardBody = hasCustomChildren ? ( + children + ) : ( + <> + {renderGroupedFields(regularProps)} + + + + {renderGroupedFields(advancedProps)} + + + ); + + // Static (non-collapsible) card: keep the labeled header, always show content. + if (disableCollapsible) { + return ( + + {cardHeaderContent} + {cardBody} + + ); + } + // Nested objects render as collapsible cards return ( @@ -465,38 +537,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
-
- - {inferredLabel} - {objectRequiresRestart && ( - - )} - - {inferredDescription && ( -

- {inferredDescription} -

- )} - {fieldDocsUrl && ( -
- e.stopPropagation()} - > - {t("readTheDocumentation", { ns: "common" })} - - -
- )} -
+ {cardHeaderContent} {isOpen ? ( ) : ( @@ -506,31 +547,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { - - {hasCustomChildren ? ( - children - ) : ( - <> - {renderGroupedFields(regularProps)} - - - - {renderGroupedFields(advancedProps)} - - - )} - + {cardBody} diff --git a/web/themes/theme-default.css b/web/themes/theme-default.css index b96f2ecca8..98de4bf0ef 100644 --- a/web/themes/theme-default.css +++ b/web/themes/theme-default.css @@ -113,8 +113,8 @@ --foreground: hsl(0, 0%, 100%); --foreground: 0, 0%, 100%; - --card: hsl(0, 0%, 15%); - --card: 0, 0%, 15%; + --card: hsl(0, 0%, 12%); + --card: 0, 0%, 12%; --card-foreground: hsl(210, 40%, 98%); --card-foreground: 210 40% 98%;