mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-07 14:04:10 +03:00
feat: add zones friendly name
This commit is contained in:
parent
1e50d83d06
commit
2339b5f35d
@ -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."
|
||||
)
|
||||
|
||||
@ -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<typeof LabelPrimitive.Root> {
|
||||
camera?: string | CameraConfig;
|
||||
}
|
||||
|
||||
interface ZoneNameLabelProps
|
||||
extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> {
|
||||
zone: string;
|
||||
camera?: string;
|
||||
}
|
||||
|
||||
const CameraNameLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
CameraNameLabelProps
|
||||
@ -21,4 +28,17 @@ const CameraNameLabel = React.forwardRef<
|
||||
});
|
||||
CameraNameLabel.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { CameraNameLabel };
|
||||
const ZoneNameLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
ZoneNameLabelProps
|
||||
>(({ className, zone, camera, ...props }, ref) => {
|
||||
const displayName = useZoneFriendlyName(zone, camera);
|
||||
return (
|
||||
<LabelPrimitive.Root ref={ref} className={className} {...props}>
|
||||
{displayName}
|
||||
</LabelPrimitive.Root>
|
||||
);
|
||||
});
|
||||
ZoneNameLabel.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { CameraNameLabel, ZoneNameLabel };
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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 (
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
{isCameraName ? (
|
||||
{type === "camera" ? (
|
||||
<CameraNameLabel
|
||||
className={`mx-2 w-full cursor-pointer text-sm font-medium leading-none text-primary smart-capitalize peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${disabled ? "text-secondary-foreground" : ""}`}
|
||||
htmlFor={label}
|
||||
camera={label}
|
||||
/>
|
||||
) : type === "zone" ? (
|
||||
<ZoneNameLabel
|
||||
className={`mx-2 w-full cursor-pointer text-sm font-medium leading-none text-primary smart-capitalize peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${disabled ? "text-secondary-foreground" : ""}`}
|
||||
htmlFor={label}
|
||||
camera={extraValue}
|
||||
zone={label}
|
||||
/>
|
||||
) : (
|
||||
<Label
|
||||
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
|
||||
|
||||
@ -550,7 +550,8 @@ export function GeneralFilterContent({
|
||||
{allZones.map((item) => (
|
||||
<FilterSwitch
|
||||
key={item}
|
||||
label={item.replaceAll("_", " ")}
|
||||
label={item}
|
||||
type={"zone"}
|
||||
isChecked={filter.zones?.includes(item) ?? false}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
|
||||
@ -53,7 +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";
|
||||
import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel";
|
||||
|
||||
type InputWithTagsProps = {
|
||||
inputFocused: boolean;
|
||||
@ -831,6 +831,8 @@ export default function InputWithTags({
|
||||
getTranslatedLabel(value)
|
||||
) : filterType === "cameras" ? (
|
||||
<CameraNameLabel camera={value} />
|
||||
) : filterType === "zones" ? (
|
||||
<ZoneNameLabel zone={value} />
|
||||
) : (
|
||||
value.replaceAll("_", " ")
|
||||
)}
|
||||
@ -934,6 +936,11 @@ export default function InputWithTags({
|
||||
<CameraNameLabel camera={suggestion} />
|
||||
{")"}
|
||||
</>
|
||||
) : currentFilterType === "zones" ? (
|
||||
<>
|
||||
{suggestion} {" ("} <ZoneNameLabel zone={suggestion} />
|
||||
{")"}
|
||||
</>
|
||||
) : (
|
||||
suggestion
|
||||
)
|
||||
@ -943,6 +950,8 @@ export default function InputWithTags({
|
||||
{currentFilterType ? (
|
||||
currentFilterType === "cameras" ? (
|
||||
<CameraNameLabel camera={suggestion} />
|
||||
) : currentFilterType === "zones" ? (
|
||||
<ZoneNameLabel zone={suggestion} />
|
||||
) : (
|
||||
formatFilterValues(currentFilterType, suggestion)
|
||||
)
|
||||
|
||||
@ -47,7 +47,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDateLocale } from "@/hooks/use-date-locale";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { CameraNameLabel } from "../camera/CameraNameLabel";
|
||||
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
||||
|
||||
type LiveContextMenuProps = {
|
||||
className?: string;
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { CameraNameLabel } from "../camera/CameraNameLabel";
|
||||
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||
|
||||
type EditRoleCamerasOverlayProps = {
|
||||
show: boolean;
|
||||
|
||||
@ -4,7 +4,7 @@ import { Button } from "../ui/button";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CameraNameLabel } from "../camera/CameraNameLabel";
|
||||
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
||||
|
||||
type MobileCameraDrawerProps = {
|
||||
allCameras: string[];
|
||||
|
||||
@ -12,6 +12,7 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Event } from "@/types/event";
|
||||
import { resolveZoneName } from "@/hooks/use-zone-friendly-name";
|
||||
|
||||
// Use a small tolerance (10ms) for browsers with seek precision by-design issues
|
||||
const TOLERANCE = 0.01;
|
||||
@ -73,6 +74,10 @@ export default function ObjectTrackOverlay({
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
|
||||
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
|
||||
return zones?.map((zone) => resolveZoneName(config, zone)) ?? [];
|
||||
};
|
||||
|
||||
const timelineResults = useMemo(() => {
|
||||
// Group timeline entries by source_id
|
||||
if (!timelineData) return selectedObjectIds.map(() => []);
|
||||
@ -86,8 +91,19 @@ export default function ObjectTrackOverlay({
|
||||
}
|
||||
|
||||
// Return timeline arrays in the same order as selectedObjectIds
|
||||
return selectedObjectIds.map((id) => grouped[id] || []);
|
||||
}, [selectedObjectIds, timelineData]);
|
||||
return selectedObjectIds.map((id) => {
|
||||
const entries = grouped[id] || [];
|
||||
return entries.map((event) => ({
|
||||
...event,
|
||||
data: {
|
||||
...event.data,
|
||||
zones_friendly_names: config
|
||||
? getZonesFriendlyNames(event.data?.zones, config)
|
||||
: [],
|
||||
},
|
||||
}));
|
||||
});
|
||||
}, [selectedObjectIds, timelineData, config]);
|
||||
|
||||
const typeColorMap = useMemo(
|
||||
() => ({
|
||||
|
||||
@ -8,6 +8,9 @@ import {
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { resolveZoneName } from "@/hooks/use-zone-friendly-name";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
|
||||
type ObjectPathProps = {
|
||||
positions?: Position[];
|
||||
@ -42,16 +45,30 @@ export function ObjectPath({
|
||||
visible = true,
|
||||
}: ObjectPathProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const getAbsolutePositions = useCallback(() => {
|
||||
if (!imgRef.current || !positions) return [];
|
||||
const imgRect = imgRef.current.getBoundingClientRect();
|
||||
return positions.map((pos) => ({
|
||||
x: pos.x * imgRect.width,
|
||||
y: pos.y * imgRect.height,
|
||||
timestamp: pos.timestamp,
|
||||
lifecycle_item: pos.lifecycle_item,
|
||||
}));
|
||||
}, [positions, imgRef]);
|
||||
return positions.map((pos) => {
|
||||
if (config && pos.lifecycle_item?.data?.zones) {
|
||||
pos.lifecycle_item = {
|
||||
...pos.lifecycle_item,
|
||||
data: {
|
||||
...pos.lifecycle_item.data,
|
||||
zones_friendly_names: pos.lifecycle_item.data.zones.map((zone) => {
|
||||
return resolveZoneName(config, zone);
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: pos.x * imgRect.width,
|
||||
y: pos.y * imgRect.height,
|
||||
timestamp: pos.timestamp,
|
||||
lifecycle_item: pos.lifecycle_item,
|
||||
};
|
||||
});
|
||||
}, [imgRef, positions, config]);
|
||||
|
||||
const generateStraightPath = useCallback((points: Position[]) => {
|
||||
if (!points || points.length < 2) return "";
|
||||
|
||||
@ -77,7 +77,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";
|
||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||
import { PiPath } from "react-icons/pi";
|
||||
import Heading from "@/components/ui/heading";
|
||||
|
||||
|
||||
@ -429,7 +429,8 @@ export function ZoneFilterContent({
|
||||
{allZones.map((item) => (
|
||||
<FilterSwitch
|
||||
key={item}
|
||||
label={item.replaceAll("_", " ")}
|
||||
label={item}
|
||||
type={"zone"}
|
||||
isChecked={zones?.includes(item) ?? false}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
|
||||
@ -261,7 +261,9 @@ export default function PolygonItem({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<p className="cursor-default">{polygon.name}</p>
|
||||
<p className="cursor-default">
|
||||
{polygon.friendly_name ?? polygon.name}
|
||||
</p>
|
||||
</div>
|
||||
<AlertDialog
|
||||
open={deleteDialogOpen}
|
||||
@ -278,7 +280,7 @@ export default function PolygonItem({
|
||||
ns="views/settings"
|
||||
values={{
|
||||
type: polygon.type.replace("_", " "),
|
||||
name: polygon.name,
|
||||
name: polygon.friendly_name ?? polygon.name,
|
||||
}}
|
||||
>
|
||||
masksAndZones.form.polygonDrawing.delete.desc
|
||||
|
||||
@ -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,38 @@ 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",
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||
.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 +271,7 @@ export default function ZoneEditPane({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
name: polygon?.name ?? "",
|
||||
friendly_name: polygon?.friendly_name ?? "",
|
||||
inertia:
|
||||
polygon?.camera &&
|
||||
polygon?.name &&
|
||||
@ -286,6 +311,7 @@ export default function ZoneEditPane({
|
||||
async (
|
||||
{
|
||||
name: zoneName,
|
||||
friendly_name,
|
||||
inertia,
|
||||
loitering_time,
|
||||
objects: form_objects,
|
||||
@ -415,9 +441,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 +458,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 +572,16 @@ export default function ZoneEditPane({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-2 space-y-6">
|
||||
<FormField
|
||||
<NameAndIdFields
|
||||
type="zone"
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("masksAndZones.zones.name.title")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
placeholder={t("masksAndZones.zones.name.inputPlaceHolder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("masksAndZones.zones.name.tips")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
nameField="friendly_name"
|
||||
idField="name"
|
||||
nameLabel={t("masksAndZones.zones.name.title")}
|
||||
nameDescription={t("masksAndZones.zones.name.tips")}
|
||||
placeholderName={t("masksAndZones.zones.name.inputPlaceHolder")}
|
||||
/>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
41
web/src/hooks/use-zone-friendly-name.ts
Normal file
41
web/src/hooks/use-zone-friendly-name.ts
Normal file
@ -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<FrigateConfig>("config");
|
||||
|
||||
const name = useMemo(
|
||||
() => resolveZoneName(config, zoneId, cameraId),
|
||||
[config, cameraId, zoneId],
|
||||
);
|
||||
|
||||
return name;
|
||||
}
|
||||
@ -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,
|
||||
@ -642,7 +642,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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -280,6 +280,7 @@ export interface CameraConfig {
|
||||
speed_threshold: number;
|
||||
objects: string[];
|
||||
color: number[];
|
||||
friendly_name?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,7 @@ 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";
|
||||
|
||||
type CameraSettingsViewProps = {
|
||||
selectedCamera: string;
|
||||
@ -86,16 +86,23 @@ 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
|
||||
@ -526,7 +533,7 @@ export default function CameraSettingsView({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal smart-capitalize">
|
||||
{zone.name.replaceAll("_", " ")}
|
||||
{zone.friendly_name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -548,14 +555,9 @@ export default function CameraSettingsView({
|
||||
"cameraReview.reviewClassification.zoneObjectAlertsTips",
|
||||
{
|
||||
alertsLabels,
|
||||
zone: watchedAlertsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
zone: watchedAlertsZones.map((zone) =>
|
||||
getZoneName(zone),
|
||||
),
|
||||
cameraName: selectCameraName,
|
||||
},
|
||||
)
|
||||
@ -628,7 +630,7 @@ export default function CameraSettingsView({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal smart-capitalize">
|
||||
{zone.name.replaceAll("_", " ")}
|
||||
{zone.friendly_name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -667,14 +669,9 @@ export default function CameraSettingsView({
|
||||
i18nKey="cameraReview.reviewClassification.zoneObjectDetectionsTips.text"
|
||||
values={{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
zone: watchedDetectionsZones.map((zone) =>
|
||||
getZoneName(zone),
|
||||
),
|
||||
cameraName: selectCameraName,
|
||||
}}
|
||||
ns="views/settings"
|
||||
@ -684,14 +681,9 @@ export default function CameraSettingsView({
|
||||
i18nKey="cameraReview.reviewClassification.zoneObjectDetectionsTips.notSelectDetections"
|
||||
values={{
|
||||
detectionsLabels,
|
||||
zone: watchedDetectionsZones
|
||||
.map((zone) =>
|
||||
capitalizeFirstLetter(zone).replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
),
|
||||
)
|
||||
.join(", "),
|
||||
zone: watchedDetectionsZones.map((zone) =>
|
||||
getZoneName(zone),
|
||||
),
|
||||
cameraName: selectCameraName,
|
||||
}}
|
||||
ns="views/settings"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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({
|
||||
<FilterSwitch
|
||||
key={camera.name}
|
||||
label={camera.name}
|
||||
isCameraName={true}
|
||||
type={"camera"}
|
||||
isChecked={field.value?.includes(
|
||||
camera.name,
|
||||
)}
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
|
||||
|
||||
type CameraMetricsProps = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user