camera group layout changes and tweaks

This commit is contained in:
Josh Hawkins 2024-05-10 08:21:22 -05:00
parent 386ffbf5a6
commit a5650ac8b3
4 changed files with 205 additions and 59 deletions

View File

@ -95,10 +95,11 @@ export default function IconPicker({
align="start" align="start"
side="top" side="top"
container={containerRef.current} container={containerRef.current}
className="max-h-[50dvh]" className="flex flex-col max-h-[50dvh] md:max-h-[30dvh] overflow-y-hidden"
> >
<div className="flex flex-row justify-between items-center mb-3"> <div className="flex flex-row justify-between items-center mb-3">
<Heading as="h4">Select an icon</Heading> <Heading as="h4">Select an icon</Heading>
<span tabIndex={0} className="sr-only" />
<IoClose <IoClose
size={15} size={15}
className="hover:cursor-pointer" className="hover:cursor-pointer"
@ -110,24 +111,24 @@ export default function IconPicker({
<Input <Input
type="text" type="text"
placeholder="Search for an icon..." placeholder="Search for an icon..."
className="mb-3" className="mb-3 text-md md:text-sm"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
<div className="flex flex-col flex-1 h-[20dvh]"> <div className="flex flex-col h-full overflow-y-auto">
<div className="grid grid-cols-6 my-2 gap-2 max-h-[20dvh] overflow-y-auto pr-1"> <div className="grid grid-cols-6 gap-2 pr-1">
{icons.map(([name, Icon]) => ( {icons.map(([name, Icon]) => (
<div <div
key={name} key={name}
className={cn( className={cn(
"flex flex-row justify-center items-start hover:cursor-pointer p-1 rounded-lg", "flex flex-row justify-center items-center hover:cursor-pointer p-1 rounded-lg",
selectedIcon?.name === name selectedIcon?.name === name
? "bg-selected text-white" ? "bg-selected text-white"
: "hover:bg-secondary-foreground", : "hover:bg-secondary-foreground",
)} )}
> >
<Icon <Icon
size={20} className="size-6"
onClick={() => { onClick={() => {
handleIconSelect({ name, Icon }); handleIconSelect({ name, Icon });
setOpen(false); setOpen(false);

View File

@ -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<void> | 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<void> | 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<T extends HTMLElement = HTMLElement>(
elementRef: RefObject<T>,
) {
const [fullscreen, setFullscreen] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(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 };
}

View File

@ -30,12 +30,14 @@ import { cn } from "@/lib/utils";
import { EditGroupDialog } from "@/components/filter/CameraGroupSelector"; import { EditGroupDialog } from "@/components/filter/CameraGroupSelector";
import { usePersistedOverlayState } from "@/hooks/use-overlay-state"; import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
import { FaCompress, FaExpand } from "react-icons/fa"; import { FaCompress, FaExpand } from "react-icons/fa";
import { Button } from "@/components/ui/button";
import { import {
Tooltip, Tooltip,
TooltipTrigger, TooltipTrigger,
TooltipContent, TooltipContent,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useFullscreen } from "@/hooks/use-fullscreen";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
type DraggableGridLayoutProps = { type DraggableGridLayoutProps = {
cameras: CameraConfig[]; cameras: CameraConfig[];
@ -148,15 +150,15 @@ export default function DraggableGridLayout({
if (aspectRatio < 1) { if (aspectRatio < 1) {
// Portrait // Portrait
height = 2 * columnsPerPlayer; height = 4 * columnsPerPlayer;
width = columnsPerPlayer; width = columnsPerPlayer;
} else if (aspectRatio > 2) { } else if (aspectRatio > 2) {
// Wide // Wide
height = 1 * columnsPerPlayer; height = 2 * columnsPerPlayer;
width = 2 * columnsPerPlayer; width = 2 * columnsPerPlayer;
} else { } else {
// Landscape // Landscape
height = 1 * columnsPerPlayer; height = 2 * columnsPerPlayer;
width = columnsPerPlayer; width = columnsPerPlayer;
} }
@ -271,25 +273,20 @@ export default function DraggableGridLayout({
// fullscreen state // fullscreen state
const { fullscreen, toggleFullscreen, error, clearError } =
useFullscreen(gridContainerRef);
useEffect(() => { useEffect(() => {
if (gridContainerRef.current == null) { if (error !== null) {
return; toast.error(`Error attempting fullscreen mode: ${error}`, {
position: "top-center",
});
clearError();
} }
}, [error, clearError]);
const listener = () => {
setFullscreen(document.fullscreenElement != null);
};
document.addEventListener("fullscreenchange", listener);
return () => {
document.removeEventListener("fullscreenchange", listener);
};
}, [gridContainerRef]);
const [fullscreen, setFullscreen] = useState(false);
const cellHeight = useMemo(() => { const cellHeight = useMemo(() => {
const aspectRatio = 16 / 9; const aspectRatio = 32 / 9;
// subtract container margin, 1 camera takes up at least 4 rows // subtract container margin, 1 camera takes up at least 4 rows
// account for additional margin on bottom of each row // account for additional margin on bottom of each row
return ( return (
@ -297,12 +294,13 @@ export default function DraggableGridLayout({
12 / 12 /
aspectRatio - aspectRatio -
marginValue + marginValue +
marginValue / 4 marginValue / 8
); );
}, [containerWidth, marginValue]); }, [containerWidth, marginValue]);
return ( return (
<> <>
<Toaster position="top-center" closeButton={true} />
{!isGridLayoutLoaded || !currentGridLayout ? ( {!isGridLayoutLoaded || !currentGridLayout ? (
<div className="mt-2 px-2 grid grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"> <div className="mt-2 px-2 grid grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4">
{includeBirdseye && birdseyeConfig?.enabled && ( {includeBirdseye && birdseyeConfig?.enabled && (
@ -393,8 +391,17 @@ export default function DraggableGridLayout({
</LivePlayerGridItem> </LivePlayerGridItem>
); );
})} })}
{/* <div
key="test"
data-grid={{ x: 4, y: 0, w: 8, h: 8 }}
className=" bg-green-500"
>
<div className="size-full bg-green-500">
<img src="https://placehold.co/2560x720" />
</div>
</div> */}
</ResponsiveGridLayout> </ResponsiveGridLayout>
{isDesktop && !fullscreen && ( {isDesktop && (
<div <div
className={cn( className={cn(
"fixed", "fixed",
@ -406,22 +413,18 @@ export default function DraggableGridLayout({
> >
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <div
className="px-2 py-1 bg-secondary-foreground rounded-lg opacity-30 hover:opacity-100 transition-all duration-300" className="rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer opacity-60 hover:opacity-100 transition-all duration-300"
onClick={() => onClick={() =>
setIsEditMode((prevIsEditMode) => !prevIsEditMode) setIsEditMode((prevIsEditMode) => !prevIsEditMode)
} }
> >
{isEditMode ? ( {isEditMode ? (
<> <IoClose className="size-5 md:m-[6px]" />
<IoClose className="size-5" />
</>
) : ( ) : (
<> <LuLayoutDashboard className="size-5 md:m-[6px]" />
<LuLayoutDashboard className="size-5" />
</>
)} )}
</Button> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{isEditMode ? "Exit Editing" : "Edit Layout"} {isEditMode ? "Exit Editing" : "Edit Layout"}
@ -431,14 +434,14 @@ export default function DraggableGridLayout({
<> <>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <div
className="px-2 py-1 bg-secondary-foreground rounded-lg opacity-30 hover:opacity-100 transition-all duration-300" className="rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer opacity-60 hover:opacity-100 transition-all duration-300"
onClick={() => onClick={() =>
setEditGroup((prevEditGroup) => !prevEditGroup) setEditGroup((prevEditGroup) => !prevEditGroup)
} }
> >
<LuPencil className="size-5" /> <LuPencil className="size-5 md:m-[6px]" />
</Button> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{isEditMode ? "Exit Editing" : "Edit Camera Group"} {isEditMode ? "Exit Editing" : "Edit Camera Group"}
@ -446,26 +449,16 @@ export default function DraggableGridLayout({
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <div
className="px-2 py-1 bg-secondary-foreground rounded-lg opacity-30 hover:opacity-100 transition-all duration-300" className="rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer opacity-60 hover:opacity-100 transition-all duration-300"
onClick={() => { onClick={toggleFullscreen}
if (fullscreen) {
document.exitFullscreen();
} else {
gridContainerRef.current?.requestFullscreen();
}
}}
> >
{fullscreen ? ( {fullscreen ? (
<> <FaCompress className="size-5 md:m-[6px]" />
<FaCompress className="size-5" />
</>
) : ( ) : (
<> <FaExpand className="size-5 md:m-[6px]" />
<FaExpand className="size-5" />
</>
)} )}
</Button> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{fullscreen ? "Exit Fullscreen" : "Fullscreen"} {fullscreen ? "Exit Fullscreen" : "Fullscreen"}

View File

@ -22,7 +22,8 @@ import {
import useSWR from "swr"; import useSWR from "swr";
import DraggableGridLayout from "./DraggableGridLayout"; import DraggableGridLayout from "./DraggableGridLayout";
import { IoClose } from "react-icons/io5"; 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 = { type LiveDashboardViewProps = {
cameras: CameraConfig[]; cameras: CameraConfig[];
@ -190,13 +191,18 @@ export default function LiveDashboardView({
{cameraGroup && cameraGroup !== "default" && isTablet && ( {cameraGroup && cameraGroup !== "default" && isTablet && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
className="p-1" className={cn(
"p-1",
isEditMode
? "text-primary bg-selected"
: "text-secondary-foreground bg-secondary",
)}
size="xs" size="xs"
onClick={() => onClick={() =>
setIsEditMode((prevIsEditMode) => !prevIsEditMode) setIsEditMode((prevIsEditMode) => !prevIsEditMode)
} }
> >
{isEditMode ? <IoClose /> : <LuMove />} {isEditMode ? <IoClose /> : <LuLayoutDashboard />}
</Button> </Button>
</div> </div>
)} )}