diff --git a/frigate/config.py b/frigate/config.py index 8d3ca6827..3ab7ea956 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -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: diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 362c58c00..0dfc763c0 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -1,58 +1,71 @@ 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("config"); const navigate = useNavigate(); const [group, setGroup] = useOverlayState("cameraGroup"); - if (isMobile) { - return ( -
- - {Object.entries(config?.camera_groups ?? {}).map(([name, config]) => { - return ( - - ); - })} -
- ); - } + const groups = useMemo(() => { + if (!config) { + return []; + } - return
; + return Object.entries(config.camera_groups).sort( + (a, b) => a[1].order - b[1].order, + ); + }, [config]); + + return ( +
+ + {groups.map(([name, config]) => { + return ( + + ); + })} +
+ ); } function getGroupIcon(icon: string) { switch (icon) { case "car": return ; + case "cat": + return ; + case "dog": + return ; case "leaf": return ; default: diff --git a/web/src/components/navigation/Sidebar.tsx b/web/src/components/navigation/Sidebar.tsx index 1fdea0c41..2e911b825 100644 --- a/web/src/components/navigation/Sidebar.tsx +++ b/web/src/components/navigation/Sidebar.tsx @@ -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() {
{navbarLinks.map((item) => ( - + <> + + {item.id == 1 && } + ))}
diff --git a/web/src/hooks/use-overlay-state.tsx b/web/src/hooks/use-overlay-state.tsx index 3b7b45982..bcdc2144d 100644 --- a/web/src/hooks/use-overlay-state.tsx +++ b/web/src/hooks/use-overlay-state.tsx @@ -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 diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 25c6fdb1b..848283eb0 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -7,17 +7,26 @@ import useSWR from "swr"; function Live() { const { data: config } = useSWR("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), diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index a72e7f240..7825e7f8f 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -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; + camera_groups: { [groupName: string]: CameraGroupConfig }; + live: { height: number; quality: number; diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index b31d79100..908ce33ff 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -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 && (
-
+