From 5101dc4bef9efd1806d7dad6ac0e78c47c35a6cd Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 20 Jun 2024 13:29:16 -0400 Subject: [PATCH] some cleanup, add page buttons --- .../components/ImageGrid/GalleryImageGrid.tsx | 28 +--- .../ImageGrid/GalleryPagination.tsx | 45 ++++++ .../components/NextPrevImageButtons.tsx | 7 +- .../gallery/hooks/useGalleryHotkeys.ts | 16 +- .../gallery/hooks/useGalleryImages.ts | 108 +------------ .../gallery/hooks/useGalleryPagination.ts | 144 ++++++++++++++++++ 6 files changed, 206 insertions(+), 142 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryPagination.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/hooks/useGalleryPagination.ts 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