From a39f4b8192f90e4839feaf34df4edbb97aac1725 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 29 Sep 2025 13:04:15 -0600 Subject: [PATCH] Cleanup components --- .../components/overlay/PtzControlPanel.tsx | 321 ++++++++++++++++ web/src/views/button/TooltipButton.tsx | 52 +++ web/src/views/live/LiveCameraView.tsx | 348 +----------------- 3 files changed, 374 insertions(+), 347 deletions(-) create mode 100644 web/src/components/overlay/PtzControlPanel.tsx create mode 100644 web/src/views/button/TooltipButton.tsx diff --git a/web/src/components/overlay/PtzControlPanel.tsx b/web/src/components/overlay/PtzControlPanel.tsx new file mode 100644 index 000000000..132883eb6 --- /dev/null +++ b/web/src/components/overlay/PtzControlPanel.tsx @@ -0,0 +1,321 @@ +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({ + camera, + enabled, + clickOverlay, + setClickOverlay, +}: { + camera: string; + enabled: boolean; + clickOverlay: boolean; + setClickOverlay: React.Dispatch>; +}) { + const { t } = useTranslation(["views/live"]); + const { data: ptz } = useSWR( + 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 ( +
+ {ptz?.features?.includes("pt") && ( + <> + { + e.preventDefault(); + sendPtz("MOVE_LEFT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_LEFT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("MOVE_UP"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_UP"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("MOVE_DOWN"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_DOWN"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("MOVE_RIGHT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_RIGHT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + + )} + {ptz?.features?.includes("zoom") && ( + <> + { + e.preventDefault(); + sendPtz("ZOOM_IN"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("ZOOM_IN"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("ZOOM_OUT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("ZOOM_OUT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + + )} + {ptz?.features?.includes("focus") && ( + <> + { + e.preventDefault(); + sendPtz("FOCUS_IN"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("FOCUS_IN"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("FOCUS_OUT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("FOCUS_OUT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + + )} + + {ptz?.features?.includes("pt-r-fov") && ( + + + + + +

+ {clickOverlay + ? t("ptz.move.clickMove.disable") + : t("ptz.move.clickMove.enable")} +

+
+
+ )} + {(ptz?.presets?.length ?? 0) > 0 && ( + + + + + + + + +

{t("ptz.presets")}

+
+
+ + e.preventDefault()} + > + {ptz?.presets.map((preset) => ( + sendPtz(`preset_${preset}`)} + > + {preset} + + ))} + +
+ )} +
+ ); +} diff --git a/web/src/views/button/TooltipButton.tsx b/web/src/views/button/TooltipButton.tsx new file mode 100644 index 000000000..5203c9e60 --- /dev/null +++ b/web/src/views/button/TooltipButton.tsx @@ -0,0 +1,52 @@ +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ReactNode } from "react"; + +type TooltipButtonProps = { + label: string; + onClick?: () => void; + onMouseDown?: (e: React.MouseEvent) => void; + onMouseUp?: (e: React.MouseEvent) => void; + onTouchStart?: (e: React.TouchEvent) => void; + onTouchEnd?: (e: React.TouchEvent) => void; + children: ReactNode; + className?: string; +}; + +export default function TooltipButton({ + label, + onClick, + onMouseDown, + onMouseUp, + onTouchStart, + onTouchEnd, + children, + className, + ...props +}: TooltipButtonProps) { + return ( + + + + + +

{label}

+
+
+ ); +} diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index bcc18c765..fb729e389 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -17,7 +17,6 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -25,11 +24,6 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { useResizeObserver } from "@/hooks/resize-observer"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; @@ -38,10 +32,8 @@ import { LiveStreamMetadata, VideoResolutionType, } from "@/types/live"; -import { CameraPtzInfo } from "@/types/ptz"; import { RecordingStartingPoint } from "@/types/record"; import React, { - ReactNode, useCallback, useEffect, useMemo, @@ -56,12 +48,7 @@ import { isTablet, useMobileOrientation, } from "react-device-detect"; -import { BsThreeDotsVertical } from "react-icons/bs"; import { - FaAngleDown, - FaAngleLeft, - FaAngleRight, - FaAngleUp, FaCog, FaCompress, FaExpand, @@ -91,8 +78,6 @@ import { LuX, } from "react-icons/lu"; import { - MdCenterFocusStrong, - MdCenterFocusWeak, MdClosedCaption, MdClosedCaptionDisabled, MdNoPhotography, @@ -100,8 +85,6 @@ import { MdPersonOff, MdPersonSearch, MdPhotoCamera, - MdZoomIn, - MdZoomOut, } from "react-icons/md"; import { Link, useNavigate } from "react-router-dom"; import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; @@ -126,6 +109,7 @@ import { Toaster } from "@/components/ui/sonner"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { Trans, useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import PtzControlPanel from "@/components/overlay/PtzControlPanel"; type LiveCameraViewProps = { config?: FrigateConfig; @@ -680,336 +664,6 @@ export default function LiveCameraView({ ); } -type TooltipButtonProps = { - label: string; - onClick?: () => void; - onMouseDown?: (e: React.MouseEvent) => void; - onMouseUp?: (e: React.MouseEvent) => void; - onTouchStart?: (e: React.TouchEvent) => void; - onTouchEnd?: (e: React.TouchEvent) => void; - children: ReactNode; - className?: string; -}; - -function TooltipButton({ - label, - onClick, - onMouseDown, - onMouseUp, - onTouchStart, - onTouchEnd, - children, - className, - ...props -}: TooltipButtonProps) { - return ( - - - - - -

{label}

-
-
- ); -} - -function PtzControlPanel({ - camera, - enabled, - clickOverlay, - setClickOverlay, -}: { - camera: string; - enabled: boolean; - clickOverlay: boolean; - setClickOverlay: React.Dispatch>; -}) { - const { t } = useTranslation(["views/live"]); - const { data: ptz } = useSWR( - 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 ( -
- {ptz?.features?.includes("pt") && ( - <> - { - e.preventDefault(); - sendPtz("MOVE_LEFT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_LEFT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("MOVE_UP"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_UP"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("MOVE_DOWN"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_DOWN"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("MOVE_RIGHT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_RIGHT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - - )} - {ptz?.features?.includes("zoom") && ( - <> - { - e.preventDefault(); - sendPtz("ZOOM_IN"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("ZOOM_IN"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("ZOOM_OUT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("ZOOM_OUT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - - )} - {ptz?.features?.includes("focus") && ( - <> - { - e.preventDefault(); - sendPtz("FOCUS_IN"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("FOCUS_IN"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("FOCUS_OUT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("FOCUS_OUT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - - )} - - {ptz?.features?.includes("pt-r-fov") && ( - - - - - -

- {clickOverlay - ? t("ptz.move.clickMove.disable") - : t("ptz.move.clickMove.enable")} -

-
-
- )} - {(ptz?.presets?.length ?? 0) > 0 && ( - - - - - - - - -

{t("ptz.presets")}

-
-
- - e.preventDefault()} - > - {ptz?.presets.map((preset) => ( - sendPtz(`preset_${preset}`)} - > - {preset} - - ))} - -
- )} -
- ); -} - function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) { const { t } = useTranslation(["views/live", "views/events"]); const rankMap = { all: 0, motion: 1, active_objects: 2 };