mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-22 02:18:32 +01:00
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
This commit is contained in:
324
web/src/components/overlay/PtzControlPanel.tsx
Normal file
324
web/src/components/overlay/PtzControlPanel.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user