mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-19 01:17:06 +03:00
add context menu to dashboards
This commit is contained in:
parent
9d2b678965
commit
1c5b6adf2b
@ -21,7 +21,7 @@ import {
|
|||||||
} from "react-grid-layout";
|
} 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 { LivePlayerError, LivePlayerMode } from "@/types/live";
|
import { LivePlayerMode } from "@/types/live";
|
||||||
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
||||||
|
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
||||||
|
|
||||||
type DraggableGridLayoutProps = {
|
type DraggableGridLayoutProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
@ -83,8 +84,13 @@ export default function DraggableGridLayout({
|
|||||||
|
|
||||||
// preferred live modes per camera
|
// preferred live modes per camera
|
||||||
|
|
||||||
const { preferredLiveModes, setPreferredLiveModes, resetPreferredLiveMode } =
|
const {
|
||||||
useCameraLiveMode(cameras, windowVisible);
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
} = useCameraLiveMode(cameras, windowVisible);
|
||||||
|
|
||||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||||
|
|
||||||
@ -359,6 +365,34 @@ export default function DraggableGridLayout({
|
|||||||
placeholder.h = layoutItem.h;
|
placeholder.h = layoutItem.h;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// audio states
|
||||||
|
|
||||||
|
const [audioStates, setAudioStates] = useState<Record<string, boolean>>({});
|
||||||
|
const [volumeStates, setVolumeStates] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
const toggleAudio = (cameraName: string): void => {
|
||||||
|
setAudioStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[cameraName]: !prev[cameraName],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const muteAll = (): void => {
|
||||||
|
const updatedStates: Record<string, boolean> = {};
|
||||||
|
visibleCameras.forEach((cameraName) => {
|
||||||
|
updatedStates[cameraName] = false;
|
||||||
|
});
|
||||||
|
setAudioStates(updatedStates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unmuteAll = (): void => {
|
||||||
|
const updatedStates: Record<string, boolean> = {};
|
||||||
|
visibleCameras.forEach((cameraName) => {
|
||||||
|
updatedStates[cameraName] = true;
|
||||||
|
});
|
||||||
|
setAudioStates(updatedStates);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
@ -381,7 +415,7 @@ export default function DraggableGridLayout({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="no-scrollbar my-2 overflow-x-hidden px-2 pb-8"
|
className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8"
|
||||||
ref={gridContainerRef}
|
ref={gridContainerRef}
|
||||||
>
|
>
|
||||||
<EditGroupDialog
|
<EditGroupDialog
|
||||||
@ -451,7 +485,29 @@ export default function DraggableGridLayout({
|
|||||||
currentGroupStreamingSettings?.[camera.name]
|
currentGroupStreamingSettings?.[camera.name]
|
||||||
?.compatibilityMode || false;
|
?.compatibilityMode || false;
|
||||||
return (
|
return (
|
||||||
<LivePlayerGridItem
|
<GridLiveContextMenu
|
||||||
|
key={camera.name}
|
||||||
|
camera={camera.name}
|
||||||
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||||
|
isRestreamed={isRestreamedStates[camera.name]}
|
||||||
|
supportsAudio={
|
||||||
|
supportsAudioOutputStates[streamName].supportsAudio
|
||||||
|
}
|
||||||
|
audioState={audioStates[camera.name]}
|
||||||
|
toggleAudio={() => toggleAudio(camera.name)}
|
||||||
|
volumeState={volumeStates[camera.name]}
|
||||||
|
setVolumeState={(value) =>
|
||||||
|
setVolumeStates({
|
||||||
|
[camera.name]: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
muteAll={muteAll}
|
||||||
|
unmuteAll={unmuteAll}
|
||||||
|
resetPreferredLiveMode={() =>
|
||||||
|
resetPreferredLiveMode(camera.name)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LivePlayer
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
streamName={streamName}
|
streamName={streamName}
|
||||||
autoLive={autoLive ?? globalAutoLive}
|
autoLive={autoLive ?? globalAutoLive}
|
||||||
@ -470,6 +526,7 @@ export default function DraggableGridLayout({
|
|||||||
}
|
}
|
||||||
cameraConfig={camera}
|
cameraConfig={camera}
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||||
|
playInBackground={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
!isEditMode && onSelectCamera(camera.name);
|
!isEditMode && onSelectCamera(camera.name);
|
||||||
}}
|
}}
|
||||||
@ -485,9 +542,9 @@ export default function DraggableGridLayout({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||||
>
|
/>
|
||||||
{isEditMode && showCircles && <CornerCircles />}
|
{isEditMode && showCircles && <CornerCircles />}
|
||||||
</LivePlayerGridItem>
|
</GridLiveContextMenu>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ResponsiveGridLayout>
|
</ResponsiveGridLayout>
|
||||||
@ -630,49 +687,47 @@ const BirdseyeLivePlayerGridItem = React.forwardRef<
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
type LivePlayerGridItemProps = {
|
type GridLiveContextMenuProps = {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className: string;
|
|
||||||
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
onMouseUp?: React.MouseEventHandler<HTMLDivElement>;
|
onMouseUp?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
cameraRef: (node: HTMLElement | null) => void;
|
camera: string;
|
||||||
windowVisible: boolean;
|
preferredLiveMode: string;
|
||||||
cameraConfig: CameraConfig;
|
isRestreamed: boolean;
|
||||||
preferredLiveMode: LivePlayerMode;
|
supportsAudio: boolean;
|
||||||
onClick: () => void;
|
audioState: boolean;
|
||||||
onError: (e: LivePlayerError) => void;
|
toggleAudio: () => void;
|
||||||
onResetLiveMode: () => void;
|
volumeState?: number;
|
||||||
streamName: string;
|
setVolumeState: (volumeState: number) => void;
|
||||||
autoLive: boolean;
|
muteAll: () => void;
|
||||||
showStillWithoutActivity: boolean;
|
unmuteAll: () => void;
|
||||||
useWebGL: boolean;
|
resetPreferredLiveMode: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LivePlayerGridItem = React.forwardRef<
|
const GridLiveContextMenu = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
LivePlayerGridItemProps
|
GridLiveContextMenuProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
style,
|
style,
|
||||||
className,
|
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
onMouseUp,
|
onMouseUp,
|
||||||
onTouchEnd,
|
onTouchEnd,
|
||||||
children,
|
children,
|
||||||
cameraRef,
|
camera,
|
||||||
windowVisible,
|
|
||||||
cameraConfig,
|
|
||||||
preferredLiveMode,
|
preferredLiveMode,
|
||||||
onClick,
|
isRestreamed,
|
||||||
onError,
|
supportsAudio,
|
||||||
onResetLiveMode,
|
audioState,
|
||||||
autoLive,
|
toggleAudio,
|
||||||
showStillWithoutActivity,
|
volumeState,
|
||||||
streamName,
|
setVolumeState,
|
||||||
useWebGL,
|
muteAll,
|
||||||
|
unmuteAll,
|
||||||
|
resetPreferredLiveMode,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@ -686,23 +741,21 @@ const LivePlayerGridItem = React.forwardRef<
|
|||||||
onTouchEnd={onTouchEnd}
|
onTouchEnd={onTouchEnd}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<LivePlayer
|
<LiveContextMenu
|
||||||
cameraRef={cameraRef}
|
camera={camera}
|
||||||
className={className}
|
|
||||||
windowVisible={windowVisible}
|
|
||||||
cameraConfig={cameraConfig}
|
|
||||||
preferredLiveMode={preferredLiveMode}
|
preferredLiveMode={preferredLiveMode}
|
||||||
streamName={streamName}
|
isRestreamed={isRestreamed}
|
||||||
onClick={onClick}
|
supportsAudio={supportsAudio}
|
||||||
onError={onError}
|
audioState={audioState}
|
||||||
onResetLiveMode={onResetLiveMode}
|
toggleAudio={toggleAudio}
|
||||||
containerRef={ref as React.RefObject<HTMLDivElement>}
|
volumeState={volumeState}
|
||||||
autoLive={autoLive}
|
setVolumeState={setVolumeState}
|
||||||
playInBackground={false}
|
muteAll={muteAll}
|
||||||
showStillWithoutActivity={showStillWithoutActivity}
|
unmuteAll={unmuteAll}
|
||||||
useWebGL={useWebGL}
|
resetPreferredLiveMode={resetPreferredLiveMode}
|
||||||
/>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</LiveContextMenu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import { LivePlayerError } from "@/types/live";
|
|||||||
import { FaCompress, FaExpand } from "react-icons/fa";
|
import { FaCompress, FaExpand } from "react-icons/fa";
|
||||||
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
|
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
||||||
|
|
||||||
type LiveDashboardViewProps = {
|
type LiveDashboardViewProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
@ -188,8 +189,13 @@ export default function LiveDashboardView({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { preferredLiveModes, setPreferredLiveModes, resetPreferredLiveMode } =
|
const {
|
||||||
useCameraLiveMode(cameras, windowVisible);
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
} = useCameraLiveMode(cameras, windowVisible);
|
||||||
|
|
||||||
const [allGroupsStreamingSettings, setAllGroupsStreamingSettings] =
|
const [allGroupsStreamingSettings, setAllGroupsStreamingSettings] =
|
||||||
useState<AllGroupsStreamingSettings>({});
|
useState<AllGroupsStreamingSettings>({});
|
||||||
@ -237,9 +243,37 @@ export default function LiveDashboardView({
|
|||||||
[setPreferredLiveModes],
|
[setPreferredLiveModes],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// audio states
|
||||||
|
|
||||||
|
const [audioStates, setAudioStates] = useState<Record<string, boolean>>({});
|
||||||
|
const [volumeStates, setVolumeStates] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
const toggleAudio = (cameraName: string): void => {
|
||||||
|
setAudioStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[cameraName]: !prev[cameraName],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const muteAll = (): void => {
|
||||||
|
const updatedStates: Record<string, boolean> = {};
|
||||||
|
visibleCameras.forEach((cameraName) => {
|
||||||
|
updatedStates[cameraName] = false;
|
||||||
|
});
|
||||||
|
setAudioStates(updatedStates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unmuteAll = (): void => {
|
||||||
|
const updatedStates: Record<string, boolean> = {};
|
||||||
|
visibleCameras.forEach((cameraName) => {
|
||||||
|
updatedStates[cameraName] = true;
|
||||||
|
});
|
||||||
|
setAudioStates(updatedStates);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="scrollbar-container size-full overflow-y-auto px-1 pt-2 md:p-2"
|
className="scrollbar-container size-full select-none overflow-y-auto px-1 pt-2 md:p-2"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
@ -364,6 +398,30 @@ export default function LiveDashboardView({
|
|||||||
grow = "aspect-video";
|
grow = "aspect-video";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
<LiveContextMenu
|
||||||
|
key={camera.name}
|
||||||
|
camera={camera.name}
|
||||||
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||||
|
isRestreamed={isRestreamedStates[camera.name]}
|
||||||
|
supportsAudio={
|
||||||
|
supportsAudioOutputStates[
|
||||||
|
Object.values(camera.live.streams)?.[0]
|
||||||
|
]?.supportsAudio ?? false
|
||||||
|
}
|
||||||
|
audioState={audioStates[camera.name]}
|
||||||
|
toggleAudio={() => toggleAudio(camera.name)}
|
||||||
|
volumeState={volumeStates[camera.name]}
|
||||||
|
setVolumeState={(value) =>
|
||||||
|
setVolumeStates({
|
||||||
|
[camera.name]: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
muteAll={muteAll}
|
||||||
|
unmuteAll={unmuteAll}
|
||||||
|
resetPreferredLiveMode={() =>
|
||||||
|
resetPreferredLiveMode(camera.name)
|
||||||
|
}
|
||||||
|
>
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
cameraRef={cameraRef}
|
cameraRef={cameraRef}
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
@ -380,7 +438,10 @@ export default function LiveDashboardView({
|
|||||||
onClick={() => onSelectCamera(camera.name)}
|
onClick={() => onSelectCamera(camera.name)}
|
||||||
onError={(e) => handleError(camera.name, e)}
|
onError={(e) => handleError(camera.name, e)}
|
||||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||||
|
playAudio={audioStates[camera.name] ?? false}
|
||||||
|
volume={volumeStates[camera.name]}
|
||||||
/>
|
/>
|
||||||
|
</LiveContextMenu>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user