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"
|
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);
|
||||||
|
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,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} 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-grid-layout/css/styles.css";
|
||||||
import "react-resizable/css/styles.css";
|
import "react-resizable/css/styles.css";
|
||||||
import { LivePlayerMode } from "@/types/live";
|
import { LivePlayerMode } from "@/types/live";
|
||||||
@ -30,12 +35,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[];
|
||||||
@ -271,22 +278,17 @@ 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 = 16 / 9;
|
||||||
@ -301,8 +303,27 @@ export default function DraggableGridLayout({
|
|||||||
);
|
);
|
||||||
}, [containerWidth, marginValue]);
|
}, [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 (
|
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 && (
|
||||||
@ -344,6 +365,7 @@ export default function DraggableGridLayout({
|
|||||||
containerPadding={[0, isEditMode ? 6 : 3]}
|
containerPadding={[0, isEditMode ? 6 : 3]}
|
||||||
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
|
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
|
||||||
onDragStop={handleLayoutChange}
|
onDragStop={handleLayoutChange}
|
||||||
|
onResize={handleResize}
|
||||||
onResizeStop={handleLayoutChange}
|
onResizeStop={handleLayoutChange}
|
||||||
>
|
>
|
||||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||||
@ -394,7 +416,7 @@ export default function DraggableGridLayout({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ResponsiveGridLayout>
|
</ResponsiveGridLayout>
|
||||||
{isDesktop && !fullscreen && (
|
{isDesktop && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed",
|
"fixed",
|
||||||
@ -406,22 +428,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 +449,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 +464,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"}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user