feat: Add camera nickname (#19567)

* refactor: Refactor camera nickname

* fix: fix cameraNameLabel visually

* chore: The Explore search function also displays the Camera's nickname in English

* chore: add mobile page camera nickname

* feat: webpush support camera nickname

* fix: fix storage camera name is null

* chore: fix review detail and context menu camera nickname

* chore: fix use-stats and notification setting camera nickname

* fix: fix stats camera if not nickname need capitalize

* fix: fix debug page open camera web ui i18n and camera nickname support

* fix: fix camera metrics not use nickname

* refactor: refactor use-camera-nickname hook.
This commit is contained in:
GuoQing Liu
2025-08-27 01:15:01 +08:00
committed by GitHub
parent 195f705616
commit d3af748366
31 changed files with 276 additions and 99 deletions

View File

@@ -64,6 +64,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
type RecordingViewProps = {
startCamera: string;
@@ -719,7 +720,7 @@ export function RecordingView({
</div>
</TooltipTrigger>
<TooltipContent className="smart-capitalize">
{cam.replaceAll("_", " ")}
<CameraNameLabel camera={cam} />
</TooltipContent>
</Tooltip>
);

View File

@@ -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({
<SelectValue placeholder={t("camera.selectCamera")} />
</SelectTrigger>
<SelectContent>
{cameras.map((camera) => (
<SelectItem key={camera} value={camera}>
{capitalizeFirstLetter(camera.replaceAll("_", " "))}
</SelectItem>
))}
{cameras.map((camera) => {
return (
<SelectItem key={camera} value={camera}>
<CameraNameLabel camera={camera} />
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
@@ -612,18 +618,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,
},
)}
</div>
@@ -735,9 +737,7 @@ export default function CameraSettingsView({
),
)
.join(", "),
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
cameraName: selectCameraName,
}}
ns="views/settings"
/>
@@ -754,9 +754,7 @@ export default function CameraSettingsView({
),
)
.join(", "),
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
cameraName: selectCameraName,
}}
ns="views/settings"
/>
@@ -766,9 +764,7 @@ export default function CameraSettingsView({
i18nKey="camera.reviewClassification.objectDetectionsTips"
values={{
detectionsLabels,
cameraName: capitalizeFirstLetter(
cameraConfig?.name ?? "",
).replaceAll("_", " "),
cameraName: selectCameraName,
}}
ns="views/settings"
/>

View File

@@ -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"
>
<td className="px-4 py-2">{name}</td>
<td className="px-4 py-2">
<CameraNameLabel camera={name} />
</td>
<td className="px-4 py-2 text-center">
{camera.snapshots.enabled ? (
<CheckCircle2 className="mx-auto size-5 text-green-500" />

View File

@@ -11,7 +11,6 @@ import {
} from "@/components/ui/form";
import Heading from "@/components/ui/heading";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Toaster } from "@/components/ui/sonner";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
@@ -46,6 +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";
const NOTIFICATION_SERVICE_WORKER = "notifications-worker.js";
@@ -464,7 +464,8 @@ export default function NotificationView({
{allCameras?.map((camera) => (
<FilterSwitch
key={camera.name}
label={camera.name.replaceAll("_", " ")}
label={camera.name}
isCameraName={true}
isChecked={field.value?.includes(camera.name)}
onCheckedChange={(checked) => {
setChangedValue(true);
@@ -697,12 +698,11 @@ export function CameraNotificationSwitch({
<LuX className="size-6 text-danger" />
)}
<div className="flex flex-col">
<Label
<CameraNameLabel
className="text-md cursor-pointer text-primary smart-capitalize"
htmlFor="camera"
>
{camera.replaceAll("_", " ")}
</Label>
camera={camera}
/>
{!isSuspended ? (
<div className="flex flex-row items-center gap-2 text-sm text-success">

View File

@@ -30,6 +30,7 @@ import { isDesktop } from "react-device-detect";
import { Trans, useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { getTranslatedLabel } from "@/utils/i18n";
import { useCameraNickname } from "@/hooks/use-camera-nickname";
import { AudioLevelGraph } from "@/components/audio/AudioLevelGraph";
import { useWs } from "@/api/ws";
@@ -128,6 +129,8 @@ export default function ObjectSettingsView({
}
}, [config, selectedCamera]);
const cameraName = useCameraNickname(cameraConfig);
const { objects, audio_detections } = useCameraActivity(
cameraConfig ?? ({} as CameraConfig),
);
@@ -186,7 +189,9 @@ export default function ObjectSettingsView({
rel="noopener noreferrer"
className="inline"
>
Open {capitalizeFirstLetter(cameraConfig.name)}'s Web UI
{t("debug.openCameraWebUI", {
camera: cameraName,
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>

View File

@@ -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<string>();
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")}
</Heading>
<p className="text-sm text-muted-foreground">
{t("triggers.management.desc", { camera: selectedCamera })}
{t("triggers.management.desc", {
camera: cameraName,
})}
</p>
</div>
<Button

View File

@@ -4,7 +4,7 @@ import CameraInfoDialog from "@/components/overlay/CameraInfoDialog";
import { Skeleton } from "@/components/ui/skeleton";
import { FrigateConfig } from "@/types/frigateConfig";
import { FrigateStats } from "@/types/stats";
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { MdInfo } from "react-icons/md";
import {
Tooltip,
@@ -13,6 +13,8 @@ import {
} from "@/components/ui/tooltip";
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
import { resolveCameraName } from "@/hooks/use-camera-nickname";
type CameraMetricsProps = {
lastUpdated: number;
@@ -26,6 +28,11 @@ export default function CameraMetrics({
const { t } = useTranslation(["views/system"]);
// camera info dialog
const getCameraName = useCallback(
(cameraId: string) => resolveCameraName(config, cameraId),
[config],
);
const [showCameraInfoDialog, setShowCameraInfoDialog] = useState(false);
const [probeCameraName, setProbeCameraName] = useState<string>();
@@ -142,7 +149,7 @@ export default function CameraMetrics({
}
if (!(key in series)) {
const camName = key.replaceAll("_", " ");
const camName = getCameraName(key);
series[key] = {};
series[key]["ffmpeg"] = {
name: t("cameras.label.cameraFfmpeg", { camName: camName }),
@@ -173,7 +180,7 @@ export default function CameraMetrics({
});
});
return series;
}, [config, statsHistory, t]);
}, [config, getCameraName, statsHistory, t]);
const cameraFpsSeries = useMemo(() => {
if (!statsHistory) {
@@ -193,7 +200,7 @@ export default function CameraMetrics({
Object.entries(stats.cameras).forEach(([key, camStats]) => {
if (!(key in series)) {
const camName = key.replaceAll("_", " ");
const camName = getCameraName(key);
series[key] = {};
series[key]["fps"] = {
name: t("cameras.label.cameraFramesPerSecond", {
@@ -230,7 +237,7 @@ export default function CameraMetrics({
});
});
return series;
}, [statsHistory, t]);
}, [getCameraName, statsHistory, t]);
useEffect(() => {
if (!showCameraInfoDialog) {
@@ -276,7 +283,7 @@ export default function CameraMetrics({
<div className="flex w-full flex-col gap-3">
<div className="flex flex-row items-center justify-between">
<div className="text-sm font-medium text-muted-foreground smart-capitalize">
{camera.name.replaceAll("_", " ")}
<CameraNameLabel camera={camera} />
</div>
<Tooltip>
<TooltipTrigger>