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 c99be29788..8b0f118f92 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx @@ -1,73 +1,54 @@ -import { Button, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; -import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination'; -import { PiCaretDoubleLeftBold, PiCaretDoubleRightBold, PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; +import { Button, Flex, IconButton, Spacer } from '@invoke-ai/ui-library'; +import { ELLIPSIS, useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination'; +import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; export const GalleryPagination = () => { - const { - goPrev, - goNext, - goToFirst, - goToLast, - isFirstEnabled, - isLastEnabled, - isPrevEnabled, - isNextEnabled, - pageButtons, - goToPage, - currentPage, - rangeDisplay, - total, - } = useGalleryPagination(); + const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, total } = + useGalleryPagination(); if (!total) { - return ; + return null; } return ( - - - } - onClick={goToFirst} - isDisabled={!isFirstEnabled} - /> - } - onClick={goPrev} - isDisabled={!isPrevEnabled} - /> - - {pageButtons.map((page) => ( + + } + onClick={goPrev} + isDisabled={!isPrevEnabled} + variant="ghost" + /> + + {pageButtons.map((page, i) => { + if (page === ELLIPSIS) { + return ( + + ); + } + return ( - ))} - - } - onClick={goNext} - isDisabled={!isNextEnabled} - /> - } - onClick={goToLast} - isDisabled={!isLastEnabled} - /> - - {rangeDisplay} + ); + })} + + } + onClick={goNext} + isDisabled={!isNextEnabled} + variant="ghost" + /> ); }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts index 0663e5e5bc..158c722092 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts @@ -4,7 +4,58 @@ import { offsetChanged } from 'features/gallery/store/gallerySlice'; import { useCallback, useEffect, useMemo } from 'react'; import { useListImagesQuery } from 'services/api/endpoints/images'; -export const useGalleryPagination = (pageButtonsPerSide: number = 2) => { +// Some logic copied from https://github.com/chakra-ui/zag/blob/1925b7342dc76fb06a7ec59a5a4c0063a4620422/packages/machines/pagination/src/pagination.utils.ts + +export const range = (start: number, end: number) => { + const length = end - start + 1; + return Array.from({ length }, (_, idx) => idx + start); +}; + +export const ELLIPSIS = 'ellipsis' as const; + +export const getRange = (currentPage: number, totalPages: number, siblingCount: number) => { + /** + * `2 * ctx.siblingCount + 5` explanation: + * 2 * ctx.siblingCount for left/right siblings + * 5 for 2x left/right ellipsis, 2x first/last page + 1x current page + * + * For some page counts (e.g. totalPages: 8, siblingCount: 2), + * calculated max page is higher than total pages, + * so we need to take the minimum of both. + */ + const totalPageNumbers = Math.min(2 * siblingCount + 5, totalPages); + + const firstPageIndex = 1; + const lastPageIndex = totalPages; + + const leftSiblingIndex = Math.max(currentPage - siblingCount, firstPageIndex); + const rightSiblingIndex = Math.min(currentPage + siblingCount, lastPageIndex); + + const showLeftEllipsis = leftSiblingIndex > firstPageIndex + 1; + const showRightEllipsis = rightSiblingIndex < lastPageIndex - 1; + + const itemCount = totalPageNumbers - 2; // 2 stands for one ellipsis and either first or last page + + if (!showLeftEllipsis && showRightEllipsis) { + const leftRange = range(1, itemCount); + return [...leftRange, ELLIPSIS, lastPageIndex]; + } + + if (showLeftEllipsis && !showRightEllipsis) { + const rightRange = range(lastPageIndex - itemCount + 1, lastPageIndex); + return [firstPageIndex, ELLIPSIS, ...rightRange]; + } + + if (showLeftEllipsis && showRightEllipsis) { + const middleRange = range(leftSiblingIndex, rightSiblingIndex); + return [firstPageIndex, ELLIPSIS, ...middleRange, ELLIPSIS, lastPageIndex]; + } + + const fullRange = range(firstPageIndex, lastPageIndex); + return fullRange as (number | 'ellipsis')[]; +}; + +export const useGalleryPagination = () => { const dispatch = useAppDispatch(); const { offset, limit } = useAppSelector((s) => s.gallery); const queryArgs = useAppSelector(selectListImagesQueryArgs); @@ -57,24 +108,12 @@ export const useGalleryPagination = (pageButtonsPerSide: number = 2) => { } }, [currentPage, pages, goToLast]); - // calculate the page buttons to display - current page with 3 around it const pageButtons = useMemo(() => { - const buttons = []; - const maxPageButtons = pageButtonsPerSide * 2 + 1; - let startPage = Math.max(currentPage - Math.floor(maxPageButtons / 2), 0); - const endPage = Math.min(startPage + maxPageButtons - 1, pages - 1); - - if (endPage - startPage < maxPageButtons - 1) { - startPage = Math.max(endPage - maxPageButtons + 1, 0); + if (pages > 7) { + return getRange(currentPage + 1, pages, 1); } - - for (let i = startPage; i <= endPage; i++) { - buttons.push(i); - } - - return buttons; - }, [currentPage, pageButtonsPerSide, pages]); - + return range(1, pages); + }, [currentPage, pages]); const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]); const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]);