mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 20:25:26 +03:00
Implement camera groups in config and live view
This commit is contained in:
parent
0d6ebcdba8
commit
6188a622d3
@ -1010,6 +1010,7 @@ class CameraGroupConfig(FrigateBaseModel):
|
||||
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:
|
||||
|
||||
@ -1,42 +1,54 @@
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import useSWR from "swr";
|
||||
import { MdHome } from "react-icons/md";
|
||||
import { FaCar, FaCircle, FaLeaf } from "react-icons/fa";
|
||||
import { FaCar, FaCat, FaCircle, FaDog, FaLeaf } from "react-icons/fa";
|
||||
import useOverlayState from "@/hooks/use-overlay-state";
|
||||
import { Button } from "../ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function CameraGroupSelector() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const navigate = useNavigate();
|
||||
const [group, setGroup] = useOverlayState("cameraGroup");
|
||||
|
||||
if (isMobile) {
|
||||
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">
|
||||
<div
|
||||
className={`flex items-center justify-start gap-2 ${isDesktop ? "flex-col mb-4" : ""}`}
|
||||
>
|
||||
<Button
|
||||
className={
|
||||
group == undefined
|
||||
? "text-selected bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
||||
: "text-muted-foreground bg-muted"
|
||||
: "text-muted-foreground bg-secondary"
|
||||
}
|
||||
size="xs"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<MdHome className="size-4" />
|
||||
</Button>
|
||||
{Object.entries(config?.camera_groups ?? {}).map(([name, config]) => {
|
||||
{groups.map(([name, config]) => {
|
||||
return (
|
||||
<Button
|
||||
key={name}
|
||||
className={
|
||||
group == name
|
||||
? "text-selected bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
||||
: "text-muted-foreground bg-muted"
|
||||
: "text-muted-foreground bg-secondary"
|
||||
}
|
||||
size="xs"
|
||||
onClick={() => setGroup(name)}
|
||||
onClick={() => setGroup(name, group != undefined)}
|
||||
>
|
||||
{getGroupIcon(config.icon)}
|
||||
</Button>
|
||||
@ -44,15 +56,16 @@ export function CameraGroupSelector() {
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
function getGroupIcon(icon: string) {
|
||||
switch (icon) {
|
||||
case "car":
|
||||
return <FaCar className="size-4" />;
|
||||
case "cat":
|
||||
return <FaCat className="size-4" />;
|
||||
case "dog":
|
||||
return <FaDog className="size-4" />;
|
||||
case "leaf":
|
||||
return <FaLeaf className="size-4" />;
|
||||
default:
|
||||
|
||||
@ -2,6 +2,7 @@ import Logo from "../Logo";
|
||||
import { navbarLinks } from "@/pages/site-navigation";
|
||||
import SettingsNavItems from "../settings/SettingsNavItems";
|
||||
import NavItem from "./NavItem";
|
||||
import { CameraGroupSelector } from "../filter/CameraGroupSelector";
|
||||
|
||||
function Sidebar() {
|
||||
return (
|
||||
@ -10,14 +11,17 @@ function Sidebar() {
|
||||
<div className="w-full flex flex-col gap-0 items-center">
|
||||
<Logo className="w-8 h-8 mb-6" />
|
||||
{navbarLinks.map((item) => (
|
||||
<>
|
||||
<NavItem
|
||||
className="mx-[10px] mb-6"
|
||||
className={`mx-[10px] ${item.id == 1 ? "mb-2" : "mb-6"}`}
|
||||
key={item.id}
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
url={item.url}
|
||||
dev={item.dev}
|
||||
/>
|
||||
{item.id == 1 && <CameraGroupSelector />}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
|
||||
|
||||
@ -3,16 +3,16 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
export default function useOverlayState(
|
||||
key: string,
|
||||
): [string | undefined, (value: string) => void] {
|
||||
): [string | undefined, (value: string, replace?: boolean) => void] {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentLocationState = location.state;
|
||||
|
||||
const setOverlayStateValue = useCallback(
|
||||
(value: string) => {
|
||||
(value: string, replace: boolean = false) => {
|
||||
const newLocationState = { ...currentLocationState };
|
||||
newLocationState[key] = value;
|
||||
navigate(location.pathname, { state: newLocationState });
|
||||
navigate(location.pathname, { state: newLocationState, replace });
|
||||
},
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@ -7,17 +7,26 @@ import useSWR from "swr";
|
||||
|
||||
function Live() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const [selectedCameraName, setSelectedCameraName] = useOverlayState("camera");
|
||||
const [cameraGroup] = useOverlayState("cameraGroup");
|
||||
|
||||
const cameras = useMemo(() => {
|
||||
if (!config) {
|
||||
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)
|
||||
.filter((conf) => conf.ui.dashboard && conf.enabled)
|
||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||
}, [config]);
|
||||
}, [config, cameraGroup]);
|
||||
|
||||
const selectedCamera = useMemo(
|
||||
() => 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 {
|
||||
audio: {
|
||||
enabled: boolean;
|
||||
@ -276,6 +282,8 @@ export interface FrigateConfig {
|
||||
|
||||
go2rtc: Record<string, unknown>;
|
||||
|
||||
camera_groups: { [groupName: string]: CameraGroupConfig };
|
||||
|
||||
live: {
|
||||
height: number;
|
||||
quality: number;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useFrigateReviews } from "@/api/ws";
|
||||
import Logo from "@/components/Logo";
|
||||
import { CameraGroupSelector } from "@/components/filter/CameraGroupSelector";
|
||||
import { AnimatedEventThumbnail } from "@/components/image/AnimatedEventThumbnail";
|
||||
import LivePlayer from "@/components/player/LivePlayer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -78,7 +79,7 @@ export default function LiveDashboardView({
|
||||
{isMobile && (
|
||||
<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" />
|
||||
<div />
|
||||
<CameraGroupSelector />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
className={
|
||||
|
||||
Loading…
Reference in New Issue
Block a user