Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Camera group layout fixes #11334

Merged
merged 6 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions web/src/components/icons/IconPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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);
Expand Down
146 changes: 146 additions & 0 deletions web/src/hooks/use-fullscreen.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
100 changes: 54 additions & 46 deletions web/src/views/live/DraggableGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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[];
Expand Down Expand Up @@ -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;
Expand All @@ -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 && (
Expand Down Expand Up @@ -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 && (
Expand Down Expand Up @@ -394,7 +416,7 @@ export default function DraggableGridLayout({
);
})}
</ResponsiveGridLayout>
{isDesktop && !fullscreen && (
{isDesktop && (
<div
className={cn(
"fixed",
Expand All @@ -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"}
Expand All @@ -431,41 +449,31 @@ 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"}
</TooltipContent>
</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"}
Expand Down
Loading
Loading