mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-16 13:47:07 +02:00
Camera groups (#10223)
* Add camera group config * Add saving of camera group selection * Implement camera groups in config and live view * Fix warnings * Add tooltips to camera group items on desktop * Add camera groups to the filters for events * Fix tooltips and group selection * Cleanup
This commit is contained in:
parent
38e76666e7
commit
b4b2162ada
@ -1003,6 +1003,16 @@ class LoggerConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CameraGroupConfig(FrigateBaseModel):
|
||||||
|
"""Represents a group of cameras."""
|
||||||
|
|
||||||
|
cameras: list[str] = Field(
|
||||||
|
default_factory=list, title="List of cameras in this group."
|
||||||
|
)
|
||||||
|
icon: str = Field(default="generic", title="Icon that represents camera group.")
|
||||||
|
order: int = Field(default=0, title="Sort order for group.")
|
||||||
|
|
||||||
|
|
||||||
def verify_config_roles(camera_config: CameraConfig) -> None:
|
def verify_config_roles(camera_config: CameraConfig) -> None:
|
||||||
"""Verify that roles are setup in the config correctly."""
|
"""Verify that roles are setup in the config correctly."""
|
||||||
assigned_roles = list(
|
assigned_roles = list(
|
||||||
@ -1157,6 +1167,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
default_factory=DetectConfig, title="Global object tracking configuration."
|
default_factory=DetectConfig, title="Global object tracking configuration."
|
||||||
)
|
)
|
||||||
cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
|
cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
|
||||||
|
camera_groups: Dict[str, CameraGroupConfig] = Field(
|
||||||
|
default_factory=CameraGroupConfig, title="Camera group configuration"
|
||||||
|
)
|
||||||
timestamp_style: TimestampStyleConfig = Field(
|
timestamp_style: TimestampStyleConfig = Field(
|
||||||
default_factory=TimestampStyleConfig,
|
default_factory=TimestampStyleConfig,
|
||||||
title="Global timestamp style configuration.",
|
title="Global timestamp style configuration.",
|
||||||
|
102
web/src/components/filter/CameraGroupSelector.tsx
Normal file
102
web/src/components/filter/CameraGroupSelector.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { MdHome } from "react-icons/md";
|
||||||
|
import useOverlayState from "@/hooks/use-overlay-state";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
import { getIconForGroup } from "@/utils/iconUtil";
|
||||||
|
|
||||||
|
type CameraGroupSelectorProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// tooltip
|
||||||
|
|
||||||
|
const [tooltip, setTooltip] = useState<string>();
|
||||||
|
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
||||||
|
const showTooltip = useCallback(
|
||||||
|
(newTooltip: string | undefined) => {
|
||||||
|
if (!newTooltip) {
|
||||||
|
setTooltip(newTooltip);
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTimeoutId(setTimeout(() => setTooltip(newTooltip), 500));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[timeoutId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// groups
|
||||||
|
|
||||||
|
const [group, setGroup] = useOverlayState("cameraGroup");
|
||||||
|
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(config.camera_groups).sort(
|
||||||
|
(a, b) => a[1].order - b[1].order,
|
||||||
|
);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-start gap-2 ${className ?? ""} ${isDesktop ? "flex-col" : ""}`}
|
||||||
|
>
|
||||||
|
<Tooltip open={tooltip == "home"}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={
|
||||||
|
group == undefined
|
||||||
|
? "text-selected bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
||||||
|
: "text-muted-foreground bg-secondary focus:text-muted-foreground focus:bg-secondary"
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
onMouseEnter={() => (isDesktop ? showTooltip("home") : null)}
|
||||||
|
onMouseLeave={() => (isDesktop ? showTooltip(undefined) : null)}
|
||||||
|
>
|
||||||
|
<MdHome className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="capitalize" side="right">
|
||||||
|
Home
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{groups.map(([name, config]) => {
|
||||||
|
return (
|
||||||
|
<Tooltip key={name} open={tooltip == name}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={
|
||||||
|
group == name
|
||||||
|
? "text-selected bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
||||||
|
: "text-muted-foreground bg-secondary"
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
onClick={() => setGroup(name, group != undefined)}
|
||||||
|
onMouseEnter={() => (isDesktop ? showTooltip(name) : null)}
|
||||||
|
onMouseLeave={() => (isDesktop ? showTooltip(undefined) : null)}
|
||||||
|
>
|
||||||
|
{getIconForGroup(config.icon)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="capitalize" side="right">
|
||||||
|
{name}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { LuCheck, LuVideo } from "react-icons/lu";
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -16,6 +16,8 @@ import { ReviewFilter } from "@/types/review";
|
|||||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||||
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||||
import { FaCalendarAlt, FaFilter, FaVideo } from "react-icons/fa";
|
import { FaCalendarAlt, FaFilter, FaVideo } from "react-icons/fa";
|
||||||
|
import { getIconTypeForGroup } from "@/utils/iconUtil";
|
||||||
|
import { IconType } from "react-icons";
|
||||||
|
|
||||||
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||||
|
|
||||||
@ -57,6 +59,16 @@ export default function ReviewFilterGroup({
|
|||||||
[config, allLabels],
|
[config, allLabels],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(config.camera_groups).sort(
|
||||||
|
(a, b) => a[1].order - b[1].order,
|
||||||
|
);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
// handle updating filters
|
// handle updating filters
|
||||||
|
|
||||||
const onUpdateSelectedDay = useCallback(
|
const onUpdateSelectedDay = useCallback(
|
||||||
@ -74,6 +86,7 @@ export default function ReviewFilterGroup({
|
|||||||
<div>
|
<div>
|
||||||
<CamerasFilterButton
|
<CamerasFilterButton
|
||||||
allCameras={filterValues.cameras}
|
allCameras={filterValues.cameras}
|
||||||
|
groups={groups}
|
||||||
selectedCameras={filter?.cameras}
|
selectedCameras={filter?.cameras}
|
||||||
updateCameraFilter={(newCameras) => {
|
updateCameraFilter={(newCameras) => {
|
||||||
onUpdateFilter({ ...filter, cameras: newCameras });
|
onUpdateFilter({ ...filter, cameras: newCameras });
|
||||||
@ -102,11 +115,13 @@ export default function ReviewFilterGroup({
|
|||||||
|
|
||||||
type CameraFilterButtonProps = {
|
type CameraFilterButtonProps = {
|
||||||
allCameras: string[];
|
allCameras: string[];
|
||||||
|
groups: [string, CameraGroupConfig][];
|
||||||
selectedCameras: string[] | undefined;
|
selectedCameras: string[] | undefined;
|
||||||
updateCameraFilter: (cameras: string[] | undefined) => void;
|
updateCameraFilter: (cameras: string[] | undefined) => void;
|
||||||
};
|
};
|
||||||
function CamerasFilterButton({
|
function CamerasFilterButton({
|
||||||
allCameras,
|
allCameras,
|
||||||
|
groups,
|
||||||
selectedCameras,
|
selectedCameras,
|
||||||
updateCameraFilter,
|
updateCameraFilter,
|
||||||
}: CameraFilterButtonProps) {
|
}: CameraFilterButtonProps) {
|
||||||
@ -144,6 +159,24 @@ function CamerasFilterButton({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{groups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{groups.map(([name, conf]) => {
|
||||||
|
return (
|
||||||
|
<FilterCheckBox
|
||||||
|
key={name}
|
||||||
|
label={name}
|
||||||
|
CheckIcon={getIconTypeForGroup(conf.icon)}
|
||||||
|
isChecked
|
||||||
|
onCheckedChange={() => {
|
||||||
|
setCurrentCameras([...conf.cameras]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{allCameras.map((item) => (
|
{allCameras.map((item) => (
|
||||||
<FilterCheckBox
|
<FilterCheckBox
|
||||||
@ -350,12 +383,14 @@ function LabelsFilterButton({
|
|||||||
|
|
||||||
type FilterCheckBoxProps = {
|
type FilterCheckBoxProps = {
|
||||||
label: string;
|
label: string;
|
||||||
|
CheckIcon?: IconType;
|
||||||
isChecked: boolean;
|
isChecked: boolean;
|
||||||
onCheckedChange: (isChecked: boolean) => void;
|
onCheckedChange: (isChecked: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FilterCheckBox({
|
function FilterCheckBox({
|
||||||
label,
|
label,
|
||||||
|
CheckIcon = LuCheck,
|
||||||
isChecked,
|
isChecked,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
}: FilterCheckBoxProps) {
|
}: FilterCheckBoxProps) {
|
||||||
@ -366,7 +401,7 @@ function FilterCheckBox({
|
|||||||
onClick={() => onCheckedChange(!isChecked)}
|
onClick={() => onCheckedChange(!isChecked)}
|
||||||
>
|
>
|
||||||
{isChecked ? (
|
{isChecked ? (
|
||||||
<LuCheck className="w-6 h-6" />
|
<CheckIcon className="w-6 h-6" />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-6 h-6" />
|
<div className="w-6 h-6" />
|
||||||
)}
|
)}
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useState } from "react";
|
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
@ -42,32 +41,36 @@ export default function NavItem({
|
|||||||
}: NavItemProps) {
|
}: NavItemProps) {
|
||||||
const shouldRender = dev ? ENV !== "production" : true;
|
const shouldRender = dev ? ENV !== "production" : true;
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
if (!shouldRender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
shouldRender && (
|
<NavLink
|
||||||
<Tooltip open={isDesktop && showTooltip}>
|
to={url}
|
||||||
<NavLink
|
onClick={onClick}
|
||||||
to={url}
|
className={({ isActive }) =>
|
||||||
onClick={onClick}
|
`${className} flex flex-col justify-center items-center rounded-lg ${
|
||||||
className={({ isActive }) =>
|
variants[variant][isActive ? "active" : "inactive"]
|
||||||
`${className} flex flex-col justify-center items-center rounded-lg ${
|
}`
|
||||||
variants[variant][isActive ? "active" : "inactive"]
|
}
|
||||||
}`
|
>
|
||||||
}
|
<Icon className="size-5 md:m-[6px]" />
|
||||||
onMouseEnter={() => (isDesktop ? setShowTooltip(true) : null)}
|
</NavLink>
|
||||||
onMouseLeave={() => (isDesktop ? setShowTooltip(false) : null)}
|
);
|
||||||
>
|
|
||||||
<TooltipTrigger>
|
if (isDesktop) {
|
||||||
<Icon className="size-5 md:m-[6px]" />
|
return (
|
||||||
</TooltipTrigger>
|
<Tooltip>
|
||||||
</NavLink>
|
<TooltipTrigger>{content}</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>{title}</p>
|
<p>{title}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,30 @@ import Logo from "../Logo";
|
|||||||
import { navbarLinks } from "@/pages/site-navigation";
|
import { navbarLinks } from "@/pages/site-navigation";
|
||||||
import SettingsNavItems from "../settings/SettingsNavItems";
|
import SettingsNavItems from "../settings/SettingsNavItems";
|
||||||
import NavItem from "./NavItem";
|
import NavItem from "./NavItem";
|
||||||
|
import { CameraGroupSelector } from "../filter/CameraGroupSelector";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="absolute w-[52px] z-10 left-o inset-y-0 overflow-y-auto scrollbar-hidden py-4 flex flex-col justify-between bg-primary border-r border-secondary-highlight">
|
<aside className="absolute w-[52px] z-10 left-o inset-y-0 overflow-y-auto scrollbar-hidden py-4 flex flex-col justify-between bg-primary border-r border-secondary-highlight">
|
||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
<div className="w-full flex flex-col gap-0 items-center">
|
<div className="w-full flex flex-col gap-0 items-center">
|
||||||
<Logo className="w-8 h-8 mb-6" />
|
<Logo className="w-8 h-8 mb-6" />
|
||||||
{navbarLinks.map((item) => (
|
{navbarLinks.map((item) => (
|
||||||
<NavItem
|
<div key={item.id}>
|
||||||
className="mx-[10px] mb-6"
|
<NavItem
|
||||||
key={item.id}
|
className={`mx-[10px] ${item.id == 1 ? "mb-2" : "mb-4"}`}
|
||||||
Icon={item.icon}
|
Icon={item.icon}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
url={item.url}
|
url={item.url}
|
||||||
dev={item.dev}
|
dev={item.dev}
|
||||||
/>
|
/>
|
||||||
|
{item.id == 1 && item.url == location.pathname && (
|
||||||
|
<CameraGroupSelector className="mb-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
|
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
|
||||||
|
@ -22,7 +22,7 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-4 py-2",
|
||||||
xs: "h-6 rounded-md",
|
xs: "size-6 rounded-md",
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: "h-9 rounded-md px-3",
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: "h-11 rounded-md px-8",
|
||||||
icon: "h-10 w-10",
|
icon: "h-10 w-10",
|
||||||
|
@ -1,22 +1,28 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export default function useOverlayState(key: string) {
|
export default function useOverlayState(
|
||||||
|
key: string,
|
||||||
|
): [string | undefined, (value: string, replace?: boolean) => void] {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const currentLocationState = location.state;
|
const currentLocationState = location.state;
|
||||||
|
|
||||||
const setOverlayStateValue = useCallback(
|
const setOverlayStateValue = useCallback(
|
||||||
(value: string) => {
|
(value: string, replace: boolean = false) => {
|
||||||
const newLocationState = { ...currentLocationState };
|
const newLocationState = { ...currentLocationState };
|
||||||
newLocationState[key] = value;
|
newLocationState[key] = value;
|
||||||
navigate(location.pathname, { state: newLocationState });
|
navigate(location.pathname, { state: newLocationState, replace });
|
||||||
},
|
},
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[key, navigate],
|
[key, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const overlayStateValue = location.state && location.state[key];
|
const overlayStateValue = useMemo<string | undefined>(
|
||||||
|
() => location.state && location.state[key],
|
||||||
|
[location, key],
|
||||||
|
);
|
||||||
|
|
||||||
return [overlayStateValue, setOverlayStateValue];
|
return [overlayStateValue, setOverlayStateValue];
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,26 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
function Live() {
|
function Live() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
const [selectedCameraName, setSelectedCameraName] = useOverlayState("camera");
|
const [selectedCameraName, setSelectedCameraName] = useOverlayState("camera");
|
||||||
|
const [cameraGroup] = useOverlayState("cameraGroup");
|
||||||
|
|
||||||
const cameras = useMemo(() => {
|
const cameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cameraGroup) {
|
||||||
|
const group = config.camera_groups[cameraGroup];
|
||||||
|
return Object.values(config.cameras)
|
||||||
|
.filter((conf) => conf.enabled && group.cameras.includes(conf.name))
|
||||||
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
|
}
|
||||||
|
|
||||||
return Object.values(config.cameras)
|
return Object.values(config.cameras)
|
||||||
.filter((conf) => conf.ui.dashboard && conf.enabled)
|
.filter((conf) => conf.ui.dashboard && conf.enabled)
|
||||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
}, [config]);
|
}, [config, cameraGroup]);
|
||||||
|
|
||||||
const selectedCamera = useMemo(
|
const selectedCamera = useMemo(
|
||||||
() => cameras.find((cam) => cam.name == selectedCameraName),
|
() => cameras.find((cam) => cam.name == selectedCameraName),
|
||||||
|
@ -204,6 +204,12 @@ export interface CameraConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CameraGroupConfig = {
|
||||||
|
cameras: string[];
|
||||||
|
icon: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface FrigateConfig {
|
export interface FrigateConfig {
|
||||||
audio: {
|
audio: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -276,6 +282,8 @@ export interface FrigateConfig {
|
|||||||
|
|
||||||
go2rtc: Record<string, unknown>;
|
go2rtc: Record<string, unknown>;
|
||||||
|
|
||||||
|
camera_groups: { [groupName: string]: CameraGroupConfig };
|
||||||
|
|
||||||
live: {
|
live: {
|
||||||
height: number;
|
height: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
|
@ -3,14 +3,36 @@ import {
|
|||||||
FaAmazon,
|
FaAmazon,
|
||||||
FaCarSide,
|
FaCarSide,
|
||||||
FaCat,
|
FaCat,
|
||||||
|
FaCircle,
|
||||||
FaDog,
|
FaDog,
|
||||||
FaFedex,
|
FaFedex,
|
||||||
FaFire,
|
FaFire,
|
||||||
|
FaLeaf,
|
||||||
FaUps,
|
FaUps,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { LuBox, LuLassoSelect } from "react-icons/lu";
|
import { LuBox, LuLassoSelect } from "react-icons/lu";
|
||||||
import { MdRecordVoiceOver } from "react-icons/md";
|
import { MdRecordVoiceOver } from "react-icons/md";
|
||||||
|
|
||||||
|
export function getIconTypeForGroup(icon: string) {
|
||||||
|
switch (icon) {
|
||||||
|
case "car":
|
||||||
|
return FaCarSide;
|
||||||
|
case "cat":
|
||||||
|
return FaCat;
|
||||||
|
case "dog":
|
||||||
|
return FaDog;
|
||||||
|
case "leaf":
|
||||||
|
return FaLeaf;
|
||||||
|
default:
|
||||||
|
return FaCircle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIconForGroup(icon: string, className: string = "size-4") {
|
||||||
|
const GroupIcon = getIconTypeForGroup(icon);
|
||||||
|
return <GroupIcon className={className} />;
|
||||||
|
}
|
||||||
|
|
||||||
export function getIconForLabel(label: string, className?: string) {
|
export function getIconForLabel(label: string, className?: string) {
|
||||||
switch (label) {
|
switch (label) {
|
||||||
case "car":
|
case "car":
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useFrigateReviews } from "@/api/ws";
|
import { useFrigateReviews } from "@/api/ws";
|
||||||
import Logo from "@/components/Logo";
|
import Logo from "@/components/Logo";
|
||||||
|
import { CameraGroupSelector } from "@/components/filter/CameraGroupSelector";
|
||||||
import { AnimatedEventThumbnail } from "@/components/image/AnimatedEventThumbnail";
|
import { AnimatedEventThumbnail } from "@/components/image/AnimatedEventThumbnail";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
import LivePlayer from "@/components/player/LivePlayer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -78,7 +79,7 @@ export default function LiveDashboardView({
|
|||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="relative h-9 flex items-center justify-between">
|
<div className="relative h-9 flex items-center justify-between">
|
||||||
<Logo className="absolute inset-y-0 inset-x-1/2 -translate-x-1/2 h-8" />
|
<Logo className="absolute inset-y-0 inset-x-1/2 -translate-x-1/2 h-8" />
|
||||||
<div />
|
<CameraGroupSelector />
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
className={
|
className={
|
||||||
|
Loading…
Reference in New Issue
Block a user