mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
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:
parent
386ffbf5a6
commit
82e443a5c3
@ -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);
|
||||
|
146
web/src/hooks/use-fullscreen.ts
Normal file
146
web/src/hooks/use-fullscreen.ts
Normal 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 };
|
||||
}
|
@ -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"}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user