diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index f0a64b92a..44f7e0b53 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -7,6 +7,7 @@ import { import React, { useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -20,7 +21,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { useResizeObserver } from "@/hooks/resize-observer"; import { isEqual } from "lodash"; import useSWR from "swr"; -import { isSafari } from "react-device-detect"; +import { isDesktop, isMobile, isSafari } from "react-device-detect"; import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer"; import LivePlayer from "@/components/player/LivePlayer"; import { Button } from "@/components/ui/button"; @@ -30,25 +31,32 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { IoClose } from "react-icons/io5"; -import { LuMoveDiagonal2 } from "react-icons/lu"; +import { LuMove } from "react-icons/lu"; +import { cn } from "@/lib/utils"; type DraggableGridLayoutProps = { cameras: CameraConfig[]; cameraGroup: string; cameraRef: (node: HTMLElement | null) => void; + containerRef: React.RefObject; includeBirdseye: boolean; onSelectCamera: (camera: string) => void; windowVisible: boolean; visibleCameras: string[]; + isEditMode: boolean; + setIsEditMode: React.Dispatch>; }; export default function DraggableGridLayout({ cameras, cameraGroup, + containerRef, cameraRef, includeBirdseye, onSelectCamera, windowVisible, visibleCameras, + isEditMode, + setIsEditMode, }: DraggableGridLayoutProps) { const { data: config } = useSWR("config"); const birdseyeConfig = useMemo(() => config?.birdseye, [config]); @@ -66,8 +74,6 @@ export default function DraggableGridLayout({ Layout[] | undefined >(); - const [isEditMode, setIsEditMode] = useState(false); - const handleLayoutChange = useCallback( (currentLayout: Layout[]) => { if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) { @@ -160,12 +166,12 @@ export default function DraggableGridLayout({ birdseyeConfig, ]); - const toggleEditMode = useCallback(() => { + useEffect(() => { if (currentGridLayout) { const updatedGridLayout = currentGridLayout.map((layout) => ({ ...layout, - isDraggable: !isEditMode, - isResizable: !isEditMode, + isDraggable: isEditMode, + isResizable: isEditMode, })); if (isEditMode) { setGridLayout(updatedGridLayout); @@ -173,9 +179,10 @@ export default function DraggableGridLayout({ } else { setGridLayout(updatedGridLayout); } - setIsEditMode((prevIsEditMode) => !prevIsEditMode); } - }, [currentGridLayout, isEditMode, setGridLayout]); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEditMode, setGridLayout]); useEffect(() => { if (isGridLayoutLoaded) { @@ -218,31 +225,58 @@ export default function DraggableGridLayout({ isGridLayoutLoaded, ]); + const [marginValue, setMarginValue] = useState(16); + + // calculate margin value for browsers that don't have default font size of 16px + useLayoutEffect(() => { + const calculateRemValue = () => { + const htmlElement = document.documentElement; + const fontSize = window.getComputedStyle(htmlElement).fontSize; + setMarginValue(parseFloat(fontSize)); + }; + + calculateRemValue(); + }, []); + const gridContainerRef = useRef(null); - const [{ width: containerWidth }] = useResizeObserver(gridContainerRef); + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(gridContainerRef); + + const hasScrollbar = useMemo(() => { + return ( + containerHeight && + containerRef.current && + containerRef.current.offsetHeight < + (gridContainerRef.current?.scrollHeight ?? 0) + ); + }, [containerRef, gridContainerRef, containerHeight]); const cellHeight = useMemo(() => { const aspectRatio = 16 / 9; - const totalMarginWidth = 11 * 13; // 11 margins with 13px each - const rowHeight = - ((containerWidth ?? window.innerWidth) - totalMarginWidth) / - (13 * aspectRatio); - return rowHeight; - }, [containerWidth]); + // subtract container margin, 1 camera takes up at least 4 rows + // account for additional margin on bottom of each row + return ( + ((containerWidth ?? window.innerWidth) - 2 * marginValue) / + 12 / + aspectRatio - + marginValue + + marginValue / 4 + ); + }, [containerWidth, marginValue]); return ( <> {!isGridLayoutLoaded || !currentGridLayout ? (
{includeBirdseye && birdseyeConfig?.enabled && ( - + )} {cameras.map((camera) => { return ( ); })} @@ -264,37 +298,33 @@ export default function DraggableGridLayout({ rowHeight={cellHeight} breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }} - margin={[16, 16]} - containerPadding={[8, 8]} - resizeHandles={["sw", "nw", "se", "ne"]} + margin={[marginValue, marginValue]} + containerPadding={[0, isEditMode ? 6 : 3]} + resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []} onDragStop={handleLayoutChange} onResizeStop={handleLayoutChange} > {includeBirdseye && birdseyeConfig?.enabled && ( onSelectCamera("birdseye")} > - {isEditMode && ( - <> -
-
-
-
- - )} + {isEditMode && } )} {cameras.map((camera) => { let grow; const aspectRatio = camera.detect.width / camera.detect.height; if (aspectRatio > ASPECT_WIDE_LAYOUT) { - grow = `aspect-wide`; + grow = `aspect-wide w-full`; } else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) { - grow = `aspect-tall`; + grow = `aspect-tall h-full`; } else { grow = "aspect-video"; } @@ -302,7 +332,12 @@ export default function DraggableGridLayout({ - {isEditMode && ( - <> -
-
-
-
- - )} + {isEditMode && } ); })} -
- - - - - - {isEditMode ? "Exit Editing" : "Edit Layout"} - - -
+ {isDesktop && ( + + )}
)} ); } +type DesktopEditLayoutButtonProps = { + isEditMode?: boolean; + setIsEditMode: React.Dispatch>; + hasScrollbar?: boolean | 0 | null; +}; + +function DesktopEditLayoutButton({ + isEditMode, + setIsEditMode, + hasScrollbar, +}: DesktopEditLayoutButtonProps) { + return ( +
+ + + + + + {isEditMode ? "Exit Editing" : "Edit Layout"} + + +
+ ); +} + +function CornerCircles() { + return ( + <> +
+
+
+
+ + ); +} + type BirdseyeLivePlayerGridItemProps = { style?: React.CSSProperties; className?: string; diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 1c7d2d3e0..3a9616195 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -17,9 +17,12 @@ import { isMobile, isMobileOnly, isSafari, + isTablet, } from "react-device-detect"; import useSWR from "swr"; import DraggableGridLayout from "./DraggableGridLayout"; +import { IoClose } from "react-icons/io5"; +import { LuMove } from "react-icons/lu"; type LiveDashboardViewProps = { cameras: CameraConfig[]; @@ -42,6 +45,9 @@ export default function LiveDashboardView({ isDesktop ? "grid" : "list", ); + const [isEditMode, setIsEditMode] = useState(false); + const containerRef = useRef(null); + // recent events const { payload: eventUpdate } = useFrigateReviews(); const { data: allEvents, mutate: updateEvents } = useSWR([ @@ -148,37 +154,52 @@ export default function LiveDashboardView({ const birdseyeConfig = useMemo(() => config?.birdseye, [config]); return ( -
+
{isMobile && (
-
- - -
+ {(!cameraGroup || cameraGroup == "default" || isMobileOnly) && ( +
+ + +
+ )} + {cameraGroup && cameraGroup !== "default" && isTablet && ( +
+ +
+ )}
)} @@ -235,11 +256,14 @@ export default function LiveDashboardView({ )}