diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 601a28f9c..005b4afdb 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -142,29 +142,33 @@ export default function FaceLibrary() { const [selectedFaces, setSelectedFaces] = useState([]); - const onClickFace = useCallback( - (imageId: string, ctrl: boolean) => { + const onClickFaces = useCallback( + (images: string[], ctrl: boolean) => { if (selectedFaces.length == 0 && !ctrl) { return; } - const index = selectedFaces.indexOf(imageId); + let newSelectedFaces = [...selectedFaces]; - if (index != -1) { - if (selectedFaces.length == 1) { - setSelectedFaces([]); + images.forEach((imageId) => { + const index = newSelectedFaces.indexOf(imageId); + + if (index != -1) { + if (selectedFaces.length == 1) { + newSelectedFaces = []; + } else { + const copy = [ + ...newSelectedFaces.slice(0, index), + ...newSelectedFaces.slice(index + 1), + ]; + newSelectedFaces = copy; + } } else { - const copy = [ - ...selectedFaces.slice(0, index), - ...selectedFaces.slice(index + 1), - ]; - setSelectedFaces(copy); + newSelectedFaces.push(imageId); } - } else { - const copy = [...selectedFaces]; - copy.push(imageId); - setSelectedFaces(copy); - } + }); + + setSelectedFaces(newSelectedFaces); }, [selectedFaces, setSelectedFaces], ); @@ -283,7 +287,7 @@ export default function FaceLibrary() { attemptImages={trainImages} faceNames={faces} selectedFaces={selectedFaces} - onClickFace={onClickFace} + onClickFaces={onClickFaces} onRefresh={refreshFaces} /> ) : ( @@ -391,7 +395,7 @@ type TrainingGridProps = { attemptImages: string[]; faceNames: string[]; selectedFaces: string[]; - onClickFace: (image: string, ctrl: boolean) => void; + onClickFaces: (images: string[], ctrl: boolean) => void; onRefresh: () => void; }; function TrainingGrid({ @@ -399,7 +403,7 @@ function TrainingGrid({ attemptImages, faceNames, selectedFaces, - onClickFace, + onClickFaces, onRefresh, }: TrainingGridProps) { const { t } = useTranslation(["views/faceLibrary"]); @@ -523,7 +527,7 @@ function TrainingGrid({ event={event} faceNames={faceNames} selectedFaces={selectedFaces} - onClickFace={onClickFace} + onClickFaces={onClickFaces} onSelectEvent={setSelectedEvent} onRefresh={onRefresh} /> @@ -540,7 +544,7 @@ type FaceAttemptGroupProps = { event?: Event; faceNames: string[]; selectedFaces: string[]; - onClickFace: (image: string, ctrl: boolean) => void; + onClickFaces: (image: string[], ctrl: boolean) => void; onSelectEvent: (event: Event) => void; onRefresh: () => void; }; @@ -550,7 +554,7 @@ function FaceAttemptGroup({ event, faceNames, selectedFaces, - onClickFace, + onClickFaces, onSelectEvent, onRefresh, }: FaceAttemptGroupProps) { @@ -564,15 +568,48 @@ function FaceAttemptGroup({ [group, selectedFaces], ); + // interaction + + const handleClickEvent = useCallback( + (meta: boolean) => { + if (event && selectedFaces.length == 0 && !meta) { + onSelectEvent(event); + } else { + const anySelected = + group.find((face) => selectedFaces.includes(face.filename)) != + undefined; + + if (anySelected) { + // deselect all + const toDeselect: string[] = []; + group.forEach((face) => { + if (selectedFaces.includes(face.filename)) { + toDeselect.push(face.filename); + } + }); + onClickFaces(toDeselect, false); + } else { + // select all + onClickFaces( + group.map((face) => face.filename), + true, + ); + } + } + }, + [event, group, selectedFaces, onClickFaces, onSelectEvent], + ); + return (
handleClickEvent(e.metaKey)} >
@@ -623,7 +660,7 @@ function FaceAttemptGroup({ } onClick={(data, meta) => { if (meta || selectedFaces.length > 0) { - onClickFace(data.filename, true); + onClickFaces([data.filename], true); } else if (event) { onSelectEvent(event); } @@ -749,7 +786,10 @@ function FaceAttempt({ ref={imgRef} className={cn("size-44", isMobile && "w-full")} src={`${baseUrl}clips/faces/train/${data.filename}`} - onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)} + onClick={(e) => { + e.stopPropagation(); + onClick(data, e.metaKey || e.ctrlKey); + }} />