From a5650ac8b33e7322653a20b3eb96d87e9b7e781d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 10 May 2024 08:21:22 -0500 Subject: [PATCH] camera group layout changes and tweaks --- web/src/components/icons/IconPicker.tsx | 13 +- web/src/hooks/use-fullscreen.ts | 146 +++++++++++++++++++++ web/src/views/live/DraggableGridLayout.tsx | 93 ++++++------- web/src/views/live/LiveDashboardView.tsx | 12 +- 4 files changed, 205 insertions(+), 59 deletions(-) create mode 100644 web/src/hooks/use-fullscreen.ts diff --git a/web/src/components/icons/IconPicker.tsx b/web/src/components/icons/IconPicker.tsx index 21883ccdc..6450626d4 100644 --- a/web/src/components/icons/IconPicker.tsx +++ b/web/src/components/icons/IconPicker.tsx @@ -95,10 +95,11 @@ export default function IconPicker({ align="start" side="top" container={containerRef.current} - className="max-h-[50dvh]" + className="flex flex-col max-h-[50dvh] md:max-h-[30dvh] overflow-y-hidden" >
Select an icon + setSearchTerm(e.target.value)} /> -
-
+
+
{icons.map(([name, Icon]) => (
{ handleIconSelect({ name, Icon }); setOpen(false); diff --git a/web/src/hooks/use-fullscreen.ts b/web/src/hooks/use-fullscreen.ts new file mode 100644 index 000000000..0c5d28fbf --- /dev/null +++ b/web/src/hooks/use-fullscreen.ts @@ -0,0 +1,146 @@ +import { RefObject, useCallback, useEffect, useState } from "react"; + +function getFullscreenElement(): HTMLElement | null { + return ( + document.fullscreenElement || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (document as any).webkitFullscreenElement || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (document as any).mozFullScreenElement || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (document as any).msFullscreenElement + ); +} + +function exitFullscreen(): Promise | null { + if (document.exitFullscreen) return document.exitFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((document as any).msExitFullscreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (document as any).msExitFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((document as any).webkitExitFullscreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (document as any).webkitExitFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((document as any).mozCancelFullScreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (document as any).mozCancelFullScreen(); + return null; +} + +function enterFullScreen(element: HTMLElement): Promise | null { + if (element.requestFullscreen) return element.requestFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((element as any).msRequestFullscreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (element as any).msRequestFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((element as any).webkitEnterFullscreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (element as any).webkitEnterFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((element as any).webkitRequestFullscreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (element as any).webkitRequestFullscreen(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((element as any).mozRequestFullscreen) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (element as any).mozRequestFullscreen(); + return null; +} + +const prefixes = ["", "webkit", "moz", "ms"]; + +function addEventListeners( + element: HTMLElement, + onFullScreen: (event: Event) => void, + onError: (event: Event) => void, +) { + prefixes.forEach((prefix) => { + element.addEventListener(`${prefix}fullscreenchange`, onFullScreen); + element.addEventListener(`${prefix}fullscreenerror`, onError); + }); +} + +function removeEventListeners( + element: HTMLElement, + onFullScreen: (event: Event) => void, + onError: (event: Event) => void, +) { + prefixes.forEach((prefix) => { + element.removeEventListener(`${prefix}fullscreenchange`, onFullScreen); + element.removeEventListener(`${prefix}fullscreenerror`, onError); + }); +} + +export function useFullscreen( + elementRef: RefObject, +) { + const [fullscreen, setFullscreen] = useState(false); + const [error, setError] = useState(null); + + const handleFullscreenChange = useCallback((event: Event) => { + setFullscreen(event.target === getFullscreenElement()); + }, []); + + const handleFullscreenError = useCallback((event: Event) => { + setFullscreen(false); + setError( + new Error( + `Error attempting full-screen mode: ${event} (${event.target})`, + ), + ); + }, []); + + const toggleFullscreen = useCallback(async () => { + try { + if (!getFullscreenElement()) { + await enterFullScreen(elementRef.current!); + } else { + await exitFullscreen(); + } + setError(null); + } catch (err) { + setError(err as Error); + } + }, [elementRef]); + + const clearError = useCallback(() => { + setError(null); + }, []); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.code === "F11") { + toggleFullscreen(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [toggleFullscreen]); + + useEffect(() => { + const currentElement = elementRef.current; + if (currentElement) { + addEventListeners( + currentElement, + handleFullscreenChange, + handleFullscreenError, + ); + return () => { + removeEventListeners( + currentElement, + handleFullscreenChange, + handleFullscreenError, + ); + }; + } + }, [elementRef, handleFullscreenChange, handleFullscreenError]); + + return { fullscreen, toggleFullscreen, error, clearError }; +} diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 891c41bd0..fd1b0907b 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -30,12 +30,14 @@ import { cn } from "@/lib/utils"; import { EditGroupDialog } from "@/components/filter/CameraGroupSelector"; import { usePersistedOverlayState } from "@/hooks/use-overlay-state"; import { FaCompress, FaExpand } from "react-icons/fa"; -import { Button } from "@/components/ui/button"; import { Tooltip, TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; +import { useFullscreen } from "@/hooks/use-fullscreen"; +import { toast } from "sonner"; +import { Toaster } from "@/components/ui/sonner"; type DraggableGridLayoutProps = { cameras: CameraConfig[]; @@ -148,15 +150,15 @@ export default function DraggableGridLayout({ if (aspectRatio < 1) { // Portrait - height = 2 * columnsPerPlayer; + height = 4 * columnsPerPlayer; width = columnsPerPlayer; } else if (aspectRatio > 2) { // Wide - height = 1 * columnsPerPlayer; + height = 2 * columnsPerPlayer; width = 2 * columnsPerPlayer; } else { // Landscape - height = 1 * columnsPerPlayer; + height = 2 * columnsPerPlayer; width = columnsPerPlayer; } @@ -271,25 +273,20 @@ export default function DraggableGridLayout({ // fullscreen state + const { fullscreen, toggleFullscreen, error, clearError } = + useFullscreen(gridContainerRef); + useEffect(() => { - if (gridContainerRef.current == null) { - return; + if (error !== null) { + toast.error(`Error attempting fullscreen mode: ${error}`, { + position: "top-center", + }); + clearError(); } - - const listener = () => { - setFullscreen(document.fullscreenElement != null); - }; - document.addEventListener("fullscreenchange", listener); - - return () => { - document.removeEventListener("fullscreenchange", listener); - }; - }, [gridContainerRef]); - - const [fullscreen, setFullscreen] = useState(false); + }, [error, clearError]); const cellHeight = useMemo(() => { - const aspectRatio = 16 / 9; + const aspectRatio = 32 / 9; // subtract container margin, 1 camera takes up at least 4 rows // account for additional margin on bottom of each row return ( @@ -297,12 +294,13 @@ export default function DraggableGridLayout({ 12 / aspectRatio - marginValue + - marginValue / 4 + marginValue / 8 ); }, [containerWidth, marginValue]); return ( <> + {!isGridLayoutLoaded || !currentGridLayout ? (
{includeBirdseye && birdseyeConfig?.enabled && ( @@ -393,8 +391,17 @@ export default function DraggableGridLayout({ ); })} + {/*
+
+ +
+
*/} - {isDesktop && !fullscreen && ( + {isDesktop && (
- +
{isEditMode ? "Exit Editing" : "Edit Layout"} @@ -431,14 +434,14 @@ export default function DraggableGridLayout({ <> - + +
{isEditMode ? "Exit Editing" : "Edit Camera Group"} @@ -446,26 +449,16 @@ export default function DraggableGridLayout({ - +
{fullscreen ? "Exit Fullscreen" : "Fullscreen"} diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 3a9616195..c109bb977 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -22,7 +22,8 @@ import { import useSWR from "swr"; import DraggableGridLayout from "./DraggableGridLayout"; import { IoClose } from "react-icons/io5"; -import { LuMove } from "react-icons/lu"; +import { LuLayoutDashboard } from "react-icons/lu"; +import { cn } from "@/lib/utils"; type LiveDashboardViewProps = { cameras: CameraConfig[]; @@ -190,13 +191,18 @@ export default function LiveDashboardView({ {cameraGroup && cameraGroup !== "default" && isTablet && (
)}