diff --git a/web/package-lock.json b/web/package-lock.json index 03734f4f9..7b8bb2bde 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -63,7 +63,7 @@ "react-device-detect": "^2.2.3", "react-dom": "^18.3.1", "react-dropzone": "^14.3.8", - "react-grid-layout": "^1.5.0", + "react-grid-layout": "^2.2.2", "react-hook-form": "^7.52.1", "react-i18next": "^15.2.0", "react-icons": "^5.5.0", @@ -96,7 +96,6 @@ "@types/node": "^20.14.10", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", - "@types/react-grid-layout": "^1.3.5", "@types/react-icons": "^3.0.0", "@types/react-transition-group": "^4.4.10", "@types/strftime": "^0.9.8", @@ -5223,15 +5222,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-grid-layout": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", - "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-icons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz", @@ -10959,11 +10949,12 @@ } }, "node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", "dependencies": { - "clsx": "^1.1.1", + "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { @@ -10971,14 +10962,6 @@ "react-dom": ">= 16.3.0" } }, - "node_modules/react-draggable/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" - } - }, "node_modules/react-dropzone": { "version": "14.3.8", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", @@ -10997,15 +10980,15 @@ } }, "node_modules/react-grid-layout": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", - "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", + "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", "license": "MIT", "dependencies": { - "clsx": "^2.0.0", + "clsx": "^2.1.1", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", - "react-draggable": "^4.4.5", + "react-draggable": "^4.4.6", "react-resizable": "^3.0.5", "resize-observer-polyfill": "^1.5.1" }, @@ -11188,15 +11171,17 @@ } }, "node_modules/react-resizable": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", - "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz", + "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==", + "license": "MIT", "dependencies": { "prop-types": "15.x", - "react-draggable": "^4.0.3" + "react-draggable": "^4.5.0" }, "peerDependencies": { - "react": ">= 16.3" + "react": ">= 16.3", + "react-dom": ">= 16.3" } }, "node_modules/react-router": { diff --git a/web/package.json b/web/package.json index 0ceb82ef9..49825da36 100644 --- a/web/package.json +++ b/web/package.json @@ -69,7 +69,7 @@ "react-device-detect": "^2.2.3", "react-dom": "^18.3.1", "react-dropzone": "^14.3.8", - "react-grid-layout": "^1.5.0", + "react-grid-layout": "^2.2.2", "react-hook-form": "^7.52.1", "react-i18next": "^15.2.0", "react-icons": "^5.5.0", @@ -102,7 +102,6 @@ "@types/node": "^20.14.10", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", - "@types/react-grid-layout": "^1.3.5", "@types/react-icons": "^3.0.0", "@types/react-transition-group": "^4.4.10", "@types/strftime": "^0.9.8", diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 5b875ae5c..6d9c6262c 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -14,10 +14,10 @@ import React, { useState, } from "react"; import { - ItemCallback, Layout, + LayoutItem, Responsive, - WidthProvider, + useContainerWidth, } from "react-grid-layout"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; @@ -116,11 +116,8 @@ export default function DraggableGridLayout({ // grid layout - const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []); - - const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence< - Layout[] - >(`${cameraGroup}-draggable-layout`); + const [gridLayout, setGridLayout, isGridLayoutLoaded] = + useUserPersistence(`${cameraGroup}-draggable-layout`); const [group] = useUserPersistedOverlayState( "cameraGroup", @@ -158,11 +155,11 @@ export default function DraggableGridLayout({ const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = useState(); const [currentGridLayout, setCurrentGridLayout] = useState< - Layout[] | undefined + Layout | undefined >(); const handleLayoutChange = useCallback( - (currentLayout: Layout[]) => { + (currentLayout: Layout) => { if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) { return; } @@ -174,7 +171,7 @@ export default function DraggableGridLayout({ ); const generateLayout = useCallback( - (baseLayout: Layout[] | undefined) => { + (baseLayout: Layout | undefined) => { if (!isGridLayoutLoaded) { return; } @@ -184,7 +181,7 @@ export default function DraggableGridLayout({ ? ["birdseye", ...cameras.map((camera) => camera?.name || "")] : cameras.map((camera) => camera?.name || ""); - const optionsMap: Layout[] = baseLayout + const optionsMap: LayoutItem[] = baseLayout ? baseLayout.filter((layout) => cameraNames?.includes(layout.i)) : []; @@ -325,6 +322,24 @@ export default function DraggableGridLayout({ const gridContainerRef = useRef(null); + const { + width: gridWidth, + containerRef: gridWidthRef, + mounted: gridMounted, + } = useContainerWidth(); + + // Combine gridContainerRef and gridWidthRef into a single callback ref + const combinedGridRef = useCallback( + (node: HTMLDivElement | null) => { + ( + gridContainerRef as React.MutableRefObject + ).current = node; + (gridWidthRef as React.MutableRefObject).current = + node; + }, + [gridWidthRef], + ); + const [{ width: containerWidth, height: containerHeight }] = useResizeObserver(gridContainerRef); @@ -363,12 +378,14 @@ export default function DraggableGridLayout({ ); }, [availableWidth, marginValue]); - const handleResize: ItemCallback = ( - _: Layout[], - oldLayoutItem: Layout, - layoutItem: Layout, - placeholder: Layout, + const handleResize = ( + _layout: Layout, + oldLayoutItem: LayoutItem | null, + layoutItem: LayoutItem | null, + placeholder: LayoutItem | null, ) => { + if (!oldLayoutItem || !layoutItem || !placeholder) return; + const heightDiff = layoutItem.h - oldLayoutItem.h; const widthDiff = layoutItem.w - oldLayoutItem.w; const changeCoef = oldLayoutItem.w / oldLayoutItem.h; @@ -529,7 +546,7 @@ export default function DraggableGridLayout({ ) : (
- setShowCircles(false)} - onResizeStop={handleLayoutChange} - isDraggable={isEditMode} - isResizable={isEditMode} - > - {includeBirdseye && birdseyeConfig?.enabled && ( - onSelectCamera("birdseye")} - > - {isEditMode && showCircles && } - - )} - {cameras.map((camera) => { - let grow; - const aspectRatio = camera.detect.width / camera.detect.height; - if (aspectRatio > ASPECT_WIDE_LAYOUT) { - grow = `aspect-wide w-full`; - } else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) { - grow = `aspect-tall h-full`; - } else { - grow = "aspect-video"; - } - const availableStreams = camera.live.streams || {}; - const firstStreamEntry = Object.values(availableStreams)[0] || ""; - - const streamNameFromSettings = - currentGroupStreamingSettings?.[camera.name]?.streamName || ""; - const streamExists = - streamNameFromSettings && - Object.values(availableStreams).includes( - streamNameFromSettings, - ); - - const streamName = streamExists - ? streamNameFromSettings - : firstStreamEntry; - const streamType = - currentGroupStreamingSettings?.[camera.name]?.streamType; - const autoLive = - streamType !== undefined - ? streamType !== "no-streaming" - : undefined; - const showStillWithoutActivity = - currentGroupStreamingSettings?.[camera.name]?.streamType !== - "continuous"; - const useWebGL = - currentGroupStreamingSettings?.[camera.name] - ?.compatibilityMode || false; - return ( - toggleAudio(camera.name)} - statsState={statsStates[camera.name]} - toggleStats={() => toggleStats(camera.name)} - volumeState={volumeStates[camera.name]} - setVolumeState={(value) => - setVolumeStates({ - [camera.name]: value, - }) - } - muteAll={muteAll} - unmuteAll={unmuteAll} - resetPreferredLiveMode={() => - resetPreferredLiveMode(camera.name) - } - config={config} - streamMetadata={streamMetadata} + {gridMounted && ( + setShowCircles(false)} + onResizeStop={handleLayoutChange} + > + {includeBirdseye && birdseyeConfig?.enabled && ( + onSelectCamera("birdseye")} > - { - !isEditMode && onSelectCamera(camera.name); - }} - onError={(e) => { - setPreferredLiveModes((prevModes) => { - const newModes = { ...prevModes }; - if (e === "mse-decode") { - newModes[camera.name] = "webrtc"; - } else { - newModes[camera.name] = "jsmpeg"; - } - return newModes; - }); - }} - onResetLiveMode={() => resetPreferredLiveMode(camera.name)} - playAudio={audioStates[camera.name]} - volume={volumeStates[camera.name]} - /> {isEditMode && showCircles && } - - ); - })} - + + )} + {cameras.map((camera) => { + let grow; + const aspectRatio = camera.detect.width / camera.detect.height; + if (aspectRatio > ASPECT_WIDE_LAYOUT) { + grow = `aspect-wide w-full`; + } else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) { + grow = `aspect-tall h-full`; + } else { + grow = "aspect-video"; + } + const availableStreams = camera.live.streams || {}; + const firstStreamEntry = + Object.values(availableStreams)[0] || ""; + + const streamNameFromSettings = + currentGroupStreamingSettings?.[camera.name]?.streamName || + ""; + const streamExists = + streamNameFromSettings && + Object.values(availableStreams).includes( + streamNameFromSettings, + ); + + const streamName = streamExists + ? streamNameFromSettings + : firstStreamEntry; + const streamType = + currentGroupStreamingSettings?.[camera.name]?.streamType; + const autoLive = + streamType !== undefined + ? streamType !== "no-streaming" + : undefined; + const showStillWithoutActivity = + currentGroupStreamingSettings?.[camera.name]?.streamType !== + "continuous"; + const useWebGL = + currentGroupStreamingSettings?.[camera.name] + ?.compatibilityMode || false; + return ( + toggleAudio(camera.name)} + statsState={statsStates[camera.name]} + toggleStats={() => toggleStats(camera.name)} + volumeState={volumeStates[camera.name]} + setVolumeState={(value) => + setVolumeStates({ + [camera.name]: value, + }) + } + muteAll={muteAll} + unmuteAll={unmuteAll} + resetPreferredLiveMode={() => + resetPreferredLiveMode(camera.name) + } + config={config} + streamMetadata={streamMetadata} + > + { + !isEditMode && onSelectCamera(camera.name); + }} + onError={(e) => { + setPreferredLiveModes((prevModes) => { + const newModes = { ...prevModes }; + if (e === "mse-decode") { + newModes[camera.name] = "webrtc"; + } else { + newModes[camera.name] = "jsmpeg"; + } + return newModes; + }); + }} + onResetLiveMode={() => + resetPreferredLiveMode(camera.name) + } + playAudio={audioStates[camera.name]} + volume={volumeStates[camera.name]} + /> + {isEditMode && showCircles && } + + ); + })} + + )} {isDesktop && (