diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index b48f602c8e..2467cf6009 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -47,7 +47,6 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte
import { addModelSelectedListener } from './listeners/modelSelected';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
import { addReceivedPageOfImagesListener } from './listeners/receivedPageOfImages';
-import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch';
import {
addSessionCanceledFulfilledListener,
addSessionCanceledPendingListener,
@@ -201,7 +200,7 @@ addBoardIdSelectedListener();
addReceivedOpenAPISchemaListener();
// Batches
-addSelectionAddedToBatchListener();
+// addSelectionAddedToBatchListener();
addAddBoardToBatchListener();
// DND
diff --git a/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx
new file mode 100644
index 0000000000..2136acc3c3
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx
@@ -0,0 +1,42 @@
+import { Box, Flex, Icon } from '@chakra-ui/react';
+import { FaExclamation } from 'react-icons/fa';
+
+const IAIErrorLoadingImageFallback = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default IAIErrorLoadingImageFallback;
diff --git a/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx
new file mode 100644
index 0000000000..a3c83cb734
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx
@@ -0,0 +1,30 @@
+import { Box, Skeleton } from '@chakra-ui/react';
+
+const IAIFillSkeleton = () => {
+ return (
+
+
+
+ );
+};
+
+export default IAIFillSkeleton;
diff --git a/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx b/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx
index 14d6e83efc..833e0b12d2 100644
--- a/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx
+++ b/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx
@@ -1,10 +1,12 @@
-import { Box, Spinner } from '@chakra-ui/react';
+import { Box } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage';
+import IAIErrorLoadingImageFallback from 'common/components/IAIErrorLoadingImageFallback';
+import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
import {
batchImageRangeEndSelected,
batchImageSelected,
@@ -32,11 +34,13 @@ type BatchImageProps = {
const BatchImage = (props: BatchImageProps) => {
const dispatch = useAppDispatch();
-
const { imageName } = props;
-
- const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
-
+ const {
+ currentData: imageDTO,
+ isLoading,
+ isError,
+ isSuccess,
+ } = useGetImageDTOQuery(imageName);
const selector = useMemo(() => makeSelector(imageName), [imageName]);
const { isSelected, selectionCount, selection } = useAppSelector(selector);
@@ -76,8 +80,12 @@ const BatchImage = (props: BatchImageProps) => {
}
}, [imageDTO, selection, selectionCount]);
- if (!imageDTO) {
- return ;
+ if (isLoading) {
+ return ;
+ }
+
+ if (isError || !imageDTO) {
+ return ;
}
return (
@@ -108,6 +116,7 @@ const BatchImage = (props: BatchImageProps) => {
isUploadDisabled={true}
resetTooltip="Remove from batch"
withResetIcon
+ thumbnail
/>
)}
diff --git a/invokeai/frontend/web/src/features/gallery/components/BatchGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/BatchGrid.tsx
index 350f4c18c1..7a733221ee 100644
--- a/invokeai/frontend/web/src/features/gallery/components/BatchGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/BatchGrid.tsx
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppSelector } from 'app/store/storeHooks';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { memo, useEffect, useRef, useState } from 'react';
@@ -26,7 +26,6 @@ const selector = createSelector(
);
const BatchGrid = () => {
- const dispatch = useAppDispatch();
const { t } = useTranslation();
const rootRef = useRef(null);
const [scroller, setScroller] = useState(null);
diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardGrid.tsx
new file mode 100644
index 0000000000..7e9f0a56a1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/BoardGrid.tsx
@@ -0,0 +1,102 @@
+import { Box, Spinner } from '@chakra-ui/react';
+import { useOverlayScrollbars } from 'overlayscrollbars-react';
+
+import { memo, useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaExclamation, FaImage } from 'react-icons/fa';
+
+import { createSelector } from '@reduxjs/toolkit';
+import { stateSelector } from 'app/store/store';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import BatchImage from 'features/batch/components/BatchImage';
+import { VirtuosoGrid } from 'react-virtuoso';
+import { useGetAllBoardImagesForBoardQuery } from 'services/api/endpoints/boardImages';
+import ItemContainer from './ItemContainer';
+import ListContainer from './ListContainer';
+
+const selector = createSelector(
+ [stateSelector],
+ (state) => {
+ return {
+ imageNames: state.batch.imageNames,
+ };
+ },
+ defaultSelectorOptions
+);
+
+type BoardGridProps = {
+ board_id: string;
+};
+
+const BoardGrid = (props: BoardGridProps) => {
+ const { board_id } = props;
+ const { data, isLoading, isError, isSuccess } =
+ useGetAllBoardImagesForBoardQuery({
+ board_id,
+ });
+ const { t } = useTranslation();
+ const rootRef = useRef(null);
+ const [scroller, setScroller] = useState(null);
+ const [initialize, osInstance] = useOverlayScrollbars({
+ defer: true,
+ options: {
+ scrollbars: {
+ visibility: 'auto',
+ autoHide: 'leave',
+ autoHideDelay: 1300,
+ theme: 'os-theme-dark',
+ },
+ overflow: { x: 'hidden' },
+ },
+ });
+
+ useEffect(() => {
+ const { current: root } = rootRef;
+ if (scroller && root) {
+ initialize({
+ target: root,
+ elements: {
+ viewport: scroller,
+ },
+ });
+ }
+ return () => osInstance()?.destroy();
+ }, [scroller, initialize, osInstance]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (isError) {
+ return ;
+ }
+
+ if (isSuccess && data.image_names) {
+ return (
+
+ (
+
+ )}
+ />
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default memo(BoardGrid);
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx
index cba52cfba3..0e7ae6cd79 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx
@@ -1,4 +1,4 @@
-import { Box } from '@chakra-ui/react';
+import { Box, Spinner } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store';
@@ -7,8 +7,12 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage';
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
-import { ImageDTO } from 'services/api/types';
-import { imageSelected } from '../store/gallerySlice';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import {
+ imageRangeEndSelected,
+ imageSelected,
+ imageSelectionToggled,
+} from '../store/gallerySlice';
import ImageContextMenu from './ImageContextMenu';
export const makeSelector = (image_name: string) =>
@@ -23,33 +27,29 @@ export const makeSelector = (image_name: string) =>
);
interface HoverableImageProps {
- imageDTO: ImageDTO;
+ imageName: string;
}
const GalleryImage = (props: HoverableImageProps) => {
const dispatch = useAppDispatch();
-
- const { imageDTO } = props;
- const { image_name } = imageDTO;
-
- const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
+ const { imageName } = props;
+ const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
+ const localSelector = useMemo(() => makeSelector(imageName), [imageName]);
const { isSelected, selectionCount, selection } =
useAppSelector(localSelector);
const handleClick = useCallback(
(e: MouseEvent) => {
- // multiselect disabled for now
- // if (e.shiftKey) {
- // dispatch(imageRangeEndSelected(props.imageDTO.image_name));
- // } else if (e.ctrlKey || e.metaKey) {
- // dispatch(imageSelectionToggled(props.imageDTO.image_name));
- // } else {
- // dispatch(imageSelected(props.imageDTO.image_name));
- // }
- dispatch(imageSelected(props.imageDTO.image_name));
+ if (e.shiftKey) {
+ dispatch(imageRangeEndSelected(imageName));
+ } else if (e.ctrlKey || e.metaKey) {
+ dispatch(imageSelectionToggled(imageName));
+ } else {
+ dispatch(imageSelected(imageName));
+ }
},
- [dispatch, props.imageDTO.image_name]
+ [dispatch, imageName]
);
const handleDelete = useCallback(
@@ -81,13 +81,17 @@ const GalleryImage = (props: HoverableImageProps) => {
}
}, [imageDTO, selection, selectionCount]);
+ if (!imageDTO) {
+ return ;
+ }
+
return (
{(ref) => (
{
}, [imageDTO.image_url]);
const handleAddSelectionToBatch = useCallback(() => {
- dispatch(selectionAddedToBatch({ images_names: selection }));
+ dispatch(imagesAddedToBatch(selection));
}, [dispatch, selection]);
const handleAddToBatch = useCallback(() => {
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index 8168b397fb..cdde89379e 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -38,6 +38,7 @@ import {
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { mode } from 'theme/util/mode';
import BatchGrid from './BatchGrid';
+import BoardGrid from './BoardGrid';
import BoardsList from './Boards/BoardsList';
import ImageGalleryGrid from './ImageGalleryGrid';
@@ -228,7 +229,13 @@ const ImageGalleryContent = () => {
- {selectedBoardId === 'batch' ? : }
+ {selectedBoardId === 'batch' ? (
+
+ ) : selectedBoardId ? (
+
+ ) : (
+
+ )}
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx
index b6240f78b9..aeae5cd4e3 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx
@@ -160,7 +160,7 @@ const ImageGalleryGrid = () => {
) : (
)
}