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