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 a00e425016..7d6f6cf4e2 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
@@ -1,24 +1,17 @@
-import { Box, Flex, Grid, IconButton } from '@invoke-ai/ui-library';
+import { Box, Flex, Grid } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
-import { useGalleryPagination } from 'features/gallery/hooks/useGalleryImages';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import {
- PiCaretDoubleLeftBold,
- PiCaretDoubleRightBold,
- PiCaretLeftBold,
- PiCaretRightBold,
- PiImageBold,
- PiWarningCircleBold,
-} from 'react-icons/pi';
+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';
+import { GalleryPagination } from './GalleryPagination';
export const imageListContainerTestId = 'image-list-container';
export const imageItemContainerTestId = 'image-item-container';
@@ -90,18 +83,3 @@ const GalleryImageContainer = memo(({ imageDTO, index }: { imageDTO: ImageDTO; i
});
GalleryImageContainer.displayName = 'GalleryImageContainer';
-
-const GalleryPagination = memo(() => {
- const { first, prev, next, last, isFirstEnabled, isPrevEnabled, isNextEnabled, isLastEnabled } =
- useGalleryPagination();
- return (
-
- } onClick={first} isDisabled={!isFirstEnabled} />
- } onClick={prev} isDisabled={!isPrevEnabled} />
- } onClick={next} isDisabled={!isNextEnabled} />
- } onClick={last} isDisabled={!isLastEnabled} />
-
- );
-});
-
-GalleryPagination.displayName = 'GalleryPagination';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx
new file mode 100644
index 0000000000..56a0d5a60a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx
@@ -0,0 +1,45 @@
+import { Button, Flex, IconButton, Text } from '@invoke-ai/ui-library';
+import { useGalleryPagination } from '../../hooks/useGalleryPagination';
+import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
+
+export const GalleryPagination = () => {
+ const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, rangeDisplay } =
+ useGalleryPagination();
+ console.log({ currentPage, pageButtons });
+
+ return (
+
+
+ }
+ onClick={goPrev}
+ isDisabled={!isPrevEnabled}
+ />
+ {pageButtons.map((page) =>
+ typeof page === 'number' ? (
+
+ ) : (
+ ...
+ )
+ )}
+ }
+ onClick={goNext}
+ isDisabled={!isNextEnabled}
+ />
+
+ {rangeDisplay} Images
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx
index edeaf4e8cb..726292fd27 100644
--- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx
@@ -1,10 +1,11 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, IconButton, Spinner } from '@invoke-ai/ui-library';
-import { useGalleryImages, useGalleryPagination } from 'features/gallery/hooks/useGalleryImages';
+import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDoubleRightBold, PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
+import { useGalleryPagination } from '../hooks/useGalleryPagination';
const nextPrevButtonStyles: ChakraProps['sx'] = {
color: 'base.100',
@@ -17,7 +18,7 @@ const NextPrevImageButtons = () => {
const { prevImage, nextImage, isOnFirstImage, isOnLastImage } = useGalleryNavigation();
const { isFetching } = useGalleryImages().queryResult;
- const { isNextEnabled, next } = useGalleryPagination();
+ const { isNextEnabled, goNext } = useGalleryPagination();
return (
@@ -49,7 +50,7 @@ const NextPrevImageButtons = () => {
aria-label={t('accessibility.loadMore')}
icon={}
variant="unstyled"
- onClick={next}
+ onClick={goNext}
boxSize={16}
sx={nextPrevButtonStyles}
/>
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
index 221a85c397..527d465de5 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
@@ -1,6 +1,6 @@
import { useAppSelector } from 'app/store/storeHooks';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
-import { useGalleryPagination } from 'features/gallery/hooks/useGalleryImages';
+import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useMemo } from 'react';
@@ -19,7 +19,7 @@ export const useGalleryHotkeys = () => {
return activeTabName !== 'canvas' || !isStaging;
}, [activeTabName, isStaging]);
- const { next, prev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
+ const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
const queryArgs = useAppSelector(selectListImagesQueryArgs);
const queryResult = useListImagesQuery(queryArgs);
@@ -37,12 +37,12 @@ export const useGalleryHotkeys = () => {
['left', 'alt+left'],
(e) => {
if (isOnFirstImageOfView && isPrevEnabled && !queryResult.isFetching) {
- prev();
+ goPrev();
return;
}
canNavigateGallery && handleLeftImage(e.altKey);
},
- [handleLeftImage, canNavigateGallery, isOnFirstImageOfView]
+ [handleLeftImage, canNavigateGallery, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching]
);
useHotkeys(
@@ -52,14 +52,14 @@ export const useGalleryHotkeys = () => {
return;
}
if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) {
- next();
+ goNext();
return;
}
if (!isOnLastImageOfView) {
handleRightImage(e.altKey);
}
},
- [isOnLastImageOfView, next, isNextEnabled, queryResult.isFetching, handleRightImage, canNavigateGallery]
+ [isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage, canNavigateGallery]
);
useHotkeys(
@@ -75,12 +75,12 @@ export const useGalleryHotkeys = () => {
['down', 'alt+down'],
(e) => {
if (!areImagesBelowCurrent && isNextEnabled && !queryResult.isFetching) {
- next();
+ goNext();
return;
}
handleDownImage(e.altKey);
},
{ preventDefault: true },
- [areImagesBelowCurrent, next, isNextEnabled, queryResult.isFetching, handleDownImage]
+ [areImagesBelowCurrent, goNext, isNextEnabled, queryResult.isFetching, handleDownImage]
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts
index 98b8b601db..9d8eb621c0 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts
@@ -1,13 +1,9 @@
import { EMPTY_ARRAY } from 'app/store/constants';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppSelector } from 'app/store/storeHooks';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
-import { offsetChanged } from 'features/gallery/store/gallerySlice';
-import { useCallback, useMemo } from 'react';
-import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
+import { useMemo } from 'react';
import { useListImagesQuery } from 'services/api/endpoints/images';
-const LIMIT = 20;
-
export const useGalleryImages = () => {
const queryArgs = useAppSelector(selectListImagesQueryArgs);
const queryResult = useListImagesQuery(queryArgs);
@@ -18,103 +14,3 @@ export const useGalleryImages = () => {
};
};
-export const useGalleryPagination = () => {
- const dispatch = useAppDispatch();
- const offset = useAppSelector((s) => s.gallery.offset);
- const galleryView = useAppSelector((s) => s.gallery.galleryView);
- const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
- const queryArgs = useAppSelector(selectListImagesQueryArgs);
- const { count } = useListImagesQuery(queryArgs, {
- selectFromResult: ({ data }) => ({ count: data?.items.length ?? 0 }),
- });
- const { data: assetsTotal } = useGetBoardAssetsTotalQuery(selectedBoardId);
- const { data: imagesTotal } = useGetBoardImagesTotalQuery(selectedBoardId);
- const total = useMemo(() => {
- if (galleryView === 'images') {
- return imagesTotal?.total ?? 0;
- } else {
- return assetsTotal?.total ?? 0;
- }
- }, [assetsTotal?.total, galleryView, imagesTotal?.total]);
- const page = useMemo(() => Math.floor(offset / LIMIT), [offset]);
- const pages = useMemo(() => Math.floor(total / LIMIT), [total]);
- const isNextEnabled = useMemo(() => {
- if (!count) {
- return false;
- }
- return page < pages;
- }, [count, page, pages]);
- const isPrevEnabled = useMemo(() => {
- if (!count) {
- return false;
- }
- return offset > 0;
- }, [count, offset]);
- const next = useCallback(() => {
- dispatch(offsetChanged(offset + LIMIT));
- }, [dispatch, offset]);
- const prev = useCallback(() => {
- dispatch(offsetChanged(Math.max(offset - LIMIT, 0)));
- }, [dispatch, offset]);
- const goToPage = useCallback(
- (page: number) => {
- const p = Math.max(0, Math.min(page, pages - 1));
- dispatch(offsetChanged(p));
- },
- [dispatch, pages]
- );
- const first = useCallback(() => {
- dispatch(offsetChanged(0));
- }, [dispatch]);
- const last = useCallback(() => {
- dispatch(offsetChanged(pages * LIMIT));
- }, [dispatch, pages]);
- // calculate the page buttons to display - current page with 3 around it
- const pageButtons = useMemo(() => {
- const buttons = [];
- const start = Math.max(0, page - 3);
- const end = Math.min(pages, start + 6);
- for (let i = start; i < end; i++) {
- buttons.push(i);
- }
- return buttons;
- }, [page, pages]);
- const isFirstEnabled = useMemo(() => page > 0, [page]);
- const isLastEnabled = useMemo(() => page < pages - 1, [page, pages]);
-
- const api = useMemo(
- () => ({
- count,
- total,
- page,
- pages,
- isNextEnabled,
- isPrevEnabled,
- next,
- prev,
- goToPage,
- first,
- last,
- pageButtons,
- isFirstEnabled,
- isLastEnabled,
- }),
- [
- count,
- total,
- page,
- pages,
- isNextEnabled,
- isPrevEnabled,
- next,
- prev,
- goToPage,
- first,
- last,
- pageButtons,
- isFirstEnabled,
- isLastEnabled,
- ]
- );
- return api;
-};
\ No newline at end of file
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts
new file mode 100644
index 0000000000..1bff323591
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts
@@ -0,0 +1,144 @@
+import { useMemo, useCallback } from "react";
+import { useAppDispatch, useAppSelector } from "../../../app/store/storeHooks";
+import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from "../../../services/api/endpoints/boards";
+import { useListImagesQuery } from "../../../services/api/endpoints/images";
+import { selectListImagesQueryArgs } from "../store/gallerySelectors";
+import { offsetChanged } from "../store/gallerySlice";
+import { IMAGE_LIMIT } from "../store/types";
+
+export const useGalleryPagination = () => {
+ const dispatch = useAppDispatch();
+ const { offset, galleryView, selectedBoardId } = useAppSelector((s) => s.gallery);
+ const queryArgs = useAppSelector(selectListImagesQueryArgs);
+
+ const { count } = useListImagesQuery(queryArgs, {
+ selectFromResult: ({ data }) => ({ count: data?.items.length ?? 0 }),
+ });
+ const { data: assetsTotal } = useGetBoardAssetsTotalQuery(selectedBoardId);
+ const { data: imagesTotal } = useGetBoardImagesTotalQuery(selectedBoardId);
+
+ const total = useMemo(() => {
+ if (galleryView === 'images') {
+ return imagesTotal?.total ?? 0;
+ } else {
+ return assetsTotal?.total ?? 0;
+ }
+ }, [assetsTotal?.total, galleryView, imagesTotal?.total]);
+ console.log({ offset })
+ const currentPage = useMemo(() => Math.ceil(offset / IMAGE_LIMIT), [offset]);
+ const pages = useMemo(() => Math.ceil(total / IMAGE_LIMIT), [total]);
+
+ const isNextEnabled = useMemo(() => {
+ if (!count) {
+ return false;
+ }
+ return currentPage < pages;
+ }, [count, currentPage, pages]);
+ const isPrevEnabled = useMemo(() => {
+ if (!count) {
+ return false;
+ }
+ return offset > 0;
+ }, [count, offset]);
+
+ const goNext = useCallback(() => {
+ dispatch(offsetChanged(offset + IMAGE_LIMIT));
+ }, [dispatch, offset]);
+ const goPrev = useCallback(() => {
+ dispatch(offsetChanged(Math.max(offset - IMAGE_LIMIT, 0)));
+ }, [dispatch, offset]);
+
+ const goToPage = useCallback(
+ (page: number) => {
+ const p = Math.max(0, Math.min(page, pages - 1));
+ dispatch(offsetChanged(page * IMAGE_LIMIT));
+ },
+ [dispatch, pages]
+ );
+ const goToFirst = useCallback(() => {
+ dispatch(offsetChanged(0));
+ }, [dispatch]);
+ const goToLast = useCallback(() => {
+ dispatch(offsetChanged(pages * IMAGE_LIMIT));
+ }, [dispatch, pages]);
+
+ // calculate the page buttons to display - current page with 3 around it
+ const pageButtons = useMemo(() => {
+ const buttons = [];
+ const maxPageButtons = 5;
+ let startPage = Math.max(currentPage - (Math.floor(maxPageButtons / 2)), 0);
+ let endPage = Math.min(startPage + maxPageButtons - 1, pages - 1);
+
+ console.log({ startPage })
+ if (endPage - startPage < maxPageButtons - 1) {
+ startPage = Math.max(endPage - maxPageButtons + 1, 0);
+ }
+
+ if (startPage > 0) {
+ buttons.push(0);
+ if (startPage > 1) {
+ buttons.push('...');
+ }
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ buttons.push(i);
+ }
+
+ if (endPage < pages - 1) {
+ if (endPage < pages - 2) {
+ buttons.push('...');
+ }
+ buttons.push(pages - 1);
+ }
+
+ return buttons;
+ }, [currentPage, pages]);
+
+ const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]);
+ 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);
+ return `${startItem}-${endItem} of ${total}`;
+ }, [total, currentPage]);
+
+ const api = useMemo(
+ () => ({
+ count,
+ total,
+ currentPage,
+ pages,
+ isNextEnabled,
+ isPrevEnabled,
+ goNext,
+ goPrev,
+ goToPage,
+ goToFirst,
+ goToLast,
+ pageButtons,
+ isFirstEnabled,
+ isLastEnabled,
+ rangeDisplay
+ }),
+ [
+ count,
+ total,
+ currentPage,
+ pages,
+ isNextEnabled,
+ isPrevEnabled,
+ goNext,
+ goPrev,
+ goToPage,
+ goToFirst,
+ goToLast,
+ pageButtons,
+ isFirstEnabled,
+ isLastEnabled,
+ rangeDisplay
+ ]
+ );
+ return api;
+};
\ No newline at end of file