diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index bd9afcd417..2354423210 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -12,6 +12,8 @@ import BoardsList from './Boards/BoardsList/BoardsList';
import GalleryBoardName from './GalleryBoardName';
import GallerySettingsPopover from './GallerySettingsPopover';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
+import { GalleryImageGridContainer } from './ImageGrid/GalleryImageGridContainer';
+import { GalleryPagination } from './ImageGrid/GalleryPagination';
const ImageGalleryContent = () => {
const { t } = useTranslation();
@@ -31,7 +33,7 @@ const ImageGalleryContent = () => {
}, [dispatch]);
return (
-
+
{galleryHeader}
@@ -73,9 +75,10 @@ const ImageGalleryContent = () => {
-
+
-
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
index 7d6f6cf4e2..1e5674dd43 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
@@ -7,7 +7,6 @@ import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelecto
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImageBold, PiWarningCircleBold } from 'react-icons/pi';
-import type { ImageDTO } from 'services/api/types';
import GalleryImage from './GalleryImage';
import { useListImagesQuery } from '../../../../services/api/endpoints/images';
@@ -19,7 +18,8 @@ export const imageItemContainerTestId = 'image-item-container';
const GalleryImageGrid = () => {
useGalleryHotkeys();
const { t } = useTranslation();
- const galleryImageMinimumWidth = useAppSelector((s) => s.gallery.galleryImageMinimumWidth);
+
+ const { galleryImageMinimumWidth, limit } = useAppSelector((s) => s.gallery);
const queryArgs = useAppSelector(selectListImagesQueryArgs);
const { imageDTOs, isLoading, isSuccess, isError } = useListImagesQuery(queryArgs, {
selectFromResult: ({ data, isLoading, isSuccess, isError }) => ({
@@ -38,7 +38,7 @@ const GalleryImageGrid = () => {
);
}
- if (isLoading) {
+ if (isLoading || !limit) {
return (
@@ -67,19 +67,9 @@ const GalleryImageGrid = () => {
))}
-
+ {/* */}
>
);
};
export default memo(GalleryImageGrid);
-
-const GalleryImageContainer = memo(({ imageDTO, index }: { imageDTO: ImageDTO; index: number }) => {
- return (
-
-
-
- );
-});
-
-GalleryImageContainer.displayName = 'GalleryImageContainer';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGridContainer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGridContainer.tsx
new file mode 100644
index 0000000000..7b9670b8e0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGridContainer.tsx
@@ -0,0 +1,58 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Box, Flex } from '@invoke-ai/ui-library';
+import GalleryImageGrid from './GalleryImageGrid';
+import { useAppSelector, useAppDispatch } from '../../../../app/store/storeHooks';
+import { limitChanged } from '../../store/gallerySlice';
+
+export const GalleryImageGridContainer = () => {
+ const { galleryImageMinimumWidth, limit } = useAppSelector((s) => s.gallery);
+ const dispatch = useAppDispatch();
+ const containerRef = useRef(null);
+
+ const calculateItemsPerPage = useCallback(() => {
+ const containerWidth = containerRef.current?.clientWidth;
+ const containerHeight = containerRef.current?.clientHeight;
+ console.log({ containerWidth, containerHeight, galleryImageMinimumWidth });
+ if (containerHeight && containerWidth) {
+ const numberHorizontal = Math.floor(containerWidth / galleryImageMinimumWidth);
+ const imageWidth = containerWidth / numberHorizontal;
+ const numberAllowedVertical = Math.floor(containerHeight / imageWidth);
+ console.log({ numberAllowedVertical, numberHorizontal });
+ dispatch(limitChanged(numberAllowedVertical * numberHorizontal));
+ }
+ }, [containerRef, galleryImageMinimumWidth]);
+
+ useEffect(() => {
+ dispatch(limitChanged(undefined));
+ calculateItemsPerPage();
+ }, [galleryImageMinimumWidth]);
+
+ useEffect(() => {
+ if (!containerRef.current) {
+ return;
+ }
+
+ const resizeObserver = new ResizeObserver(() => {
+ console.log('resize');
+ if (!containerRef.current) {
+ return;
+ }
+ dispatch(limitChanged(undefined));
+ calculateItemsPerPage();
+ });
+
+ resizeObserver.observe(containerRef.current);
+ dispatch(limitChanged(undefined));
+ calculateItemsPerPage();
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, []);
+
+ return (
+
+ {limit && }
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx
index 56a0d5a60a..cfac6a728a 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx
@@ -3,12 +3,15 @@ import { useGalleryPagination } from '../../hooks/useGalleryPagination';
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
export const GalleryPagination = () => {
- const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, rangeDisplay } =
+ const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, rangeDisplay, total } =
useGalleryPagination();
- console.log({ currentPage, pageButtons });
+
+ if (!total) {
+ return ;
+ }
return (
-
+
{
const dispatch = useAppDispatch();
- const { offset } = useAppSelector((s) => s.gallery);
+ const { offset, limit } = useAppSelector((s) => s.gallery);
const queryArgs = useAppSelector(selectListImagesQueryArgs);
const { count, total } = useListImagesQuery(queryArgs, {
selectFromResult: ({ data }) => ({ count: data?.items.length ?? 0, total: data?.total ?? 0 }),
});
- const currentPage = useMemo(() => Math.ceil(offset / IMAGE_LIMIT), [offset]);
- const pages = useMemo(() => Math.ceil(total / IMAGE_LIMIT), [total]);
+ const currentPage = useMemo(() => Math.ceil(offset / (limit || 0)), [offset, limit]);
+ const pages = useMemo(() => Math.ceil(total / (limit || 0)), [total, limit]);
const isNextEnabled = useMemo(() => {
if (!count) {
@@ -32,26 +31,26 @@ export const useGalleryPagination = () => {
}, [count, offset]);
const goNext = useCallback(() => {
- dispatch(offsetChanged(offset + IMAGE_LIMIT));
- }, [dispatch, offset]);
+ dispatch(offsetChanged(offset + (limit || 0)));
+ }, [dispatch, offset, limit]);
const goPrev = useCallback(() => {
- dispatch(offsetChanged(Math.max(offset - IMAGE_LIMIT, 0)));
- }, [dispatch, offset]);
+ dispatch(offsetChanged(Math.max(offset - (limit || 0), 0)));
+ }, [dispatch, offset, limit]);
const goToPage = useCallback(
(page: number) => {
const p = Math.max(0, Math.min(page, pages - 1));
- dispatch(offsetChanged(page * IMAGE_LIMIT));
+ dispatch(offsetChanged(page * (limit || 0)));
},
- [dispatch, pages]
+ [dispatch, pages, limit]
);
const goToFirst = useCallback(() => {
dispatch(offsetChanged(0));
}, [dispatch]);
const goToLast = useCallback(() => {
- dispatch(offsetChanged(pages * IMAGE_LIMIT));
- }, [dispatch, pages]);
+ dispatch(offsetChanged(pages * (limit || 0)));
+ }, [dispatch, pages, limit]);
// calculate the page buttons to display - current page with 3 around it
const pageButtons = useMemo(() => {
@@ -89,10 +88,10 @@ export const useGalleryPagination = () => {
const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]);
const rangeDisplay = useMemo(() => {
- const startItem = currentPage * IMAGE_LIMIT + 1;
- const endItem = Math.min((currentPage + 1) * IMAGE_LIMIT, total);
+ const startItem = currentPage * (limit || 0) + 1;
+ const endItem = Math.min((currentPage + 1) * (limit || 0), total);
return `${startItem}-${endItem} of ${total}`;
- }, [total, currentPage]);
+ }, [total, currentPage, limit]);
const api = useMemo(
() => ({
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
index 1c64fc7809..bd9eb33e1c 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
@@ -1,3 +1,4 @@
+import { SkipToken, skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
@@ -10,11 +11,11 @@ export const selectLastSelectedImage = createMemoizedSelector(
export const selectListImagesQueryArgs = createMemoizedSelector(
selectGallerySlice,
- (gallery): ListImagesArgs => ({
+ (gallery): ListImagesArgs | SkipToken => (gallery.limit ? {
board_id: gallery.selectedBoardId,
categories: gallery.galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
offset: gallery.offset,
limit: gallery.limit,
is_intermediate: false,
- })
+ } : skipToken)
);
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index 573cc7ab02..7ed948cbb0 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -19,7 +19,7 @@ const initialGalleryState: GalleryState = {
selectedBoardId: 'none',
galleryView: 'images',
boardSearchText: '',
- limit: IMAGE_LIMIT,
+ limit: undefined,
offset: 0,
isImageViewerOpen: true,
imageToCompare: null,
@@ -72,7 +72,6 @@ export const gallerySlice = createSlice({
state.selectedBoardId = action.payload.boardId;
state.galleryView = 'images';
state.offset = 0;
- state.limit = IMAGE_LIMIT;
},
autoAddBoardIdChanged: (state, action: PayloadAction) => {
if (!action.payload) {
@@ -108,6 +107,9 @@ export const gallerySlice = createSlice({
offsetChanged: (state, action: PayloadAction) => {
state.offset = action.payload;
},
+ limitChanged: (state, action: PayloadAction) => {
+ state.limit = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
@@ -150,7 +152,8 @@ export const {
comparedImagesSwapped,
comparisonFitChanged,
comparisonModeCycled,
- offsetChanged
+ offsetChanged,
+ limitChanged
} = gallerySlice.actions;
const isAnyBoardDeleted = isAnyOf(
diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts
index c929ff992e..9a4bc58db4 100644
--- a/invokeai/frontend/web/src/features/gallery/store/types.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/types.ts
@@ -2,7 +2,7 @@ import type { ImageCategory, ImageDTO } from 'services/api/types';
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
export const ASSETS_CATEGORIES: ImageCategory[] = ['control', 'mask', 'user', 'other'];
-export const IMAGE_LIMIT = 20;
+export const IMAGE_LIMIT = 15;
export type GalleryView = 'images' | 'assets';
export type BoardId = 'none' | (string & Record);
@@ -19,7 +19,7 @@ export type GalleryState = {
galleryView: GalleryView;
boardSearchText: string;
offset: number;
- limit: number;
+ limit: number | undefined;
alwaysShowImageSizeBadge: boolean;
imageToCompare: ImageDTO | null;
comparisonMode: ComparisonMode;