frigate/web/src/components/overlay/PtzControlPanel.tsx
Nicolas Mowen a08fda62f8
Implement debug live view as part of live (#20270)
* Cleanup components

* integrate debug view

* Refactor menu handling

* Cleanup

* cleanup

* Improve ptz placement for debug view

* Cleanup

* Cleanup mobile

* Always show options

* Add info for stream picking being disabled

* Add to mobile too

* Fix ns

* Cleanup
2025-09-29 18:45:55 -05:00

325 lines
8.4 KiB
TypeScript

import { usePtzCommand } from "@/api/ws";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { CameraPtzInfo } from "@/types/ptz";
import React, { useCallback } from "react";
import { isDesktop, isMobile } from "react-device-detect";
import { BsThreeDotsVertical } from "react-icons/bs";
import {
FaAngleDown,
FaAngleLeft,
FaAngleRight,
FaAngleUp,
} from "react-icons/fa";
import { TbViewfinder } from "react-icons/tb";
import {
MdCenterFocusStrong,
MdCenterFocusWeak,
MdZoomIn,
MdZoomOut,
} from "react-icons/md";
import useSWR from "swr";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import TooltipButton from "@/views/button/TooltipButton";
export default function PtzControlPanel({
className,
camera,
enabled,
clickOverlay,
setClickOverlay,
}: {
className?: string;
camera: string;
enabled: boolean;
clickOverlay: boolean;
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { t } = useTranslation(["views/live"]);
const { data: ptz } = useSWR<CameraPtzInfo>(
enabled ? `${camera}/ptz/info` : null,
);
const { send: sendPtz } = usePtzCommand(camera);
const onStop = useCallback(
(e: React.SyntheticEvent) => {
e.preventDefault();
sendPtz("STOP");
},
[sendPtz],
);
useKeyboardListener(
[
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"+",
"-",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
],
(key, modifiers) => {
if (modifiers.repeat || !key) {
return;
}
if (["1", "2", "3", "4", "5", "6", "7", "8", "9"].includes(key)) {
const presetNumber = parseInt(key);
if (
ptz &&
(ptz.presets?.length ?? 0) > 0 &&
presetNumber <= ptz.presets.length
) {
sendPtz(`preset_${ptz.presets[presetNumber - 1]}`);
}
return;
}
if (!modifiers.down) {
sendPtz("STOP");
return;
}
switch (key) {
case "ArrowLeft":
sendPtz("MOVE_LEFT");
break;
case "ArrowRight":
sendPtz("MOVE_RIGHT");
break;
case "ArrowUp":
sendPtz("MOVE_UP");
break;
case "ArrowDown":
sendPtz("MOVE_DOWN");
break;
case "+":
sendPtz(modifiers.shift ? "FOCUS_IN" : "ZOOM_IN");
break;
case "-":
sendPtz(modifiers.shift ? "FOCUS_OUT" : "ZOOM_OUT");
break;
}
},
);
return (
<div
className={cn(
"absolute inset-x-2 bottom-[10%] flex select-none flex-wrap items-center justify-center gap-1 md:left-[50%] md:-translate-x-[50%] md:flex-nowrap",
className ?? "",
isMobile && "landscape:ml-12",
)}
>
{ptz?.features?.includes("pt") && (
<>
<TooltipButton
label={t("ptz.move.left.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_LEFT");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("MOVE_LEFT");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<FaAngleLeft />
</TooltipButton>
<TooltipButton
label={t("ptz.move.up.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_UP");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("MOVE_UP");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<FaAngleUp />
</TooltipButton>
<TooltipButton
label={t("ptz.move.down.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_DOWN");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("MOVE_DOWN");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<FaAngleDown />
</TooltipButton>
<TooltipButton
label={t("ptz.move.right.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("MOVE_RIGHT");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("MOVE_RIGHT");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<FaAngleRight />
</TooltipButton>
</>
)}
{ptz?.features?.includes("zoom") && (
<>
<TooltipButton
label={t("ptz.zoom.in.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("ZOOM_IN");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("ZOOM_IN");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<MdZoomIn />
</TooltipButton>
<TooltipButton
label={t("ptz.zoom.out.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("ZOOM_OUT");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("ZOOM_OUT");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<MdZoomOut />
</TooltipButton>
</>
)}
{ptz?.features?.includes("focus") && (
<>
<TooltipButton
label={t("ptz.focus.in.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("FOCUS_IN");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("FOCUS_IN");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<MdCenterFocusStrong />
</TooltipButton>
<TooltipButton
label={t("ptz.focus.out.label")}
onMouseDown={(e) => {
e.preventDefault();
sendPtz("FOCUS_OUT");
}}
onTouchStart={(e) => {
e.preventDefault();
sendPtz("FOCUS_OUT");
}}
onMouseUp={onStop}
onTouchEnd={onStop}
>
<MdCenterFocusWeak />
</TooltipButton>
</>
)}
{ptz?.features?.includes("pt-r-fov") && (
<Tooltip>
<TooltipTrigger asChild>
<Button
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
aria-label={t("ptz.move.clickMove.label")}
onClick={() => setClickOverlay(!clickOverlay)}
>
<TbViewfinder />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{clickOverlay
? t("ptz.move.clickMove.disable")
: t("ptz.move.clickMove.enable")}
</p>
</TooltipContent>
</Tooltip>
)}
{(ptz?.presets?.length ?? 0) > 0 && (
<DropdownMenu modal={!isDesktop}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button aria-label={t("ptz.presets")}>
<BsThreeDotsVertical />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
<p>{t("ptz.presets")}</p>
</TooltipContent>
</Tooltip>
<DropdownMenuContent
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
onCloseAutoFocus={(e) => e.preventDefault()}
>
{ptz?.presets.map((preset) => (
<DropdownMenuItem
key={preset}
aria-label={preset}
className="cursor-pointer"
onSelect={() => sendPtz(`preset_${preset}`)}
>
{preset}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}