mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-16 03:52:09 +03:00
refactor: Refactor camera nickname
This commit is contained in:
parent
856aab8e6e
commit
c7c03a64bc
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
36
web/src/components/camera/CameraNameLabel.tsx
Normal file
36
web/src/components/camera/CameraNameLabel.tsx
Normal file
@ -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<typeof LabelPrimitive.Root>,
|
||||
VariantProps<typeof labelVariants> {
|
||||
camera?: string | CameraConfig;
|
||||
}
|
||||
|
||||
const CameraNameLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
CameraNameLabelProps
|
||||
>(({ className, camera, ...props }, ref) => {
|
||||
const displayName = useCameraNickname(camera);
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
>
|
||||
{displayName}
|
||||
</LabelPrimitive.Root>
|
||||
);
|
||||
});
|
||||
CameraNameLabel.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { CameraNameLabel };
|
||||
@ -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) => (
|
||||
<FormControl key={camera}>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<Label
|
||||
<CameraNameLabel
|
||||
className="mx-2 w-full cursor-pointer text-primary smart-capitalize"
|
||||
htmlFor={camera.replaceAll("_", " ")}
|
||||
>
|
||||
{camera.replaceAll("_", " ")}
|
||||
</Label>
|
||||
camera={camera}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
{camera !== "birdseye" && (
|
||||
|
||||
@ -189,7 +189,8 @@ export function CamerasFilterContent({
|
||||
<FilterSwitch
|
||||
key={item}
|
||||
isChecked={currentCameras?.includes(item) ?? false}
|
||||
label={item.replaceAll("_", " ")}
|
||||
label={item}
|
||||
isCameraName={true}
|
||||
disabled={
|
||||
mainCamera !== undefined &&
|
||||
currentCameras !== undefined &&
|
||||
|
||||
@ -1,26 +1,37 @@
|
||||
import { Switch } from "../ui/switch";
|
||||
import { Label } from "../ui/label";
|
||||
import { CameraNameLabel } from "../camera/CameraNameLabel";
|
||||
|
||||
type FilterSwitchProps = {
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
isChecked: boolean;
|
||||
isCameraName?: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
};
|
||||
export default function FilterSwitch({
|
||||
label,
|
||||
disabled = false,
|
||||
isChecked,
|
||||
isCameraName = false,
|
||||
onCheckedChange,
|
||||
}: FilterSwitchProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<Label
|
||||
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
{isCameraName ? (
|
||||
<CameraNameLabel
|
||||
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
|
||||
htmlFor={label}
|
||||
camera={label}
|
||||
/>
|
||||
) : (
|
||||
<Label
|
||||
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Switch
|
||||
id={label}
|
||||
disabled={disabled}
|
||||
|
||||
@ -53,6 +53,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { MdImageSearch } from "react-icons/md";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { CameraNameLabel } from "../camera/CameraNameLabel";
|
||||
|
||||
type InputWithTagsProps = {
|
||||
inputFocused: boolean;
|
||||
@ -826,9 +827,13 @@ export default function InputWithTags({
|
||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm text-green-800 smart-capitalize"
|
||||
>
|
||||
{t("filter.label." + filterType)}:{" "}
|
||||
{filterType === "labels"
|
||||
? getTranslatedLabel(value)
|
||||
: value.replaceAll("_", " ")}
|
||||
{filterType === "labels" ? (
|
||||
getTranslatedLabel(value)
|
||||
) : filterType === "cameras" ? (
|
||||
<CameraNameLabel camera={value} />
|
||||
) : (
|
||||
value.replaceAll("_", " ")
|
||||
)}
|
||||
<button
|
||||
onClick={() =>
|
||||
removeFilter(filterType as FilterType, value)
|
||||
@ -927,9 +932,15 @@ export default function InputWithTags({
|
||||
) : (
|
||||
<>
|
||||
{suggestion} {" ("}
|
||||
{currentFilterType
|
||||
? formatFilterValues(currentFilterType, suggestion)
|
||||
: t("filter.label." + suggestion)}
|
||||
{currentFilterType ? (
|
||||
currentFilterType === "cameras" ? (
|
||||
<CameraNameLabel camera={suggestion} />
|
||||
) : (
|
||||
formatFilterValues(currentFilterType, suggestion)
|
||||
)
|
||||
) : (
|
||||
t("filter.label." + suggestion)
|
||||
)}
|
||||
{")"}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -16,6 +16,7 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "../ui/sonner";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useCameraNickname } from "@/hooks/use-camera-nickname";
|
||||
|
||||
type CameraInfoDialogProps = {
|
||||
camera: CameraConfig;
|
||||
@ -74,6 +75,8 @@ export default function CameraInfoDialog({
|
||||
return b === 0 ? a : gcd(b, a % b);
|
||||
}
|
||||
|
||||
const cameraName = useCameraNickname(camera);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="top-center" />
|
||||
@ -85,7 +88,7 @@ export default function CameraInfoDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="smart-capitalize">
|
||||
{t("cameras.info.cameraProbeInfo", {
|
||||
camera: camera.name.replaceAll("_", " "),
|
||||
camera: cameraName,
|
||||
})}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@ -37,6 +37,7 @@ import ImagePicker from "@/components/overlay/ImagePicker";
|
||||
import { Trigger, TriggerAction, TriggerType } from "@/types/trigger";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { useCameraNickname } from "@/hooks/use-camera-nickname";
|
||||
|
||||
type CreateTriggerDialogProps = {
|
||||
show: boolean;
|
||||
@ -161,6 +162,8 @@ export default function CreateTriggerDialog({
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const cameraName = useCameraNickname(selectedCamera);
|
||||
|
||||
return (
|
||||
<Dialog open={show} onOpenChange={onCancel}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
@ -177,7 +180,9 @@ export default function CreateTriggerDialog({
|
||||
trigger
|
||||
? "triggers.dialog.editTrigger.desc"
|
||||
: "triggers.dialog.createTrigger.desc",
|
||||
{ camera: selectedCamera },
|
||||
{
|
||||
camera: cameraName,
|
||||
},
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -79,6 +79,7 @@ import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import FaceSelectionDialog from "../FaceSelectionDialog";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { CgTranscript } from "react-icons/cg";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
|
||||
const SEARCH_TABS = [
|
||||
"details",
|
||||
@ -864,7 +865,7 @@ function ObjectDetailsTab({
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">{t("details.camera")}</div>
|
||||
<div className="text-sm smart-capitalize">
|
||||
{search.camera.replaceAll("_", " ")}
|
||||
<CameraNameLabel camera={search.camera} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
||||
@ -24,6 +24,7 @@ import { baseUrl } from "@/api/baseUrl";
|
||||
import { PlayerStats } from "./PlayerStats";
|
||||
import { LuVideoOff } from "react-icons/lu";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useCameraNickname } from "@/hooks/use-camera-nickname";
|
||||
|
||||
type LivePlayerProps = {
|
||||
cameraRef?: (ref: HTMLDivElement | null) => void;
|
||||
@ -76,6 +77,7 @@ export default function LivePlayer({
|
||||
|
||||
const internalContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const cameraName = useCameraNickname(cameraConfig);
|
||||
// stats
|
||||
|
||||
const [stats, setStats] = useState<PlayerStatsType>({
|
||||
@ -412,7 +414,7 @@ export default function LivePlayer({
|
||||
<Trans
|
||||
ns="components/player"
|
||||
values={{
|
||||
cameraName: capitalizeFirstLetter(cameraConfig.name),
|
||||
cameraName: cameraName,
|
||||
}}
|
||||
>
|
||||
streamOffline.desc
|
||||
@ -444,7 +446,7 @@ export default function LivePlayer({
|
||||
<Chip
|
||||
className={`z-0 flex items-start justify-between space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
|
||||
>
|
||||
{cameraConfig.name.replaceAll("_", " ")}
|
||||
{cameraName}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -30,6 +30,12 @@ type ConfigSetBody = {
|
||||
config_data: any;
|
||||
update_topic?: string;
|
||||
};
|
||||
const generateFixedHash = (name: string): string => {
|
||||
const encoded = encodeURIComponent(name);
|
||||
const base64 = btoa(encoded);
|
||||
const cleanHash = base64.replace(/[^a-zA-Z0-9]/g, "").substring(0, 8);
|
||||
return `cam_${cleanHash.toLowerCase()}`;
|
||||
};
|
||||
|
||||
const RoleEnum = z.enum(["audio", "detect", "record"]);
|
||||
type Role = z.infer<typeof RoleEnum>;
|
||||
@ -54,10 +60,7 @@ export default function CameraEditForm({
|
||||
z.object({
|
||||
cameraName: z
|
||||
.string()
|
||||
.min(1, { message: t("camera.cameraConfig.nameRequired") })
|
||||
.regex(/^[a-zA-Z0-9_-]+$/, {
|
||||
message: t("camera.cameraConfig.nameInvalid"),
|
||||
}),
|
||||
.min(1, { message: t("camera.cameraConfig.nameRequired") }),
|
||||
enabled: z.boolean(),
|
||||
ffmpeg: z.object({
|
||||
inputs: z
|
||||
@ -101,26 +104,37 @@ export default function CameraEditForm({
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
// Determine available roles for default values
|
||||
const usedRoles = useMemo(() => {
|
||||
const roles = new Set<Role>();
|
||||
if (cameraName && config?.cameras[cameraName]) {
|
||||
const camera = config.cameras[cameraName];
|
||||
camera.ffmpeg?.inputs?.forEach((input) => {
|
||||
input.roles.forEach((role) => roles.add(role as Role));
|
||||
});
|
||||
const cameraInfo = useMemo(() => {
|
||||
if (!cameraName || !config?.cameras[cameraName]) {
|
||||
return {
|
||||
nickname: undefined,
|
||||
name: cameraName || "",
|
||||
roles: new Set<Role>(),
|
||||
};
|
||||
}
|
||||
return roles;
|
||||
|
||||
const camera = config.cameras[cameraName];
|
||||
const roles = new Set<Role>();
|
||||
|
||||
camera.ffmpeg?.inputs?.forEach((input) => {
|
||||
input.roles.forEach((role) => roles.add(role as Role));
|
||||
});
|
||||
|
||||
return {
|
||||
nickname: camera?.nickname || cameraName,
|
||||
name: cameraName,
|
||||
roles,
|
||||
};
|
||||
}, [cameraName, config]);
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
cameraName: cameraName || "",
|
||||
cameraName: cameraInfo?.nickname || cameraName || "",
|
||||
enabled: true,
|
||||
ffmpeg: {
|
||||
inputs: [
|
||||
{
|
||||
path: "",
|
||||
roles: usedRoles.has("detect") ? [] : ["detect"],
|
||||
roles: cameraInfo.roles.has("detect") ? [] : ["detect"],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -154,10 +168,19 @@ export default function CameraEditForm({
|
||||
|
||||
const saveCameraConfig = (values: FormValues) => {
|
||||
setIsLoading(true);
|
||||
let finalCameraName = values.cameraName;
|
||||
let nickname: string | undefined = undefined;
|
||||
const isValidName = /^[a-zA-Z0-9_-]+$/.test(values.cameraName);
|
||||
if (!isValidName) {
|
||||
finalCameraName = generateFixedHash(finalCameraName);
|
||||
nickname = values.cameraName;
|
||||
}
|
||||
|
||||
const configData: ConfigSetBody["config_data"] = {
|
||||
cameras: {
|
||||
[values.cameraName]: {
|
||||
[finalCameraName]: {
|
||||
enabled: values.enabled,
|
||||
...(nickname && { nickname }),
|
||||
ffmpeg: {
|
||||
inputs: values.ffmpeg.inputs.map((input) => ({
|
||||
path: input.path,
|
||||
@ -175,7 +198,7 @@ export default function CameraEditForm({
|
||||
|
||||
// Add update_topic for new cameras
|
||||
if (!cameraName) {
|
||||
requestBody.update_topic = `config/cameras/${values.cameraName}/add`;
|
||||
requestBody.update_topic = `config/cameras/${finalCameraName}/add`;
|
||||
}
|
||||
|
||||
axios
|
||||
@ -209,7 +232,11 @@ export default function CameraEditForm({
|
||||
};
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
if (cameraName && values.cameraName !== cameraName) {
|
||||
if (
|
||||
cameraName &&
|
||||
values.cameraName !== cameraName &&
|
||||
values.cameraName !== cameraInfo?.nickname
|
||||
) {
|
||||
// If camera name changed, delete old camera config
|
||||
const deleteRequestBody: ConfigSetBody = {
|
||||
requires_restart: 1,
|
||||
|
||||
@ -33,6 +33,7 @@ import { Link } from "react-router-dom";
|
||||
import { LiveStreamMetadata } from "@/types/live";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { useCameraNickname } from "@/hooks/use-camera-nickname";
|
||||
|
||||
type CameraStreamingDialogProps = {
|
||||
camera: string;
|
||||
@ -56,6 +57,8 @@ export function CameraStreamingDialog({
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const cameraName = useCameraNickname(camera);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [streamName, setStreamName] = useState(
|
||||
@ -190,7 +193,7 @@ export function CameraStreamingDialog({
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle className="smart-capitalize">
|
||||
{t("group.camera.setting.title", {
|
||||
cameraName: camera.replaceAll("_", " "),
|
||||
cameraName: cameraName,
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
21
web/src/hooks/use-camera-nickname.ts
Normal file
21
web/src/hooks/use-camera-nickname.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
export function useCameraNickname(
|
||||
cameraId: string | CameraConfig | undefined,
|
||||
): string {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const name = useMemo(() => {
|
||||
if (typeof cameraId === "object" && cameraId !== null) {
|
||||
const camera = cameraId as CameraConfig;
|
||||
return camera?.nickname || camera?.name.replaceAll("_", " ");
|
||||
} else {
|
||||
const camera = config?.cameras?.[String(cameraId)];
|
||||
return camera?.nickname || String(cameraId).replaceAll("_", " ");
|
||||
}
|
||||
}, [config, cameraId]);
|
||||
|
||||
return name;
|
||||
}
|
||||
@ -46,6 +46,7 @@ import { isPWA } from "@/utils/isPWA";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TriggerView from "@/views/settings/TriggerView";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
|
||||
const allSettingsViews = [
|
||||
"ui",
|
||||
@ -351,9 +352,11 @@ function CameraSelectButton({
|
||||
>
|
||||
<FaVideo className="text-background dark:text-primary" />
|
||||
<div className="hidden text-background dark:text-primary md:block">
|
||||
{selectedCamera == undefined
|
||||
? t("cameraSetting.noCamera")
|
||||
: selectedCamera.replaceAll("_", " ")}
|
||||
{selectedCamera == undefined ? (
|
||||
t("cameraSetting.noCamera")
|
||||
) : (
|
||||
<CameraNameLabel camera={selectedCamera} />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@ -376,7 +379,8 @@ function CameraSelectButton({
|
||||
<FilterSwitch
|
||||
key={item.name}
|
||||
isChecked={item.name === selectedCamera}
|
||||
label={item.name.replaceAll("_", " ")}
|
||||
label={item.name}
|
||||
isCameraName={true}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked && (isEnabled || isCameraSettingsPage)) {
|
||||
setSelectedCamera(item.name);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
@ -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,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
|
||||
type CameraMetricsProps = {
|
||||
lastUpdated: number;
|
||||
@ -271,7 +272,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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user