From c7c03a64bc5d2407731c637957fa2235b999cb05 Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Wed, 20 Aug 2025 17:24:30 +0000 Subject: [PATCH] refactor: Refactor camera nickname --- frigate/config/camera/camera.py | 12 +++- frigate/storage.py | 3 +- web/public/locales/en/components/camera.json | 1 + web/public/locales/en/views/settings.json | 3 +- web/src/components/camera/CameraNameLabel.tsx | 36 +++++++++++ .../components/filter/CameraGroupSelector.tsx | 9 ++- .../components/filter/CamerasFilterButton.tsx | 3 +- web/src/components/filter/FilterSwitch.tsx | 23 +++++-- web/src/components/input/InputWithTags.tsx | 23 +++++-- .../components/overlay/CameraInfoDialog.tsx | 5 +- .../overlay/CreateTriggerDialog.tsx | 7 ++- .../overlay/detail/SearchDetailDialog.tsx | 3 +- web/src/components/player/LivePlayer.tsx | 6 +- .../components/settings/CameraEditForm.tsx | 63 +++++++++++++------ .../settings/CameraStreamingDialog.tsx | 5 +- web/src/hooks/use-camera-nickname.ts | 21 +++++++ web/src/pages/Settings.tsx | 12 ++-- web/src/types/frigateConfig.ts | 1 + web/src/views/settings/CameraSettingsView.tsx | 36 +++++------ .../settings/FrigatePlusSettingsView.tsx | 5 +- .../settings/NotificationsSettingsView.tsx | 3 +- web/src/views/settings/TriggerView.tsx | 6 +- web/src/views/system/CameraMetrics.tsx | 3 +- 23 files changed, 216 insertions(+), 73 deletions(-) create mode 100644 web/src/components/camera/CameraNameLabel.tsx create mode 100644 web/src/hooks/use-camera-nickname.ts diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index a3c9733ff..4290c8bf2 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -2,7 +2,7 @@ import os from enum import Enum from typing import Optional -from pydantic import Field, PrivateAttr +from pydantic import Field, PrivateAttr, model_validator from frigate.const import CACHE_DIR, CACHE_SEGMENT_FORMAT, REGEX_CAMERA_NAME from frigate.ffmpeg_presets import ( @@ -51,6 +51,16 @@ class CameraTypeEnum(str, Enum): class CameraConfig(FrigateBaseModel): name: Optional[str] = Field(None, title="Camera name.", pattern=REGEX_CAMERA_NAME) + + nickname: Optional[str] = Field(None, title="Camera nickname. Only for display.") + + @model_validator(mode="before") + @classmethod + def handle_nickname(cls, values): + if isinstance(values, dict) and "nickname" in values: + pass + return values + enabled: bool = Field(default=True, title="Enable camera.") # Options with global fallback diff --git a/frigate/storage.py b/frigate/storage.py index 1c4650271..d186b8585 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -77,7 +77,8 @@ class StorageMaintainer(threading.Thread): .scalar() ) - usages[camera] = { + camera_key = getattr(self.config.cameras[camera], "nickname", camera) + usages[camera_key] = { "usage": camera_storage, "bandwidth": self.camera_storage_stats.get(camera, {}).get( "bandwidth", 0 diff --git a/web/public/locales/en/components/camera.json b/web/public/locales/en/components/camera.json index 10513a729..e13039b44 100644 --- a/web/public/locales/en/components/camera.json +++ b/web/public/locales/en/components/camera.json @@ -27,6 +27,7 @@ "icon": "Icon", "success": "Camera group ({{name}}) has been saved.", "camera": { + "birdseye": "Birdseye", "setting": { "label": "Camera Streaming Settings", "title": "{{cameraName}} Streaming Settings", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 516ddf9f2..0dddc1283 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -17,6 +17,7 @@ "cameras": "Camera Settings", "masksAndZones": "Masks / Zones", "motionTuner": "Motion Tuner", + "triggers": "Triggers", "debug": "Debug", "users": "Users", "notifications": "Notifications", @@ -195,7 +196,7 @@ "description": "Configure camera settings including stream inputs and roles.", "name": "Camera Name", "nameRequired": "Camera name is required", - "nameInvalid": "Camera name must contain only letters, numbers, underscores, or hyphens", + "nameLength": "Camera name must be less than 24 characters.", "namePlaceholder": "e.g., front_door", "enabled": "Enabled", "ffmpeg": { diff --git a/web/src/components/camera/CameraNameLabel.tsx b/web/src/components/camera/CameraNameLabel.tsx new file mode 100644 index 000000000..2bc5faaf2 --- /dev/null +++ b/web/src/components/camera/CameraNameLabel.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; +import { useCameraNickname } from "@/hooks/use-camera-nickname"; +import { CameraConfig } from "@/types/frigateConfig"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +interface CameraNameLabelProps + extends React.ComponentPropsWithoutRef, + VariantProps { + camera?: string | CameraConfig; +} + +const CameraNameLabel = React.forwardRef< + React.ElementRef, + CameraNameLabelProps +>(({ className, camera, ...props }, ref) => { + const displayName = useCameraNickname(camera); + return ( + + {displayName} + + ); +}); +CameraNameLabel.displayName = LabelPrimitive.Root.displayName; + +export { CameraNameLabel }; diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 6d9ea7856..6e20687cc 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -71,12 +71,12 @@ import { MobilePageTitle, } from "../mobile/MobilePage"; -import { Label } from "../ui/label"; import { Switch } from "../ui/switch"; import { CameraStreamingDialog } from "../settings/CameraStreamingDialog"; import { DialogTrigger } from "@radix-ui/react-dialog"; import { useStreamingSettings } from "@/context/streaming-settings-provider"; import { Trans, useTranslation } from "react-i18next"; +import { CameraNameLabel } from "../camera/CameraNameLabel"; type CameraGroupSelectorProps = { className?: string; @@ -846,12 +846,11 @@ export function CameraGroupEdit({ ].map((camera) => (
- + camera={camera} + />
{camera !== "birdseye" && ( diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index 247555a0a..93b8a8651 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -189,7 +189,8 @@ export function CamerasFilterContent({ void; }; export default function FilterSwitch({ label, disabled = false, isChecked, + isCameraName = false, onCheckedChange, }: FilterSwitchProps) { return (
- + {isCameraName ? ( + + ) : ( + + )} {t("filter.label." + filterType)}:{" "} - {filterType === "labels" - ? getTranslatedLabel(value) - : value.replaceAll("_", " ")} + {filterType === "labels" ? ( + getTranslatedLabel(value) + ) : filterType === "cameras" ? ( + + ) : ( + value.replaceAll("_", " ") + )} ); @@ -376,7 +379,8 @@ function CameraSelectButton({ { if (isChecked && (isEnabled || isCameraSettingsPage)) { setSelectedCamera(item.name); diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 22a35bdd9..eb806d7b5 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -33,6 +33,7 @@ export type SearchModel = "jinav1" | "jinav2"; export type SearchModelSize = "small" | "large"; export interface CameraConfig { + nickname: string; audio: { enabled: boolean; enabled_in_config: boolean; diff --git a/web/src/views/settings/CameraSettingsView.tsx b/web/src/views/settings/CameraSettingsView.tsx index 61f327c6e..90344b470 100644 --- a/web/src/views/settings/CameraSettingsView.tsx +++ b/web/src/views/settings/CameraSettingsView.tsx @@ -49,6 +49,8 @@ import { } from "@/components/ui/select"; import { IoMdArrowRoundBack } from "react-icons/io"; import { isDesktop } from "react-device-detect"; +import { useCameraNickname } from "@/hooks/use-camera-nickname"; +import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; type CameraSettingsViewProps = { selectedCamera: string; @@ -96,6 +98,8 @@ export default function CameraSettingsView({ return []; }, [config]); + const selectCameraName = useCameraNickname(selectedCamera); + // zones and labels const zones = useMemo(() => { @@ -337,11 +341,13 @@ export default function CameraSettingsView({ - {cameras.map((camera) => ( - - {capitalizeFirstLetter(camera.replaceAll("_", " "))} - - ))} + {cameras.map((camera) => { + return ( + + + + ); + })}
@@ -614,18 +620,14 @@ export default function CameraSettingsView({ ), ) .join(", "), - cameraName: capitalizeFirstLetter( - cameraConfig?.name ?? "", - ).replaceAll("_", " "), + cameraName: selectCameraName, }, ) : t( "camera.reviewClassification.objectAlertsTips", { alertsLabels, - cameraName: capitalizeFirstLetter( - cameraConfig?.name ?? "", - ).replaceAll("_", " "), + cameraName: selectCameraName, }, )}
@@ -737,9 +739,7 @@ export default function CameraSettingsView({ ), ) .join(", "), - cameraName: capitalizeFirstLetter( - cameraConfig?.name ?? "", - ).replaceAll("_", " "), + cameraName: selectCameraName, }} ns="views/settings" /> @@ -756,9 +756,7 @@ export default function CameraSettingsView({ ), ) .join(", "), - cameraName: capitalizeFirstLetter( - cameraConfig?.name ?? "", - ).replaceAll("_", " "), + cameraName: selectCameraName, }} ns="views/settings" /> @@ -768,9 +766,7 @@ export default function CameraSettingsView({ i18nKey="camera.reviewClassification.objectDetectionsTips" values={{ detectionsLabels, - cameraName: capitalizeFirstLetter( - cameraConfig?.name ?? "", - ).replaceAll("_", " "), + cameraName: selectCameraName, }} ns="views/settings" /> diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index e721aa13c..5a322ca06 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -23,6 +23,7 @@ import { SelectTrigger, } from "@/components/ui/select"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; type FrigatePlusModel = { id: string; @@ -488,7 +489,9 @@ export default function FrigatePlusSettingsView({ key={name} className="border-b border-secondary" > - {name} + + + {camera.snapshots.enabled ? ( diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index ab0241e24..7f5115e93 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -464,7 +464,8 @@ export default function NotificationView({ {allCameras?.map((camera) => ( { setChangedValue(true); diff --git a/web/src/views/settings/TriggerView.tsx b/web/src/views/settings/TriggerView.tsx index d84359f09..61270484f 100644 --- a/web/src/views/settings/TriggerView.tsx +++ b/web/src/views/settings/TriggerView.tsx @@ -23,6 +23,7 @@ import { cn } from "@/lib/utils"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { Link } from "react-router-dom"; import { useTriggers } from "@/api/ws"; +import { useCameraNickname } from "@/hooks/use-camera-nickname"; type ConfigSetBody = { requires_restart: number; @@ -78,6 +79,7 @@ export default function TriggerView({ const [triggeredTrigger, setTriggeredTrigger] = useState(); const [isLoading, setIsLoading] = useState(false); + const cameraName = useCameraNickname(selectedCamera); const triggers = useMemo(() => { if ( !config || @@ -390,7 +392,9 @@ export default function TriggerView({ {t("triggers.management.title")}

- {t("triggers.management.desc", { camera: selectedCamera })} + {t("triggers.management.desc", { + camera: cameraName, + })}