Camera group layout fixes (#11334)

* camera group layout changes and tweaks

* lock aspect ratio

* no compacting

* prevent collisions

* revert

* readd limit aspect
This commit is contained in:
Josh Hawkins 2024-05-10 11:54:37 -05:00 committed by GitHub
parent 386ffbf5a6
commit 82e443a5c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 216 additions and 55 deletions

View File

@ -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"
>
<div className="flex flex-row justify-between items-center mb-3">
<Heading as="h4">Select an icon</Heading>
<span tabIndex={0} className="sr-only" />
<IoClose
size={15}
className="hover:cursor-pointer"
@ -110,24 +111,24 @@ export default function IconPicker({
<Input
type="text"
placeholder="Search for an icon..."
className="mb-3"
className="mb-3 text-md md:text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="flex flex-col flex-1 h-[20dvh]">
<div className="grid grid-cols-6 my-2 gap-2 max-h-[20dvh] overflow-y-auto pr-1">
<div className="flex flex-col h-full overflow-y-auto">
<div className="grid grid-cols-6 gap-2 pr-1">
{icons.map(([name, Icon]) => (
<div
key={name}
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
? "bg-selected text-white"
: "hover:bg-secondary-foreground",
)}
>
<Icon
size={20}
className="size-6"
onClick={() => {
handleIconSelect({ name, Icon });
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

@ -12,7 +12,12 @@ import React, {
useRef,
useState,
} from "react";
import { Layout, Responsive, WidthProvider } from "react-grid-layout";
import {
ItemCallback,
Layout,
Responsive,
WidthProvider,
} from "react-grid-layout";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
import { LivePlayerMode } from "@/types/live";
@ -30,12 +35,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[];
@ -271,22 +278,17 @@ 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;
@ -301,8 +303,27 @@ export default function DraggableGridLayout({
);
}, [containerWidth, marginValue]);
const handleResize: ItemCallback = (
_: Layout[],
oldLayoutItem: Layout,
layoutItem: Layout,
placeholder: Layout,
) => {
const heightDiff = layoutItem.h - oldLayoutItem.h;
const widthDiff = layoutItem.w - oldLayoutItem.w;
const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
if (Math.abs(heightDiff) < Math.abs(widthDiff)) {
layoutItem.h = layoutItem.w / changeCoef;
placeholder.h = layoutItem.w / changeCoef;
} else {
layoutItem.w = layoutItem.h * changeCoef;
placeholder.w = layoutItem.h * changeCoef;
}
};
return (
<>
<Toaster position="top-center" closeButton={true} />
{!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">
{includeBirdseye && birdseyeConfig?.enabled && (
@ -344,6 +365,7 @@ export default function DraggableGridLayout({
containerPadding={[0, isEditMode ? 6 : 3]}
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
onDragStop={handleLayoutChange}
onResize={handleResize}
onResizeStop={handleLayoutChange}
>
{includeBirdseye && birdseyeConfig?.enabled && (
@ -394,7 +416,7 @@ export default function DraggableGridLayout({
);
})}
</ResponsiveGridLayout>
{isDesktop && !fullscreen && (
{isDesktop && (
<div
className={cn(
"fixed",
@ -406,22 +428,18 @@ export default function DraggableGridLayout({
>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="px-2 py-1 bg-secondary-foreground rounded-lg opacity-30 hover:opacity-100 transition-all duration-300"
<div
className="rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer opacity-60 hover:opacity-100 transition-all duration-300"
onClick={() =>
setIsEditMode((prevIsEditMode) => !prevIsEditMode)
}
>
{isEditMode ? (
<>
<IoClose className="size-5" />
</>
<IoClose className="size-5 md:m-[6px]" />
) : (
<>
<LuLayoutDashboard className="size-5" />
</>
<LuLayoutDashboard className="size-5 md:m-[6px]" />
)}
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
{isEditMode ? "Exit Editing" : "Edit Layout"}
@ -431,14 +449,14 @@ export default function DraggableGridLayout({
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="px-2 py-1 bg-secondary-foreground rounded-lg opacity-30 hover:opacity-100 transition-all duration-300"
<div
className="rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer opacity-60 hover:opacity-100 transition-all duration-300"
onClick={() =>
setEditGroup((prevEditGroup) => !prevEditGroup)
}
>
<LuPencil className="size-5" />
</Button>
<LuPencil className="size-5 md:m-[6px]" />
</div>
</TooltipTrigger>
<TooltipContent>
{isEditMode ? "Exit Editing" : "Edit Camera Group"}
@ -446,26 +464,16 @@ export default function DraggableGridLayout({
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="px-2 py-1 bg-secondary-foreground rounded-lg opacity-30 hover:opacity-100 transition-all duration-300"
onClick={() => {
if (fullscreen) {
document.exitFullscreen();
} else {
gridContainerRef.current?.requestFullscreen();
}
}}
<div
className="rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer opacity-60 hover:opacity-100 transition-all duration-300"
onClick={toggleFullscreen}
>
{fullscreen ? (
<>
<FaCompress className="size-5" />
</>
<FaCompress className="size-5 md:m-[6px]" />
) : (
<>
<FaExpand className="size-5" />
</>
<FaExpand className="size-5 md:m-[6px]" />
)}
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
{fullscreen ? "Exit Fullscreen" : "Fullscreen"}

View File

@ -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 && (
<div className="flex items-center gap-1">
<Button
className="p-1"
className={cn(
"p-1",
isEditMode
? "text-primary bg-selected"
: "text-secondary-foreground bg-secondary",
)}
size="xs"
onClick={() =>
setIsEditMode((prevIsEditMode) => !prevIsEditMode)
}
>
{isEditMode ? <IoClose /> : <LuMove />}
{isEditMode ? <IoClose /> : <LuLayoutDashboard />}
</Button>
</div>
)}