clickable overlay div to navigate to full camera view

This commit is contained in:
Josh Hawkins 2025-06-08 12:46:33 -05:00
parent f549d2c0ab
commit f73a7086f9
2 changed files with 80 additions and 3 deletions

View File

@ -13,6 +13,7 @@ type LivePlayerProps = {
liveMode: LivePlayerMode; liveMode: LivePlayerMode;
pip?: boolean; pip?: boolean;
containerRef: React.MutableRefObject<HTMLDivElement | null>; containerRef: React.MutableRefObject<HTMLDivElement | null>;
playerRef?: React.MutableRefObject<HTMLDivElement | null>;
onClick?: () => void; onClick?: () => void;
}; };
@ -22,6 +23,7 @@ export default function BirdseyeLivePlayer({
liveMode, liveMode,
pip, pip,
containerRef, containerRef,
playerRef,
onClick, onClick,
}: LivePlayerProps) { }: LivePlayerProps) {
let player; let player;
@ -76,7 +78,9 @@ export default function BirdseyeLivePlayer({
> >
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-[30%] w-full rounded-lg bg-gradient-to-b from-black/20 to-transparent md:rounded-2xl"></div> <div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-[30%] w-full rounded-lg bg-gradient-to-b from-black/20 to-transparent md:rounded-2xl"></div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-[10%] w-full rounded-lg bg-gradient-to-t from-black/20 to-transparent md:rounded-2xl"></div> <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-[10%] w-full rounded-lg bg-gradient-to-t from-black/20 to-transparent md:rounded-2xl"></div>
<div className="size-full">{player}</div> <div className="size-full" ref={playerRef}>
{player}
</div>
</div> </div>
); );
} }

View File

@ -1,11 +1,13 @@
import { useBirdseyeLayout } from "@/api/ws";
import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle"; import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer"; import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
import { cn } from "@/lib/utils";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
isDesktop, isDesktop,
isFirefox, isFirefox,
@ -122,6 +124,72 @@ export default function LiveBirdseyeView({
return "mse"; return "mse";
}, [config]); }, [config]);
const birdseyeLayout = useBirdseyeLayout();
// Click overlay handling
const playerRef = useRef<HTMLDivElement | null>(null);
const handleOverlayClick = useCallback(
(
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
) => {
let clientX;
let clientY;
if ("TouchEvent" in window && e.nativeEvent instanceof TouchEvent) {
clientX = e.nativeEvent.touches[0].clientX;
clientY = e.nativeEvent.touches[0].clientY;
} else if (e.nativeEvent instanceof MouseEvent) {
clientX = e.nativeEvent.clientX;
clientY = e.nativeEvent.clientY;
}
if (
playerRef.current &&
clientX &&
clientY &&
config &&
birdseyeLayout?.payload
) {
const playerRect = playerRef.current.getBoundingClientRect();
// Calculate coordinates relative to player div, accounting for offset
const rawX = clientX - playerRect.left;
const rawY = clientY - playerRect.top;
// Ensure click is within player bounds
if (
rawX < 0 ||
rawX > playerRect.width ||
rawY < 0 ||
rawY > playerRect.height
) {
return;
}
// Scale click coordinates to birdseye canvas resolution
const canvasX = rawX * (config.birdseye.width / playerRect.width);
const canvasY = rawY * (config.birdseye.height / playerRect.height);
for (const [cameraName, coords] of Object.entries(
birdseyeLayout.payload,
)) {
const parsedCoords =
typeof coords === "string" ? JSON.parse(coords) : coords;
if (
canvasX >= parsedCoords.x &&
canvasX < parsedCoords.x + parsedCoords.width &&
canvasY >= parsedCoords.y &&
canvasY < parsedCoords.y + parsedCoords.height
) {
navigate(`/#${cameraName}`);
break;
}
}
}
},
[playerRef, config, birdseyeLayout, navigate],
);
if (!config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@ -215,16 +283,21 @@ export default function LiveBirdseyeView({
}} }}
> >
<div <div
className={growClassName} className={cn(
"flex flex-col items-center justify-center",
growClassName,
)}
style={{ style={{
aspectRatio: constrainedAspectRatio, aspectRatio: constrainedAspectRatio,
}} }}
onClick={handleOverlayClick}
> >
<BirdseyeLivePlayer <BirdseyeLivePlayer
className={`${fullscreen ? "*:rounded-none" : ""}`} className={`${fullscreen ? "*:rounded-none" : ""}`}
birdseyeConfig={config.birdseye} birdseyeConfig={config.birdseye}
liveMode={preferredLiveMode} liveMode={preferredLiveMode}
containerRef={containerRef} containerRef={containerRef}
playerRef={playerRef}
pip={pip} pip={pip}
/> />
</div> </div>