mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-17 08:35:21 +03:00
multi select in explore
This commit is contained in:
parent
96a8caa9b2
commit
fd173e998b
@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useApiHost } from "@/api";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import useSWR from "swr";
|
||||
@ -12,10 +12,12 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { SearchResult } from "@/types/search";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import usePress from "@/hooks/use-press";
|
||||
import useContextMenu from "@/hooks/use-contextmenu";
|
||||
|
||||
type SearchThumbnailProps = {
|
||||
searchResult: SearchResult;
|
||||
onClick: (searchResult: SearchResult) => void;
|
||||
onClick: (searchResult: SearchResult, ctrl: boolean) => void;
|
||||
};
|
||||
|
||||
export default function SearchThumbnail({
|
||||
@ -28,9 +30,14 @@ export default function SearchThumbnail({
|
||||
|
||||
// interactions
|
||||
|
||||
const handleOnClick = useCallback(() => {
|
||||
onClick(searchResult);
|
||||
}, [searchResult, onClick]);
|
||||
useContextMenu(imgRef, () => {
|
||||
onClick(searchResult, true);
|
||||
});
|
||||
|
||||
const bindClickAndLongPress = usePress({
|
||||
onLongPress: () => onClick(searchResult, true),
|
||||
onPress: () => onClick(searchResult, false),
|
||||
})();
|
||||
|
||||
const objectLabel = useMemo(() => {
|
||||
if (
|
||||
@ -45,7 +52,10 @@ export default function SearchThumbnail({
|
||||
}, [config, searchResult]);
|
||||
|
||||
return (
|
||||
<div className="relative size-full cursor-pointer" onClick={handleOnClick}>
|
||||
<div
|
||||
className="relative size-full cursor-pointer"
|
||||
{...bindClickAndLongPress}
|
||||
>
|
||||
<ImageLoadingIndicator
|
||||
className="absolute inset-0"
|
||||
imgLoaded={imgLoaded}
|
||||
@ -79,7 +89,7 @@ export default function SearchThumbnail({
|
||||
<div className="mx-3 pb-1 text-sm text-white">
|
||||
<Chip
|
||||
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs`}
|
||||
onClick={() => onClick(searchResult)}
|
||||
onClick={() => onClick(searchResult, false)}
|
||||
>
|
||||
{getIconForLabel(objectLabel, "size-3 text-white")}
|
||||
{Math.round(
|
||||
|
||||
@ -26,7 +26,7 @@ type ExploreViewProps = {
|
||||
searchDetail: SearchResult | undefined;
|
||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||
setSimilaritySearch: (search: SearchResult) => void;
|
||||
onSelectSearch: (item: SearchResult, page?: SearchTab) => void;
|
||||
onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
|
||||
};
|
||||
|
||||
export default function ExploreView({
|
||||
@ -125,7 +125,7 @@ type ThumbnailRowType = {
|
||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||
mutate: () => void;
|
||||
setSimilaritySearch: (search: SearchResult) => void;
|
||||
onSelectSearch: (item: SearchResult, page?: SearchTab) => void;
|
||||
onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
|
||||
};
|
||||
|
||||
function ThumbnailRow({
|
||||
@ -205,7 +205,7 @@ type ExploreThumbnailImageProps = {
|
||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||
mutate: () => void;
|
||||
setSimilaritySearch: (search: SearchResult) => void;
|
||||
onSelectSearch: (item: SearchResult, page?: SearchTab) => void;
|
||||
onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
|
||||
};
|
||||
function ExploreThumbnailImage({
|
||||
event,
|
||||
@ -225,11 +225,11 @@ function ExploreThumbnailImage({
|
||||
};
|
||||
|
||||
const handleShowObjectLifecycle = () => {
|
||||
onSelectSearch(event, "object lifecycle");
|
||||
onSelectSearch(event, false, "object lifecycle");
|
||||
};
|
||||
|
||||
const handleShowSnapshot = () => {
|
||||
onSelectSearch(event, "snapshot");
|
||||
onSelectSearch(event, false, "snapshot");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import Chip from "@/components/indicators/Chip";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import SearchActionGroup from "@/components/filter/SearchActionGroup";
|
||||
|
||||
type SearchViewProps = {
|
||||
search: string;
|
||||
@ -185,8 +186,8 @@ export default function SearchView({
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const onSelectSearch = useCallback(
|
||||
(item: SearchResult, page: SearchTab = "details") => {
|
||||
if (selectedObjects.length > 1) {
|
||||
(item: SearchResult, ctrl: boolean, page: SearchTab = "details") => {
|
||||
if (selectedObjects.length > 1 || ctrl) {
|
||||
const index = selectedObjects.indexOf(item.id);
|
||||
|
||||
if (index != -1) {
|
||||
@ -207,19 +208,37 @@ export default function SearchView({
|
||||
} else {
|
||||
setSelectedObjects([item.id]);
|
||||
}
|
||||
if (!ctrl) {
|
||||
setPage(page);
|
||||
setSearchDetail(item);
|
||||
} else {
|
||||
setSearchDetail(undefined);
|
||||
}
|
||||
},
|
||||
[selectedObjects],
|
||||
);
|
||||
|
||||
const onSelectAllObjects = useCallback(() => {
|
||||
if (!uniqueResults || uniqueResults.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedObjects.length < uniqueResults.length) {
|
||||
setSelectedObjects(uniqueResults.map((value) => value.id));
|
||||
} else {
|
||||
setSelectedObjects([]);
|
||||
}
|
||||
}, [uniqueResults, selectedObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uniqueResults && uniqueResults.length > 0) {
|
||||
setSelectedObjects([uniqueResults[0].id]);
|
||||
} else {
|
||||
setSelectedObjects([]);
|
||||
}
|
||||
}, [searchTerm, searchFilter, uniqueResults]);
|
||||
// only select first item when search term or filter changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchTerm, searchFilter]);
|
||||
|
||||
// confidence score
|
||||
|
||||
@ -267,6 +286,11 @@ export default function SearchView({
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "a":
|
||||
if (modifiers.ctrl) {
|
||||
onSelectAllObjects();
|
||||
}
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
setSelectedObjects((prevSelected) => {
|
||||
if (uniqueResults.length === 0) return prevSelected;
|
||||
@ -324,11 +348,11 @@ export default function SearchView({
|
||||
break;
|
||||
}
|
||||
},
|
||||
[uniqueResults, inputFocused],
|
||||
[uniqueResults, inputFocused, onSelectAllObjects],
|
||||
);
|
||||
|
||||
useKeyboardListener(
|
||||
["ArrowLeft", "ArrowRight", "PageDown", "PageUp"],
|
||||
["a", "ArrowLeft", "ArrowRight", "PageDown", "PageUp"],
|
||||
onKeyboardShortcut,
|
||||
!inputFocused,
|
||||
);
|
||||
@ -336,7 +360,7 @@ export default function SearchView({
|
||||
// scroll into view
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedObjects.length > 0 && uniqueResults && itemRefs.current) {
|
||||
if (selectedObjects.length == 1 && uniqueResults && itemRefs.current) {
|
||||
const selectedIndex = uniqueResults.findIndex(
|
||||
(result) => result.id === selectedObjects[0],
|
||||
);
|
||||
@ -420,6 +444,8 @@ export default function SearchView({
|
||||
{hasExistingSearch && (
|
||||
<ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]">
|
||||
<div className="flex flex-row gap-2">
|
||||
{selectedObjects.length <= 1 ? (
|
||||
<>
|
||||
<SearchFilterGroup
|
||||
className={cn(
|
||||
"w-full justify-between md:justify-start lg:justify-end",
|
||||
@ -436,6 +462,21 @@ export default function SearchView({
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
/>
|
||||
<ScrollBar orientation="horizontal" className="h-0" />
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"scrollbar-container flex justify-center gap-2 overflow-x-auto",
|
||||
"h-10 w-full justify-between md:justify-start lg:justify-end",
|
||||
)}
|
||||
>
|
||||
<SearchActionGroup
|
||||
selectedObjects={selectedObjects}
|
||||
setSelectedObjects={setSelectedObjects}
|
||||
pullLatestData={refresh}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
@ -479,7 +520,9 @@ export default function SearchView({
|
||||
>
|
||||
<SearchThumbnail
|
||||
searchResult={value}
|
||||
onClick={() => onSelectSearch(value)}
|
||||
onClick={(value: SearchResult, ctrl: boolean) => {
|
||||
onSelectSearch(value, ctrl);
|
||||
}}
|
||||
/>
|
||||
{(searchTerm ||
|
||||
searchFilter?.search_type?.includes("similarity")) && (
|
||||
@ -520,9 +563,11 @@ export default function SearchView({
|
||||
}}
|
||||
refreshResults={refresh}
|
||||
showObjectLifecycle={() =>
|
||||
onSelectSearch(value, "object lifecycle")
|
||||
onSelectSearch(value, false, "object lifecycle")
|
||||
}
|
||||
showSnapshot={() =>
|
||||
onSelectSearch(value, false, "snapshot")
|
||||
}
|
||||
showSnapshot={() => onSelectSearch(value, "snapshot")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user