diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index 9115e0509..cf0685caa 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -70,7 +70,10 @@ export default function ExportCard({ (editName.update?.length ?? 0) > 0 ) { submitRename(); + return true; } + + return false; }, ); diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index 09929cec5..d3f3b6d24 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -109,6 +109,7 @@ export default function ReviewCard({ useKeyboardListener(["Shift"], (_, modifiers) => { bypassDialogRef.current = modifiers.shift; + return false; }); const handleDelete = useCallback(() => { diff --git a/web/src/components/filter/ReviewActionGroup.tsx b/web/src/components/filter/ReviewActionGroup.tsx index 4db8a31c0..f91f75461 100644 --- a/web/src/components/filter/ReviewActionGroup.tsx +++ b/web/src/components/filter/ReviewActionGroup.tsx @@ -75,6 +75,7 @@ export default function ReviewActionGroup({ useKeyboardListener(["Shift"], (_, modifiers) => { setBypassDialog(modifiers.shift); + return false; }); const handleDelete = useCallback(() => { diff --git a/web/src/components/filter/SearchActionGroup.tsx b/web/src/components/filter/SearchActionGroup.tsx index ad6d6ccc8..0ba024792 100644 --- a/web/src/components/filter/SearchActionGroup.tsx +++ b/web/src/components/filter/SearchActionGroup.tsx @@ -62,6 +62,7 @@ export default function SearchActionGroup({ useKeyboardListener(["Shift"], (_, modifiers) => { setBypassDialog(modifiers.shift); + return false; }); const handleDelete = useCallback(() => { diff --git a/web/src/components/overlay/PtzControlPanel.tsx b/web/src/components/overlay/PtzControlPanel.tsx index b4219842b..5deb62fd3 100644 --- a/web/src/components/overlay/PtzControlPanel.tsx +++ b/web/src/components/overlay/PtzControlPanel.tsx @@ -83,7 +83,7 @@ export default function PtzControlPanel({ ], (key, modifiers) => { if (modifiers.repeat || !key) { - return; + return true; } if (["1", "2", "3", "4", "5", "6", "7", "8", "9"].includes(key)) { @@ -95,34 +95,36 @@ export default function PtzControlPanel({ ) { sendPtz(`preset_${ptz.presets[presetNumber - 1]}`); } - return; + return true; } if (!modifiers.down) { sendPtz("STOP"); - return; + return true; } switch (key) { case "ArrowLeft": sendPtz("MOVE_LEFT"); - break; + return true; case "ArrowRight": sendPtz("MOVE_RIGHT"); - break; + return true; case "ArrowUp": sendPtz("MOVE_UP"); - break; + return true; case "ArrowDown": sendPtz("MOVE_DOWN"); - break; + return true; case "+": sendPtz(modifiers.shift ? "FOCUS_IN" : "ZOOM_IN"); - break; + return true; case "-": sendPtz(modifiers.shift ? "FOCUS_OUT" : "ZOOM_OUT"); - break; + return true; } + + return false; }, ); diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index 63755c738..f796f03f5 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -175,6 +175,8 @@ export default function ReviewDetailDialog({ if (key == "Esc" && modifiers.down && !modifiers.repeat) { setIsOpen(false); } + + return true; }); const Overlay = isDesktop ? Sheet : MobilePage; diff --git a/web/src/components/player/GenericVideoPlayer.tsx b/web/src/components/player/GenericVideoPlayer.tsx index 4d6cb4ee5..25399771b 100644 --- a/web/src/components/player/GenericVideoPlayer.tsx +++ b/web/src/components/player/GenericVideoPlayer.tsx @@ -60,7 +60,7 @@ export function GenericVideoPlayer({ ["ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp", " ", "f", "m"], (key, modifiers) => { if (!modifiers.down || modifiers.repeat) { - return; + return true; } switch (key) { @@ -92,6 +92,8 @@ export function GenericVideoPlayer({ } break; } + + return true; }, ); diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 12e6a75d8..d3bb1aa04 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -144,7 +144,7 @@ export default function VideoControls({ const onKeyboardShortcut = useCallback( (key: string | null, modifiers: KeyModifiers) => { if (!modifiers.down) { - return; + return true; } switch (key) { @@ -174,6 +174,8 @@ export default function VideoControls({ onPlayPause(!isPlaying); break; } + + return true; }, // only update when preview only changes // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/src/hooks/use-keyboard-listener.tsx b/web/src/hooks/use-keyboard-listener.tsx index d5f68bda3..d887a2bd9 100644 --- a/web/src/hooks/use-keyboard-listener.tsx +++ b/web/src/hooks/use-keyboard-listener.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { MutableRefObject, useCallback, useEffect, useMemo } from "react"; export type KeyModifiers = { down: boolean; @@ -9,9 +9,17 @@ export type KeyModifiers = { export default function useKeyboardListener( keys: string[], - listener: (key: string | null, modifiers: KeyModifiers) => void, - preventDefault: boolean = true, + listener?: (key: string | null, modifiers: KeyModifiers) => boolean, + contentRef?: MutableRefObject, ) { + const pageKeys = useMemo( + () => + contentRef != undefined + ? ["ArrowDown", "ArrowUp", "PageDown", "PageUp"] + : [], + [contentRef], + ); + const keyDownListener = useCallback( (e: KeyboardEvent) => { // @ts-expect-error we know this field exists @@ -26,14 +34,44 @@ export default function useKeyboardListener( shift: e.shiftKey, }; - if (keys.includes(e.key)) { + if (contentRef && pageKeys.includes(e.key)) { + switch (e.key) { + case "ArrowDown": + contentRef.current?.scrollBy({ + top: 100, + behavior: "smooth", + }); + break; + case "ArrowUp": + contentRef.current?.scrollBy({ + top: -100, + behavior: "smooth", + }); + break; + case "PageDown": + contentRef.current?.scrollBy({ + top: contentRef.current.clientHeight / 2, + behavior: "smooth", + }); + break; + case "PageUp": + contentRef.current?.scrollBy({ + top: -contentRef.current.clientHeight / 2, + behavior: "smooth", + }); + break; + } + } else if (keys.includes(e.key) && listener) { + const preventDefault = listener(e.key, modifiers); if (preventDefault) e.preventDefault(); - listener(e.key, modifiers); - } else if (e.key === "Shift" || e.key === "Control" || e.key === "Meta") { + } else if ( + listener && + (e.key === "Shift" || e.key === "Control" || e.key === "Meta") + ) { listener(null, modifiers); } }, - [keys, listener, preventDefault], + [keys, pageKeys, listener, contentRef], ); const keyUpListener = useCallback( @@ -49,10 +87,13 @@ export default function useKeyboardListener( shift: false, }; - if (keys.includes(e.key)) { - e.preventDefault(); - listener(e.key, modifiers); - } else if (e.key === "Shift" || e.key === "Control" || e.key === "Meta") { + if (listener && keys.includes(e.key)) { + const preventDefault = listener(e.key, modifiers); + if (preventDefault) e.preventDefault(); + } else if ( + listener && + (e.key === "Shift" || e.key === "Control" || e.key === "Meta") + ) { listener(null, modifiers); } }, diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx index e2e834243..7e4e820d9 100644 --- a/web/src/pages/Exports.tsx +++ b/web/src/pages/Exports.tsx @@ -13,12 +13,13 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Toaster } from "@/components/ui/sonner"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { useSearchEffect } from "@/hooks/use-overlay-state"; import { cn } from "@/lib/utils"; import { DeleteClipType, Export } from "@/types/export"; import axios from "axios"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; @@ -109,6 +110,11 @@ function Exports() { [mutate, t], ); + // Keyboard Listener + + const contentRef = useRef(null); + useKeyboardListener([], undefined, contentRef); + return (
@@ -194,7 +200,10 @@ function Exports() {
{exports && filteredExports && filteredExports.length > 0 ? ( -
+
{Object.values(exports).map((item) => ( { const formData = new FormData(); @@ -263,16 +268,17 @@ export default function FaceLibrary() { // keyboard + const contentRef = useRef(null); useKeyboardListener( ["a", "Escape"], (key, modifiers) => { - if (modifiers.repeat || !modifiers.down) { - return; + if (!modifiers.down) { + return true; } switch (key) { case "a": - if (modifiers.ctrl) { + if (modifiers.ctrl && !modifiers.repeat) { if (selectedFaces.length) { setSelectedFaces([]); } else { @@ -280,14 +286,18 @@ export default function FaceLibrary() { ...(pageToggle === "train" ? trainImages : faceImages), ]); } + + return true; } break; case "Escape": setSelectedFaces([]); - break; + return true; } + + return false; }, - !inputFocused, + contentRef, ); useEffect(() => { @@ -408,15 +418,16 @@ export default function FaceLibrary() { (pageToggle == "train" ? ( ) : ( ; attemptImages: string[]; faceNames: string[]; selectedFaces: string[]; onClickFaces: (images: string[], ctrl: boolean) => void; onRefresh: () => void; - setInputFocused: React.Dispatch>; }; function TrainingGrid({ config, + contentRef, attemptImages, faceNames, selectedFaces, onClickFaces, onRefresh, - setInputFocused, }: TrainingGridProps) { const { t } = useTranslation(["views/faceLibrary"]); @@ -698,10 +709,13 @@ function TrainingGrid({ setSimilarity={undefined} setSearchPage={setDialogTab} setSearch={(search) => setSelectedEvent(search as unknown as Event)} - setInputFocused={setInputFocused} + setInputFocused={() => {}} /> -
+
{Object.entries(faceGroups).map(([key, group]) => { const event = events?.find((ev) => ev.id == key); return ( @@ -1039,6 +1053,7 @@ function FaceAttempt({ } type FaceGridProps = { + contentRef: MutableRefObject; faceImages: string[]; pageToggle: string; selectedFaces: string[]; @@ -1046,6 +1061,7 @@ type FaceGridProps = { onDelete: (name: string, ids: string[]) => void; }; function FaceGrid({ + contentRef, faceImages, pageToggle, selectedFaces, @@ -1070,6 +1086,7 @@ function FaceGrid({ return (
{ if (!modifiers.down) { - return; + return true; } switch (key) { case "f": toggleFullscreen(); - break; + return true; } + + return false; }); // document title diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index 4ee713fe5..97aa3bc5f 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -337,7 +337,7 @@ function Logs() { ["PageDown", "PageUp", "ArrowDown", "ArrowUp"], (key, modifiers) => { if (!key || !modifiers.down || !lazyLogWrapperRef.current) { - return; + return true; } const container = @@ -346,7 +346,7 @@ function Logs() { const logLineHeight = container?.querySelector(".log-line")?.clientHeight; if (!logLineHeight) { - return; + return true; } const scrollAmount = key.includes("Page") @@ -354,6 +354,7 @@ function Logs() { : logLineHeight; const direction = key.includes("Down") ? 1 : -1; container?.scrollBy({ top: scrollAmount * direction }); + return true; }, ); diff --git a/web/src/views/classification/ModelTrainingView.tsx b/web/src/views/classification/ModelTrainingView.tsx index 5d5296576..7fe241496 100644 --- a/web/src/views/classification/ModelTrainingView.tsx +++ b/web/src/views/classification/ModelTrainingView.tsx @@ -37,7 +37,14 @@ import { cn } from "@/lib/utils"; import { CustomClassificationModelConfig } from "@/types/frigateConfig"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import axios from "axios"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { isDesktop, isMobile } from "react-device-detect"; import { Trans, useTranslation } from "react-i18next"; import { LuPencil, LuTrash2 } from "react-icons/lu"; @@ -226,30 +233,38 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) { // keyboard - useKeyboardListener(["a", "Escape"], (key, modifiers) => { - if (modifiers.repeat || !modifiers.down) { - return; - } + const contentRef = useRef(null); + useKeyboardListener( + ["a", "Escape"], + (key, modifiers) => { + if (!modifiers.down) { + return true; + } - switch (key) { - case "a": - if (modifiers.ctrl) { - if (selectedImages.length) { - setSelectedImages([]); - } else { - setSelectedImages([ - ...(pageToggle === "train" - ? trainImages || [] - : dataset?.[pageToggle] || []), - ]); + switch (key) { + case "a": + if (modifiers.ctrl && !modifiers.repeat) { + if (selectedImages.length) { + setSelectedImages([]); + } else { + setSelectedImages([ + ...(pageToggle === "train" + ? trainImages || [] + : dataset?.[pageToggle] || []), + ]); + } + return true; } - } - break; - case "Escape": - setSelectedImages([]); - break; - } - }); + break; + case "Escape": + setSelectedImages([]); + return true; + } + + return false; + }, + contentRef, + ); useEffect(() => { setSelectedImages([]); @@ -370,6 +385,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) { {pageToggle == "train" ? ( ) : ( ; modelName: string; categoryName: string; images: string[]; @@ -587,6 +605,7 @@ type DatasetGridProps = { onDelete: (ids: string[]) => void; }; function DatasetGrid({ + contentRef, modelName, categoryName, images, @@ -602,7 +621,10 @@ function DatasetGrid({ ); return ( -
+
{classData.map((image) => (
; classes: string[]; trainImages: string[]; trainFilter?: TrainFilter; @@ -668,6 +691,7 @@ type TrainGridProps = { }; function TrainGrid({ model, + contentRef, classes, trainImages, trainFilter, @@ -726,8 +750,9 @@ function TrainGrid({ return (
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 4ce7fcad4..ca77b22c7 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -650,42 +650,41 @@ function DetectionReview({ // keyboard - useKeyboardListener(["a", "r", "PageDown", "PageUp"], (key, modifiers) => { - if (modifiers.repeat || !modifiers.down) { - return; - } + useKeyboardListener( + ["a", "r", "Escape"], + (key, modifiers) => { + if (!modifiers.down) { + return true; + } - switch (key) { - case "a": - if (modifiers.ctrl) { - onSelectAllReviews(); - } - break; - case "r": - if (selectedReviews.length > 0) { - currentItems?.forEach((item) => { - if (selectedReviews.includes(item.id)) { - item.has_been_reviewed = true; - markItemAsReviewed(item); - } - }); + switch (key) { + case "a": + if (modifiers.ctrl && !modifiers.repeat) { + onSelectAllReviews(); + return true; + } + break; + case "r": + if (selectedReviews.length > 0 && !modifiers.repeat) { + currentItems?.forEach((item) => { + if (selectedReviews.includes(item.id)) { + item.has_been_reviewed = true; + markItemAsReviewed(item); + } + }); + setSelectedReviews([]); + return true; + } + break; + case "Escape": setSelectedReviews([]); - } - break; - case "PageDown": - contentRef.current?.scrollBy({ - top: contentRef.current.clientHeight / 2, - behavior: "smooth", - }); - break; - case "PageUp": - contentRef.current?.scrollBy({ - top: -contentRef.current.clientHeight / 2, - behavior: "smooth", - }); - break; - } - }); + return true; + } + + return false; + }, + contentRef, + ); return ( <> diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index eef1c663a..1e427435c 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -309,21 +309,25 @@ export default function LiveCameraView({ useKeyboardListener(["m"], (key, modifiers) => { if (!modifiers.down) { - return; + return true; } switch (key) { case "m": if (supportsAudioOutput) { setAudio(!audio); + return true; } break; case "t": if (supports2WayTalk) { setMic(!mic); + return true; } break; } + + return false; }); // layout state diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 2bb726bad..5cd36beab 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -308,16 +308,24 @@ export default function SearchView({ const onKeyboardShortcut = useCallback( (key: string | null, modifiers: KeyModifiers) => { - if (!modifiers.down || !uniqueResults || inputFocused) { - return; + if (inputFocused) { + return false; + } + + if (!modifiers.down || !uniqueResults) { + return true; } switch (key) { case "a": - if (modifiers.ctrl) { + if (modifiers.ctrl && !modifiers.repeat) { onSelectAllObjects(); + return true; } break; + case "Escape": + setSelectedObjects([]); + return true; case "ArrowLeft": if (uniqueResults.length > 0) { const currentIndex = searchDetail @@ -334,8 +342,7 @@ export default function SearchView({ setSearchDetail(uniqueResults[newIndex]); } - break; - + return true; case "ArrowRight": if (uniqueResults.length > 0) { const currentIndex = searchDetail @@ -351,28 +358,18 @@ export default function SearchView({ setSearchDetail(uniqueResults[newIndex]); } - break; - case "PageDown": - contentRef.current?.scrollBy({ - top: contentRef.current.clientHeight / 2, - behavior: "smooth", - }); - break; - case "PageUp": - contentRef.current?.scrollBy({ - top: -contentRef.current.clientHeight / 2, - behavior: "smooth", - }); - break; + return true; } + + return false; }, [uniqueResults, inputFocused, onSelectAllObjects, searchDetail], ); useKeyboardListener( - ["a", "ArrowLeft", "ArrowRight", "PageDown", "PageUp"], + ["a", "Escape", "ArrowLeft", "ArrowRight"], onKeyboardShortcut, - !inputFocused, + contentRef, ); // scroll into view diff --git a/web/vite.config.ts b/web/vite.config.ts index cb1a580bf..98a9afde1 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import monacoEditorPlugin from "vite-plugin-monaco-editor"; -const proxyHost = process.env.PROXY_HOST || "localhost:5000"; +const proxyHost = process.env.PROXY_HOST || "192.168.50.106:5002"; // https://vitejs.dev/config/ export default defineConfig({