mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-08-27 13:47:50 +02:00
Draggable camera grid tweaks (#11291)
* better math and other tweaks * change icon
This commit is contained in:
parent
e7ba556919
commit
db8c820677
@ -7,6 +7,7 @@ import {
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -20,7 +21,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { isSafari } from "react-device-detect";
|
import { isDesktop, isMobile, isSafari } from "react-device-detect";
|
||||||
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
import LivePlayer from "@/components/player/LivePlayer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -30,25 +31,32 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { IoClose } from "react-icons/io5";
|
import { IoClose } from "react-icons/io5";
|
||||||
import { LuMoveDiagonal2 } from "react-icons/lu";
|
import { LuMove } from "react-icons/lu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type DraggableGridLayoutProps = {
|
type DraggableGridLayoutProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
cameraGroup: string;
|
cameraGroup: string;
|
||||||
cameraRef: (node: HTMLElement | null) => void;
|
cameraRef: (node: HTMLElement | null) => void;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
includeBirdseye: boolean;
|
includeBirdseye: boolean;
|
||||||
onSelectCamera: (camera: string) => void;
|
onSelectCamera: (camera: string) => void;
|
||||||
windowVisible: boolean;
|
windowVisible: boolean;
|
||||||
visibleCameras: string[];
|
visibleCameras: string[];
|
||||||
|
isEditMode: boolean;
|
||||||
|
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
export default function DraggableGridLayout({
|
export default function DraggableGridLayout({
|
||||||
cameras,
|
cameras,
|
||||||
cameraGroup,
|
cameraGroup,
|
||||||
|
containerRef,
|
||||||
cameraRef,
|
cameraRef,
|
||||||
includeBirdseye,
|
includeBirdseye,
|
||||||
onSelectCamera,
|
onSelectCamera,
|
||||||
windowVisible,
|
windowVisible,
|
||||||
visibleCameras,
|
visibleCameras,
|
||||||
|
isEditMode,
|
||||||
|
setIsEditMode,
|
||||||
}: DraggableGridLayoutProps) {
|
}: DraggableGridLayoutProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
@ -66,8 +74,6 @@ export default function DraggableGridLayout({
|
|||||||
Layout[] | undefined
|
Layout[] | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const [isEditMode, setIsEditMode] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleLayoutChange = useCallback(
|
const handleLayoutChange = useCallback(
|
||||||
(currentLayout: Layout[]) => {
|
(currentLayout: Layout[]) => {
|
||||||
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
|
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
|
||||||
@ -160,12 +166,12 @@ export default function DraggableGridLayout({
|
|||||||
birdseyeConfig,
|
birdseyeConfig,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toggleEditMode = useCallback(() => {
|
useEffect(() => {
|
||||||
if (currentGridLayout) {
|
if (currentGridLayout) {
|
||||||
const updatedGridLayout = currentGridLayout.map((layout) => ({
|
const updatedGridLayout = currentGridLayout.map((layout) => ({
|
||||||
...layout,
|
...layout,
|
||||||
isDraggable: !isEditMode,
|
isDraggable: isEditMode,
|
||||||
isResizable: !isEditMode,
|
isResizable: isEditMode,
|
||||||
}));
|
}));
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
setGridLayout(updatedGridLayout);
|
setGridLayout(updatedGridLayout);
|
||||||
@ -173,9 +179,10 @@ export default function DraggableGridLayout({
|
|||||||
} else {
|
} else {
|
||||||
setGridLayout(updatedGridLayout);
|
setGridLayout(updatedGridLayout);
|
||||||
}
|
}
|
||||||
setIsEditMode((prevIsEditMode) => !prevIsEditMode);
|
|
||||||
}
|
}
|
||||||
}, [currentGridLayout, isEditMode, setGridLayout]);
|
// we know that these deps are correct
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isEditMode, setGridLayout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isGridLayoutLoaded) {
|
if (isGridLayoutLoaded) {
|
||||||
@ -218,31 +225,58 @@ export default function DraggableGridLayout({
|
|||||||
isGridLayoutLoaded,
|
isGridLayoutLoaded,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const [marginValue, setMarginValue] = useState(16);
|
||||||
|
|
||||||
|
// calculate margin value for browsers that don't have default font size of 16px
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const calculateRemValue = () => {
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
const fontSize = window.getComputedStyle(htmlElement).fontSize;
|
||||||
|
setMarginValue(parseFloat(fontSize));
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateRemValue();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [{ width: containerWidth }] = useResizeObserver(gridContainerRef);
|
const [{ width: containerWidth, height: containerHeight }] =
|
||||||
|
useResizeObserver(gridContainerRef);
|
||||||
|
|
||||||
|
const hasScrollbar = useMemo(() => {
|
||||||
|
return (
|
||||||
|
containerHeight &&
|
||||||
|
containerRef.current &&
|
||||||
|
containerRef.current.offsetHeight <
|
||||||
|
(gridContainerRef.current?.scrollHeight ?? 0)
|
||||||
|
);
|
||||||
|
}, [containerRef, gridContainerRef, containerHeight]);
|
||||||
|
|
||||||
const cellHeight = useMemo(() => {
|
const cellHeight = useMemo(() => {
|
||||||
const aspectRatio = 16 / 9;
|
const aspectRatio = 16 / 9;
|
||||||
const totalMarginWidth = 11 * 13; // 11 margins with 13px each
|
// subtract container margin, 1 camera takes up at least 4 rows
|
||||||
const rowHeight =
|
// account for additional margin on bottom of each row
|
||||||
((containerWidth ?? window.innerWidth) - totalMarginWidth) /
|
return (
|
||||||
(13 * aspectRatio);
|
((containerWidth ?? window.innerWidth) - 2 * marginValue) /
|
||||||
return rowHeight;
|
12 /
|
||||||
}, [containerWidth]);
|
aspectRatio -
|
||||||
|
marginValue +
|
||||||
|
marginValue / 4
|
||||||
|
);
|
||||||
|
}, [containerWidth, marginValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!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 && (
|
||||||
<Skeleton className="size-full rounded-2xl" />
|
<Skeleton className="size-full rounded-lg md:rounded-2xl" />
|
||||||
)}
|
)}
|
||||||
{cameras.map((camera) => {
|
{cameras.map((camera) => {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
className="aspect-video size-full rounded-2xl"
|
className="aspect-video size-full rounded-lg md:rounded-2xl"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -264,37 +298,33 @@ export default function DraggableGridLayout({
|
|||||||
rowHeight={cellHeight}
|
rowHeight={cellHeight}
|
||||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
|
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
|
||||||
margin={[16, 16]}
|
margin={[marginValue, marginValue]}
|
||||||
containerPadding={[8, 8]}
|
containerPadding={[0, isEditMode ? 6 : 3]}
|
||||||
resizeHandles={["sw", "nw", "se", "ne"]}
|
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
|
||||||
onDragStop={handleLayoutChange}
|
onDragStop={handleLayoutChange}
|
||||||
onResizeStop={handleLayoutChange}
|
onResizeStop={handleLayoutChange}
|
||||||
>
|
>
|
||||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||||
<BirdseyeLivePlayerGridItem
|
<BirdseyeLivePlayerGridItem
|
||||||
key="birdseye"
|
key="birdseye"
|
||||||
className={`${isEditMode ? "outline outline-2 hover:outline-4 outline-muted-foreground hover:cursor-grab active:cursor-grabbing" : ""}`}
|
className={cn(
|
||||||
|
isEditMode &&
|
||||||
|
"outline outline-2 hover:outline-4 outline-muted-foreground hover:cursor-grab active:cursor-grabbing",
|
||||||
|
)}
|
||||||
birdseyeConfig={birdseyeConfig}
|
birdseyeConfig={birdseyeConfig}
|
||||||
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
||||||
onClick={() => onSelectCamera("birdseye")}
|
onClick={() => onSelectCamera("birdseye")}
|
||||||
>
|
>
|
||||||
{isEditMode && (
|
{isEditMode && <CornerCircles />}
|
||||||
<>
|
|
||||||
<div className="absolute top-[-6px] left-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
<div className="absolute top-[-6px] right-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
<div className="absolute bottom-[-6px] right-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
<div className="absolute bottom-[-6px] left-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</BirdseyeLivePlayerGridItem>
|
</BirdseyeLivePlayerGridItem>
|
||||||
)}
|
)}
|
||||||
{cameras.map((camera) => {
|
{cameras.map((camera) => {
|
||||||
let grow;
|
let grow;
|
||||||
const aspectRatio = camera.detect.width / camera.detect.height;
|
const aspectRatio = camera.detect.width / camera.detect.height;
|
||||||
if (aspectRatio > ASPECT_WIDE_LAYOUT) {
|
if (aspectRatio > ASPECT_WIDE_LAYOUT) {
|
||||||
grow = `aspect-wide`;
|
grow = `aspect-wide w-full`;
|
||||||
} else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
|
} else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
|
||||||
grow = `aspect-tall`;
|
grow = `aspect-tall h-full`;
|
||||||
} else {
|
} else {
|
||||||
grow = "aspect-video";
|
grow = "aspect-video";
|
||||||
}
|
}
|
||||||
@ -302,7 +332,12 @@ export default function DraggableGridLayout({
|
|||||||
<LivePlayerGridItem
|
<LivePlayerGridItem
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
cameraRef={cameraRef}
|
cameraRef={cameraRef}
|
||||||
className={`${grow} size-full rounded-lg md:rounded-2xl bg-black ${isEditMode ? "outline-2 hover:outline-4 outline-muted-foreground hover:cursor-grab active:cursor-grabbing" : ""}`}
|
className={cn(
|
||||||
|
"rounded-lg md:rounded-2xl bg-black",
|
||||||
|
grow,
|
||||||
|
isEditMode &&
|
||||||
|
"outline-2 hover:outline-4 outline-muted-foreground hover:cursor-grab active:cursor-grabbing",
|
||||||
|
)}
|
||||||
windowVisible={
|
windowVisible={
|
||||||
windowVisible && visibleCameras.includes(camera.name)
|
windowVisible && visibleCameras.includes(camera.name)
|
||||||
}
|
}
|
||||||
@ -312,44 +347,76 @@ export default function DraggableGridLayout({
|
|||||||
!isEditMode && onSelectCamera(camera.name);
|
!isEditMode && onSelectCamera(camera.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEditMode && (
|
{isEditMode && <CornerCircles />}
|
||||||
<>
|
|
||||||
<div className="absolute top-[-6px] left-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
<div className="absolute top-[-6px] right-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
<div className="absolute bottom-[-6px] right-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
<div className="absolute bottom-[-6px] left-[-6px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</LivePlayerGridItem>
|
</LivePlayerGridItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ResponsiveGridLayout>
|
</ResponsiveGridLayout>
|
||||||
<div className="flex flex-row gap-2 items-center text-primary">
|
{isDesktop && (
|
||||||
<Tooltip>
|
<DesktopEditLayoutButton
|
||||||
<TooltipTrigger asChild>
|
isEditMode={isEditMode}
|
||||||
<Button
|
setIsEditMode={setIsEditMode}
|
||||||
variant="default"
|
hasScrollbar={hasScrollbar}
|
||||||
className="fixed bottom-12 lg:bottom-9 right-5 z-50 h-12 w-12 p-0 rounded-full opacity-30 hover:opacity-100 transition-all duration-300"
|
/>
|
||||||
onClick={toggleEditMode}
|
)}
|
||||||
>
|
|
||||||
{isEditMode ? (
|
|
||||||
<IoClose className="size-5" />
|
|
||||||
) : (
|
|
||||||
<LuMoveDiagonal2 className="size-5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="left">
|
|
||||||
{isEditMode ? "Exit Editing" : "Edit Layout"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DesktopEditLayoutButtonProps = {
|
||||||
|
isEditMode?: boolean;
|
||||||
|
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
hasScrollbar?: boolean | 0 | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DesktopEditLayoutButton({
|
||||||
|
isEditMode,
|
||||||
|
setIsEditMode,
|
||||||
|
hasScrollbar,
|
||||||
|
}: DesktopEditLayoutButtonProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row gap-2 items-center text-primary">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className={cn(
|
||||||
|
"fixed",
|
||||||
|
isDesktop && "bottom-12 lg:bottom-9",
|
||||||
|
isMobile && "bottom-12 lg:bottom-16",
|
||||||
|
hasScrollbar && isDesktop ? "right-6" : "right-1",
|
||||||
|
"z-50 h-8 w-8 p-0 rounded-full opacity-30 hover:opacity-100 transition-all duration-300",
|
||||||
|
)}
|
||||||
|
onClick={() => setIsEditMode((prevIsEditMode) => !prevIsEditMode)}
|
||||||
|
>
|
||||||
|
{isEditMode ? (
|
||||||
|
<IoClose className="size-5" />
|
||||||
|
) : (
|
||||||
|
<LuMove className="size-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left">
|
||||||
|
{isEditMode ? "Exit Editing" : "Edit Layout"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CornerCircles() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="absolute top-[-4px] left-[-4px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
||||||
|
<div className="absolute top-[-4px] right-[-4px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
||||||
|
<div className="absolute bottom-[-4px] right-[-4px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
||||||
|
<div className="absolute bottom-[-4px] left-[-4px] z-50 size-3 p-2 rounded-full bg-primary-variant outline-2 outline-muted text-background pointer-events-none" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type BirdseyeLivePlayerGridItemProps = {
|
type BirdseyeLivePlayerGridItemProps = {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -17,9 +17,12 @@ import {
|
|||||||
isMobile,
|
isMobile,
|
||||||
isMobileOnly,
|
isMobileOnly,
|
||||||
isSafari,
|
isSafari,
|
||||||
|
isTablet,
|
||||||
} from "react-device-detect";
|
} from "react-device-detect";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import DraggableGridLayout from "./DraggableGridLayout";
|
import DraggableGridLayout from "./DraggableGridLayout";
|
||||||
|
import { IoClose } from "react-icons/io5";
|
||||||
|
import { LuMove } from "react-icons/lu";
|
||||||
|
|
||||||
type LiveDashboardViewProps = {
|
type LiveDashboardViewProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
@ -42,6 +45,9 @@ export default function LiveDashboardView({
|
|||||||
isDesktop ? "grid" : "list",
|
isDesktop ? "grid" : "list",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isEditMode, setIsEditMode] = useState<boolean>(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// recent events
|
// recent events
|
||||||
const { payload: eventUpdate } = useFrigateReviews();
|
const { payload: eventUpdate } = useFrigateReviews();
|
||||||
const { data: allEvents, mutate: updateEvents } = useSWR<ReviewSegment[]>([
|
const { data: allEvents, mutate: updateEvents } = useSWR<ReviewSegment[]>([
|
||||||
@ -148,37 +154,52 @@ export default function LiveDashboardView({
|
|||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full p-2 overflow-y-auto">
|
<div className="size-full p-2 overflow-y-auto" ref={containerRef}>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="h-11 relative flex items-center justify-between">
|
<div className="h-11 relative flex items-center justify-between">
|
||||||
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
||||||
<div className="max-w-[45%]">
|
<div className="max-w-[45%]">
|
||||||
<CameraGroupSelector />
|
<CameraGroupSelector />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
{(!cameraGroup || cameraGroup == "default" || isMobileOnly) && (
|
||||||
<Button
|
<div className="flex items-center gap-1">
|
||||||
className={`p-1 ${
|
<Button
|
||||||
mobileLayout == "grid"
|
className={`p-1 ${
|
||||||
? "bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
mobileLayout == "grid"
|
||||||
: "bg-secondary"
|
? "bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
||||||
}`}
|
: "bg-secondary"
|
||||||
size="xs"
|
}`}
|
||||||
onClick={() => setMobileLayout("grid")}
|
size="xs"
|
||||||
>
|
onClick={() => setMobileLayout("grid")}
|
||||||
<LiveGridIcon layout={mobileLayout} />
|
>
|
||||||
</Button>
|
<LiveGridIcon layout={mobileLayout} />
|
||||||
<Button
|
</Button>
|
||||||
className={`p-1 ${
|
<Button
|
||||||
mobileLayout == "list"
|
className={`p-1 ${
|
||||||
? "bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
mobileLayout == "list"
|
||||||
: "bg-secondary"
|
? "bg-blue-900 focus:bg-blue-900 bg-opacity-60 focus:bg-opacity-60"
|
||||||
}`}
|
: "bg-secondary"
|
||||||
size="xs"
|
}`}
|
||||||
onClick={() => setMobileLayout("list")}
|
size="xs"
|
||||||
>
|
onClick={() => setMobileLayout("list")}
|
||||||
<LiveListIcon layout={mobileLayout} />
|
>
|
||||||
</Button>
|
<LiveListIcon layout={mobileLayout} />
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cameraGroup && cameraGroup !== "default" && isTablet && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
className="p-1"
|
||||||
|
size="xs"
|
||||||
|
onClick={() =>
|
||||||
|
setIsEditMode((prevIsEditMode) => !prevIsEditMode)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isEditMode ? <IoClose /> : <LuMove />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -235,11 +256,14 @@ export default function LiveDashboardView({
|
|||||||
<DraggableGridLayout
|
<DraggableGridLayout
|
||||||
cameras={cameras}
|
cameras={cameras}
|
||||||
cameraGroup={cameraGroup}
|
cameraGroup={cameraGroup}
|
||||||
|
containerRef={containerRef}
|
||||||
cameraRef={cameraRef}
|
cameraRef={cameraRef}
|
||||||
includeBirdseye={includeBirdseye}
|
includeBirdseye={includeBirdseye}
|
||||||
onSelectCamera={onSelectCamera}
|
onSelectCamera={onSelectCamera}
|
||||||
windowVisible={windowVisible}
|
windowVisible={windowVisible}
|
||||||
visibleCameras={visibleCameras}
|
visibleCameras={visibleCameras}
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
setIsEditMode={setIsEditMode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user