diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index b81ceef89..23e18c2c9 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -810,6 +810,8 @@ cameras: # NOTE: This must be different than any camera names, but can match with another zone on another # camera. front_steps: + # Optional: A friendly name or descriptive text for the zones + friendly_name: "" # Required: List of x,y coordinates to define the polygon of the zone. # NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box. coordinates: 0.033,0.306,0.324,0.138,0.439,0.185,0.042,0.428 diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md index 025384a8b..c0a11d4f6 100644 --- a/docs/docs/configuration/zones.md +++ b/docs/docs/configuration/zones.md @@ -27,6 +27,7 @@ cameras: - entire_yard zones: entire_yard: + friendly_name: Entire yard # You can use characters from any language text coordinates: ... ``` @@ -44,8 +45,10 @@ cameras: - edge_yard zones: edge_yard: + friendly_name: Edge yard # You can use characters from any language text coordinates: ... inner_yard: + friendly_name: Inner yard # You can use characters from any language text coordinates: ... ``` @@ -59,6 +62,7 @@ cameras: - entire_yard zones: entire_yard: + friendly_name: Entire yard coordinates: ... ``` @@ -82,6 +86,7 @@ cameras: Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. Objects will be tracked for any `person` that enter anywhere in the yard, and for cars only if they enter the street. + ### Zone Loitering Sometimes objects are expected to be passing through a zone, but an object loitering in an area is unexpected. Zones can be configured to have a minimum loitering time after which the object will be considered in the zone. diff --git a/frigate/config/camera/zone.py b/frigate/config/camera/zone.py index 3e69240d5..530ba1cf9 100644 --- a/frigate/config/camera/zone.py +++ b/frigate/config/camera/zone.py @@ -13,6 +13,9 @@ logger = logging.getLogger(__name__) class ZoneConfig(BaseModel): + friendly_name: Optional[str] = Field( + None, title="Zone friendly name used in the Frigate UI." + ) filters: dict[str, FilterConfig] = Field( default_factory=dict, title="Zone filters." ) diff --git a/web/src/components/camera/CameraNameLabel.tsx b/web/src/components/camera/FriendlyNameLabel.tsx similarity index 53% rename from web/src/components/camera/CameraNameLabel.tsx rename to web/src/components/camera/FriendlyNameLabel.tsx index ab022f5c8..ca0978852 100644 --- a/web/src/components/camera/CameraNameLabel.tsx +++ b/web/src/components/camera/FriendlyNameLabel.tsx @@ -2,12 +2,19 @@ import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { CameraConfig } from "@/types/frigateConfig"; +import { useZoneFriendlyName } from "@/hooks/use-zone-friendly-name"; interface CameraNameLabelProps extends React.ComponentPropsWithoutRef { camera?: string | CameraConfig; } +interface ZoneNameLabelProps + extends React.ComponentPropsWithoutRef { + zone: string; + camera?: string; +} + const CameraNameLabel = React.forwardRef< React.ElementRef, CameraNameLabelProps @@ -21,4 +28,17 @@ const CameraNameLabel = React.forwardRef< }); CameraNameLabel.displayName = LabelPrimitive.Root.displayName; -export { CameraNameLabel }; +const ZoneNameLabel = React.forwardRef< + React.ElementRef, + ZoneNameLabelProps +>(({ className, zone, camera, ...props }, ref) => { + const displayName = useZoneFriendlyName(zone, camera); + return ( + + {displayName} + + ); +}); +ZoneNameLabel.displayName = LabelPrimitive.Root.displayName; + +export { CameraNameLabel, ZoneNameLabel }; diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index cd0b118c9..a700981b6 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -76,7 +76,7 @@ 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"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useIsCustomRole } from "@/hooks/use-is-custom-role"; diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index 93b8a8651..baeccf06f 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -190,7 +190,7 @@ export function CamerasFilterContent({ key={item} isChecked={currentCameras?.includes(item) ?? false} label={item} - isCameraName={true} + type={"camera"} disabled={ mainCamera !== undefined && currentCameras !== undefined && diff --git a/web/src/components/filter/FilterSwitch.tsx b/web/src/components/filter/FilterSwitch.tsx index fa8709d96..d282b9e12 100644 --- a/web/src/components/filter/FilterSwitch.tsx +++ b/web/src/components/filter/FilterSwitch.tsx @@ -1,29 +1,39 @@ import { Switch } from "../ui/switch"; import { Label } from "../ui/label"; -import { CameraNameLabel } from "../camera/CameraNameLabel"; +import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel"; type FilterSwitchProps = { label: string; disabled?: boolean; isChecked: boolean; isCameraName?: boolean; + type?: string; + extraValue?: string; onCheckedChange: (checked: boolean) => void; }; export default function FilterSwitch({ label, disabled = false, isChecked, - isCameraName = false, + type = "", + extraValue = "", onCheckedChange, }: FilterSwitchProps) { return (
- {isCameraName ? ( + {type === "camera" ? ( + ) : type === "zone" ? ( + ) : (
masksAndZones.form.polygonDrawing.delete.desc diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 8a1e36a0e..3a0e23d1f 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -34,6 +34,7 @@ import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { getTranslatedLabel } from "@/utils/i18n"; +import NameAndIdFields from "../input/NameAndIdFields"; type ZoneEditPaneProps = { polygons?: Polygon[]; @@ -146,15 +147,37 @@ export default function ZoneEditPane({ "masksAndZones.form.zoneName.error.mustNotContainPeriod", ), }, - ) - .refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), { - message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter"), - }) - .refine((value: string) => /[a-zA-Z]/.test(value), { + ), + friendly_name: z + .string() + .min(2, { message: t( - "masksAndZones.form.zoneName.error.mustHaveAtLeastOneLetter", + "masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters", ), - }), + }) + .refine( + (value: string) => { + return !cameras.map((cam) => cam.name).includes(value); + }, + { + message: t( + "masksAndZones.form.zoneName.error.mustNotBeSameWithCamera", + ), + }, + ) + .refine( + (value: string) => { + const otherPolygonNames = + polygons + ?.filter((_, index) => index !== activePolygonIndex) + .map((polygon) => polygon.name) || []; + + return !otherPolygonNames.includes(value); + }, + { + message: t("masksAndZones.form.zoneName.error.alreadyExists"), + }, + ), inertia: z.coerce .number() .min(1, { @@ -247,6 +270,7 @@ export default function ZoneEditPane({ mode: "onBlur", defaultValues: { name: polygon?.name ?? "", + friendly_name: polygon?.friendly_name ?? polygon?.name ?? "", inertia: polygon?.camera && polygon?.name && @@ -286,6 +310,7 @@ export default function ZoneEditPane({ async ( { name: zoneName, + friendly_name, inertia, loitering_time, objects: form_objects, @@ -415,9 +440,14 @@ export default function ZoneEditPane({ } } + let friendlyNameQuery = ""; + if (friendly_name) { + friendlyNameQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.friendly_name=${encodeURIComponent(friendly_name)}`; + } + axios .put( - `config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${speedThresholdQuery}${distancesQuery}${objectQueries}${alertQueries}${detectionQueries}`, + `config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${speedThresholdQuery}${distancesQuery}${objectQueries}${friendlyNameQuery}${alertQueries}${detectionQueries}`, { requires_restart: 0, update_topic: `config/cameras/${polygon.camera}/zones`, @@ -427,7 +457,7 @@ export default function ZoneEditPane({ if (res.status === 200) { toast.success( t("masksAndZones.zones.toast.success", { - zoneName, + zoneName: friendly_name || zoneName, }), { position: "top-center", @@ -541,26 +571,16 @@ export default function ZoneEditPane({
- ( - - {t("masksAndZones.zones.name.title")} - - - - - {t("masksAndZones.zones.name.tips")} - - - - )} + nameField="friendly_name" + idField="name" + nameLabel={t("masksAndZones.zones.name.title")} + nameDescription={t("masksAndZones.zones.name.tips")} + placeholderName={t("masksAndZones.zones.name.inputPlaceHolder")} /> + ("config"); const timeline = useMemo(() => { if (!fullTimeline) { return fullTimeline; } - return fullTimeline.filter( - (t) => - t.timestamp >= review.start_time && - (review.end_time == undefined || t.timestamp <= review.end_time), - ); - }, [fullTimeline, review]); + return fullTimeline + .filter( + (t) => + t.timestamp >= review.start_time && + (review.end_time == undefined || t.timestamp <= review.end_time), + ) + .map((event) => ({ + ...event, + data: { + ...event.data, + zones_friendly_names: event.data?.zones?.map((zone) => + resolveZoneName(config, zone), + ), + }, + })); + }, [config, fullTimeline, review]); if (isValidating && (!timeline || timeline.length === 0)) { return ; diff --git a/web/src/hooks/use-zone-friendly-name.ts b/web/src/hooks/use-zone-friendly-name.ts new file mode 100644 index 000000000..18ce2a45c --- /dev/null +++ b/web/src/hooks/use-zone-friendly-name.ts @@ -0,0 +1,41 @@ +import { FrigateConfig } from "@/types/frigateConfig"; +import { useMemo } from "react"; +import useSWR from "swr"; + +export function resolveZoneName( + config: FrigateConfig | undefined, + zoneId: string, + cameraId?: string, +) { + if (!config) return String(zoneId).replace(/_/g, " "); + + if (cameraId) { + const camera = config.cameras?.[String(cameraId)]; + const zone = camera?.zones?.[zoneId]; + return zone?.friendly_name || String(zoneId).replace(/_/g, " "); + } + + for (const camKey in config.cameras) { + if (!Object.prototype.hasOwnProperty.call(config.cameras, camKey)) continue; + const cam = config.cameras[camKey]; + if (!cam?.zones) continue; + if (Object.prototype.hasOwnProperty.call(cam.zones, zoneId)) { + const zone = cam.zones[zoneId]; + return zone?.friendly_name || String(zoneId).replace(/_/g, " "); + } + } + + // Fallback: return a cleaned-up zoneId string + return String(zoneId).replace(/_/g, " "); +} + +export function useZoneFriendlyName(zoneId: string, cameraId?: string): string { + const { data: config } = useSWR("config"); + + const name = useMemo( + () => resolveZoneName(config, zoneId, cameraId), + [config, cameraId, zoneId], + ); + + return name; +} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 19fd63f5c..918d492a3 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -42,7 +42,7 @@ import { useInitialCameraState } from "@/api/ws"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { useTranslation } from "react-i18next"; import TriggerView from "@/views/settings/TriggerView"; -import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { Sidebar, SidebarContent, @@ -650,7 +650,7 @@ function CameraSelectButton({ key={item.name} isChecked={item.name === selectedCamera} label={item.name} - isCameraName={true} + type={"camera"} onCheckedChange={(isChecked) => { if (isChecked && (isEnabled || isCameraSettingsPage)) { setSelectedCamera(item.name); diff --git a/web/src/types/canvas.ts b/web/src/types/canvas.ts index 9c1748ce0..a70238834 100644 --- a/web/src/types/canvas.ts +++ b/web/src/types/canvas.ts @@ -11,10 +11,12 @@ export type Polygon = { distances: number[]; isFinished: boolean; color: number[]; + friendly_name?: string; }; export type ZoneFormValuesType = { name: string; + friendly_name: string; inertia: number; loitering_time: number; isFinished: boolean; diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index f10563379..c7cbb50b8 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -280,6 +280,7 @@ export interface CameraConfig { speed_threshold: number; objects: string[]; color: number[]; + friendly_name?: string; }; }; } diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index 4551937d9..c8e5f7543 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -22,6 +22,7 @@ export type TrackingDetailsSequence = { attribute: string; attribute_box?: [number, number, number, number]; zones: string[]; + zones_friendly_names?: string[]; }; class_type: LifecycleClassType; source_id: string; diff --git a/web/src/utils/lifecycleUtil.ts b/web/src/utils/lifecycleUtil.ts index d672445ff..7ed90c5f8 100644 --- a/web/src/utils/lifecycleUtil.ts +++ b/web/src/utils/lifecycleUtil.ts @@ -1,25 +1,7 @@ import { TrackingDetailsSequence } from "@/types/timeline"; import { t } from "i18next"; import { getTranslatedLabel } from "./i18n"; -import { capitalizeFirstLetter } from "./stringUtil"; - -function formatZonesList(zones: string[]): string { - if (zones.length === 0) return ""; - if (zones.length === 1) return zones[0]; - if (zones.length === 2) { - return t("list.two", { - 0: zones[0], - 1: zones[1], - }); - } - - const separatorWithSpace = t("list.separatorWithSpace", { ns: "common" }); - const allButLast = zones.slice(0, -1).join(separatorWithSpace); - return t("list.many", { - items: allButLast, - last: zones[zones.length - 1], - }); -} +import { capitalizeFirstLetter, formatList } from "./stringUtil"; export function getLifecycleItemDescription( lifecycleItem: TrackingDetailsSequence, @@ -42,7 +24,9 @@ export function getLifecycleItemDescription( return t("trackingDetails.lifecycleItemDesc.entered_zone", { ns: "views/explore", label, - zones: formatZonesList(lifecycleItem.data.zones), + zones: formatList( + lifecycleItem.data.zones_friendly_names ?? lifecycleItem.data.zones, + ), }); case "active": return t("trackingDetails.lifecycleItemDesc.active", { diff --git a/web/src/utils/stringUtil.ts b/web/src/utils/stringUtil.ts index 0fd34b45b..568ca85d2 100644 --- a/web/src/utils/stringUtil.ts +++ b/web/src/utils/stringUtil.ts @@ -1,3 +1,5 @@ +import { t } from "i18next"; + export const capitalizeFirstLetter = (text: string): string => { return text.charAt(0).toUpperCase() + text.slice(1); }; @@ -45,3 +47,29 @@ export function generateFixedHash(name: string, prefix: string = "id"): string { export function isValidId(name: string): boolean { return /^[a-zA-Z0-9_-]+$/.test(name) && !/^\d+$/.test(name); } + +/** + * Formats a list of strings into a human-readable format with proper localization. + * Handles different cases for empty, single-item, two-item, and multi-item lists. + * + * @param item - The array of strings to format + * @returns A formatted string representation of the list + */ +export function formatList(item: string[]): string { + if (item.length === 0) return ""; + if (item.length === 1) return item[0]; + if (item.length === 2) { + return t("list.two", { + 0: item[0], + 1: item[1], + ns: "common", + }); + } + + const separatorWithSpace = t("list.separatorWithSpace", { ns: "common" }); + const allButLast = item.slice(0, -1).join(separatorWithSpace); + return t("list.many", { + items: allButLast, + last: item[item.length - 1], + }); +} diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index c56af0eb7..a934e1cb9 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -63,7 +63,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { DetailStreamProvider } from "@/context/detail-stream-context"; import { GenAISummaryDialog } from "@/components/overlay/chip/GenAISummaryChip"; diff --git a/web/src/views/settings/AuthenticationView.tsx b/web/src/views/settings/AuthenticationView.tsx index db5f22555..5c11d8914 100644 --- a/web/src/views/settings/AuthenticationView.tsx +++ b/web/src/views/settings/AuthenticationView.tsx @@ -36,7 +36,7 @@ import EditRoleCamerasDialog from "@/components/overlay/EditRoleCamerasDialog"; import { useTranslation } from "react-i18next"; import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog"; import { Separator } from "@/components/ui/separator"; -import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; type AuthenticationViewProps = { section?: "users" | "roles"; diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index 4db384e0a..52d5879e1 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -18,7 +18,7 @@ import { } from "@/components/ui/select"; import { IoMdArrowRoundBack } from "react-icons/io"; import { isDesktop } from "react-device-detect"; -import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { Switch } from "@/components/ui/switch"; import { Trans } from "react-i18next"; import { Separator } from "@/components/ui/separator"; diff --git a/web/src/views/settings/CameraSettingsView.tsx b/web/src/views/settings/CameraSettingsView.tsx index dae7c3365..dd384b621 100644 --- a/web/src/views/settings/CameraSettingsView.tsx +++ b/web/src/views/settings/CameraSettingsView.tsx @@ -23,7 +23,6 @@ import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import axios from "axios"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; -import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { MdCircle } from "react-icons/md"; import { cn } from "@/lib/utils"; import { Trans, useTranslation } from "react-i18next"; @@ -42,6 +41,8 @@ import CameraWizardDialog from "@/components/settings/CameraWizardDialog"; import { IoMdArrowRoundBack } from "react-icons/io"; import { isDesktop } from "react-device-detect"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import { resolveZoneName } from "@/hooks/use-zone-friendly-name"; +import { formatList } from "@/utils/stringUtil"; type CameraSettingsViewProps = { selectedCamera: string; @@ -86,40 +87,47 @@ export default function CameraSettingsView({ // zones and labels + const getZoneName = useCallback( + (zoneId: string, cameraId?: string) => + resolveZoneName(config, zoneId, cameraId), + [config], + ); + const zones = useMemo(() => { if (cameraConfig) { return Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ camera: cameraConfig.name, name, + friendly_name: getZoneName(name, cameraConfig.name), objects: zoneData.objects, color: zoneData.color, })); } - }, [cameraConfig]); + }, [cameraConfig, getZoneName]); const alertsLabels = useMemo(() => { return cameraConfig?.review.alerts.labels - ? cameraConfig.review.alerts.labels - .map((label) => + ? formatList( + cameraConfig.review.alerts.labels.map((label) => getTranslatedLabel( label, cameraConfig?.audio?.listen?.includes(label) ? "audio" : "object", ), - ) - .join(", ") + ), + ) : ""; }, [cameraConfig]); const detectionsLabels = useMemo(() => { return cameraConfig?.review.detections.labels - ? cameraConfig.review.detections.labels - .map((label) => + ? formatList( + cameraConfig.review.detections.labels.map((label) => getTranslatedLabel( label, cameraConfig?.audio?.listen?.includes(label) ? "audio" : "object", ), - ) - .join(", ") + ), + ) : ""; }, [cameraConfig]); @@ -526,7 +534,7 @@ export default function CameraSettingsView({ /> - {zone.name.replaceAll("_", " ")} + {zone.friendly_name} )} @@ -548,14 +556,11 @@ export default function CameraSettingsView({ "cameraReview.reviewClassification.zoneObjectAlertsTips", { alertsLabels, - zone: watchedAlertsZones - .map((zone) => - capitalizeFirstLetter(zone).replaceAll( - "_", - " ", - ), - ) - .join(", "), + zone: formatList( + watchedAlertsZones.map((zone) => + getZoneName(zone), + ), + ), cameraName: selectCameraName, }, ) @@ -628,7 +633,7 @@ export default function CameraSettingsView({ /> - {zone.name.replaceAll("_", " ")} + {zone.friendly_name} )} @@ -667,14 +672,11 @@ export default function CameraSettingsView({ i18nKey="cameraReview.reviewClassification.zoneObjectDetectionsTips.text" values={{ detectionsLabels, - zone: watchedDetectionsZones - .map((zone) => - capitalizeFirstLetter(zone).replaceAll( - "_", - " ", - ), - ) - .join(", "), + zone: formatList( + watchedDetectionsZones.map((zone) => + getZoneName(zone), + ), + ), cameraName: selectCameraName, }} ns="views/settings" @@ -684,14 +686,11 @@ export default function CameraSettingsView({ i18nKey="cameraReview.reviewClassification.zoneObjectDetectionsTips.notSelectDetections" values={{ detectionsLabels, - zone: watchedDetectionsZones - .map((zone) => - capitalizeFirstLetter(zone).replaceAll( - "_", - " ", - ), - ) - .join(", "), + zone: formatList( + watchedDetectionsZones.map((zone) => + getZoneName(zone), + ), + ), cameraName: selectCameraName, }} ns="views/settings" diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 20e248070..52af94354 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -23,7 +23,7 @@ import { SelectTrigger, } from "@/components/ui/select"; import { useDocDomain } from "@/hooks/use-doc-domain"; -import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; type FrigatePlusModel = { id: string; diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx index 956ce3f95..27c542e87 100644 --- a/web/src/views/settings/MasksAndZonesView.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -229,6 +229,7 @@ export default function MasksAndZonesView({ typeIndex: index, camera: cameraConfig.name, name, + friendly_name: zoneData.friendly_name, objects: zoneData.objects, points: interpolatePoints( parseCoordinates(zoneData.coordinates), diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index b3a049418..6280ca6a8 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -45,7 +45,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Trans, useTranslation } from "react-i18next"; import { useDateLocale } from "@/hooks/use-date-locale"; import { useDocDomain } from "@/hooks/use-doc-domain"; -import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { cn } from "@/lib/utils"; @@ -476,7 +476,7 @@ export default function NotificationView({