Make keyboard shortcuts consistent (#20326)

* Make keyboard shortcuts consistent

* Cleanup

* Refactor prevent default to not require separate input

* Fix

* Implement escape for reviews

* Implement escape for explore

* Send content ref to get page changes for free
This commit is contained in:
Nicolas Mowen
2025-10-02 07:21:37 -06:00
committed by GitHub
parent 85ace6a6be
commit 2030809a6d
18 changed files with 231 additions and 122 deletions
@@ -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<HTMLDivElement | null>(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" ? (
<TrainGrid
model={model}
contentRef={contentRef}
classes={Object.keys(dataset || {})}
trainImages={trainImages || []}
trainFilter={trainFilter}
@@ -380,6 +396,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
/>
) : (
<DatasetGrid
contentRef={contentRef}
modelName={model.name}
categoryName={pageToggle}
images={dataset?.[pageToggle] || []}
@@ -579,6 +596,7 @@ function LibrarySelector({
}
type DatasetGridProps = {
contentRef: MutableRefObject<HTMLDivElement | null>;
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 (
<div className="flex flex-wrap gap-2 overflow-y-auto p-2">
<div
ref={contentRef}
className="scrollbar-container flex flex-wrap gap-2 overflow-y-auto p-2"
>
{classData.map((image) => (
<div
className={cn(
@@ -658,6 +680,7 @@ function DatasetGrid({
type TrainGridProps = {
model: CustomClassificationModelConfig;
contentRef: MutableRefObject<HTMLDivElement | null>;
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 (
<div
ref={contentRef}
className={cn(
"flex flex-wrap gap-2 overflow-y-auto p-2",
"scrollbar-container flex flex-wrap gap-2 overflow-y-auto p-2",
isMobile && "justify-center",
)}
>
+33 -34
View File
@@ -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 (
<>
+5 -1
View File
@@ -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
+17 -20
View File
@@ -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