mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
perf(ui): slightly more efficient gallery pagination componsts
This commit is contained in:
@@ -6,7 +6,6 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage';
|
||||
import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'features/dnd/DndDragPreviewMultipleImage';
|
||||
@@ -19,6 +18,7 @@ import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid
|
||||
import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/SizedSkeletonLoader';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { atom } from 'nanostores';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -178,7 +178,15 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
);
|
||||
}, [imageDTO, element, store, dndId]);
|
||||
|
||||
const isHovered = useBoolean(false);
|
||||
const $isHovered = useMemo(() => atom(false), []);
|
||||
|
||||
const onMouseOver = useCallback(() => {
|
||||
$isHovered.set(true);
|
||||
}, [$isHovered]);
|
||||
|
||||
const onMouseOut = useCallback(() => {
|
||||
$isHovered.set(false);
|
||||
}, [$isHovered]);
|
||||
|
||||
const onClick = useCallback<MouseEventHandler<HTMLDivElement>>(
|
||||
(e) => {
|
||||
@@ -217,8 +225,8 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
<Flex
|
||||
role="button"
|
||||
className="gallery-image"
|
||||
onMouseOver={isHovered.setTrue}
|
||||
onMouseOut={isHovered.setFalse}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
data-selected={isSelected}
|
||||
@@ -234,7 +242,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
/>
|
||||
<GalleryImageHoverIcons imageDTO={imageDTO} isHovered={isHovered.isTrue} />
|
||||
<GalleryImageHoverIcons imageDTO={imageDTO} $isHovered={$isHovered} />
|
||||
</Flex>
|
||||
</Box>
|
||||
{dragPreviewState?.type === 'multiple-image' ? createMultipleImageDragPreview(dragPreviewState) : null}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { GalleryImageDeleteIconButton } from 'features/gallery/components/ImageGrid/GalleryImageDeleteIconButton';
|
||||
import { GalleryImageOpenInViewerIconButton } from 'features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton';
|
||||
import { GalleryImageSizeBadge } from 'features/gallery/components/ImageGrid/GalleryImageSizeBadge';
|
||||
import { GalleryImageStarIconButton } from 'features/gallery/components/ImageGrid/GalleryImageStarIconButton';
|
||||
import { selectAlwaysShouldImageSizeBadge } from 'features/gallery/store/gallerySelectors';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
isHovered: boolean;
|
||||
$isHovered: Atom<boolean>;
|
||||
};
|
||||
|
||||
export const GalleryImageHoverIcons = memo(({ imageDTO, isHovered }: Props) => {
|
||||
export const GalleryImageHoverIcons = memo(({ imageDTO, $isHovered }: Props) => {
|
||||
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);
|
||||
const isHovered = useStore($isHovered);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -17,79 +17,19 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const JumpTo = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { goToPage, currentPage, pages } = useGalleryPagination();
|
||||
const [newPage, setNewPage] = useState(currentPage);
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setNewPage(currentPage);
|
||||
setTimeout(() => {
|
||||
const input = ref.current?.querySelector('input');
|
||||
input?.focus();
|
||||
input?.select();
|
||||
}, 0);
|
||||
}, [currentPage]);
|
||||
|
||||
const onChangeJumpTo = useCallback((v: number) => {
|
||||
setNewPage(v - 1);
|
||||
}, []);
|
||||
|
||||
const onClickGo = useCallback(() => {
|
||||
goToPage(newPage);
|
||||
onClose();
|
||||
}, [newPage, goToPage, onClose]);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
onClickGo();
|
||||
},
|
||||
{ enabled: isOpen, enableOnFormTags: ['input'] },
|
||||
[isOpen, onClickGo]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
setNewPage(currentPage);
|
||||
onClose();
|
||||
},
|
||||
{ enabled: isOpen, enableOnFormTags: ['input'] },
|
||||
[isOpen, onClose]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNewPage(currentPage);
|
||||
}, [currentPage]);
|
||||
const disclosure = useDisclosure();
|
||||
|
||||
return (
|
||||
<Popover isOpen={isOpen} onClose={onClose} onOpen={onOpen} isLazy lazyBehavior="unmount">
|
||||
<Popover isOpen={disclosure.isOpen} onClose={disclosure.onClose} isLazy lazyBehavior="unmount">
|
||||
<PopoverTrigger>
|
||||
<Button aria-label={t('gallery.jump')} size="sm" onClick={onToggle} variant="outline">
|
||||
<Button aria-label={t('gallery.jump')} size="sm" onClick={disclosure.onToggle} variant="outline">
|
||||
{t('gallery.jump')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<Flex gap={2} alignItems="center">
|
||||
<FormControl>
|
||||
<CompositeNumberInput
|
||||
ref={ref}
|
||||
size="sm"
|
||||
maxW="60px"
|
||||
value={newPage + 1}
|
||||
min={1}
|
||||
max={pages}
|
||||
step={1}
|
||||
onChange={onChangeJumpTo}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button h="full" size="sm" onClick={onClickGo}>
|
||||
{t('gallery.go')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<JumpToContent disclosure={disclosure} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -97,3 +37,68 @@ export const JumpTo = memo(() => {
|
||||
});
|
||||
|
||||
JumpTo.displayName = 'JumpTo';
|
||||
|
||||
const JumpToContent = memo(({ disclosure }: { disclosure: ReturnType<typeof useDisclosure> }) => {
|
||||
const { t } = useTranslation();
|
||||
const { goToPage, currentPage, pages } = useGalleryPagination();
|
||||
const [newPage, setNewPage] = useState(currentPage);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onChangeJumpTo = useCallback((v: number) => {
|
||||
setNewPage(v - 1);
|
||||
}, []);
|
||||
|
||||
const onClickGo = useCallback(() => {
|
||||
goToPage(newPage);
|
||||
disclosure.onClose();
|
||||
}, [goToPage, newPage, disclosure]);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
onClickGo();
|
||||
},
|
||||
{ enabled: disclosure.isOpen, enableOnFormTags: ['input'] },
|
||||
[disclosure.isOpen, onClickGo]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
setNewPage(currentPage);
|
||||
disclosure.onClose();
|
||||
},
|
||||
{ enabled: disclosure.isOpen, enableOnFormTags: ['input'] },
|
||||
[disclosure.isOpen, disclosure.onClose]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const input = ref.current?.querySelector('input');
|
||||
input?.focus();
|
||||
input?.select();
|
||||
}, 0);
|
||||
setNewPage(currentPage);
|
||||
}, [currentPage]);
|
||||
|
||||
return (
|
||||
<Flex gap={2} alignItems="center">
|
||||
<FormControl>
|
||||
<CompositeNumberInput
|
||||
ref={ref}
|
||||
size="sm"
|
||||
maxW="60px"
|
||||
value={newPage + 1}
|
||||
min={1}
|
||||
max={pages}
|
||||
step={1}
|
||||
onChange={onChangeJumpTo}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button h="full" size="sm" onClick={onClickGo}>
|
||||
{t('gallery.go')}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
JumpToContent.displayName = 'JumpToContent';
|
||||
|
||||
@@ -115,19 +115,13 @@ export const useGalleryPagination = () => {
|
||||
},
|
||||
[throttledOnOffsetChanged, limit]
|
||||
);
|
||||
const goToFirst = useCallback(() => {
|
||||
throttledOnOffsetChanged({ offset: 0 });
|
||||
}, [throttledOnOffsetChanged]);
|
||||
const goToLast = useCallback(() => {
|
||||
throttledOnOffsetChanged({ offset: (pages - 1) * (limit || 0) });
|
||||
}, [throttledOnOffsetChanged, pages, limit]);
|
||||
|
||||
// handle when total/pages decrease and user is on high page number (ie bulk removing or deleting)
|
||||
useEffect(() => {
|
||||
if (pages && currentPage + 1 > pages) {
|
||||
goToLast();
|
||||
throttledOnOffsetChanged({ offset: (pages - 1) * (limit || 0) });
|
||||
}
|
||||
}, [currentPage, pages, goToLast]);
|
||||
}, [currentPage, pages, throttledOnOffsetChanged, limit]);
|
||||
|
||||
const pageButtons = useMemo(() => {
|
||||
if (pages > 7) {
|
||||
@@ -135,35 +129,16 @@ export const useGalleryPagination = () => {
|
||||
}
|
||||
return range(1, pages);
|
||||
}, [currentPage, pages]);
|
||||
const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]);
|
||||
const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]);
|
||||
|
||||
const rangeDisplay = useMemo(() => {
|
||||
const startItem = currentPage * (limit || 0) + 1;
|
||||
const endItem = Math.min((currentPage + 1) * (limit || 0), total);
|
||||
return `${startItem}-${endItem} of ${total}`;
|
||||
}, [total, currentPage, limit]);
|
||||
|
||||
const numberOnPage = useMemo(() => {
|
||||
return Math.min((currentPage + 1) * (limit || 0), total);
|
||||
}, [currentPage, limit, total]);
|
||||
|
||||
return {
|
||||
count,
|
||||
total,
|
||||
currentPage,
|
||||
pages,
|
||||
isNextEnabled,
|
||||
isPrevEnabled,
|
||||
goNext,
|
||||
goPrev,
|
||||
goToPage,
|
||||
goToFirst,
|
||||
goToLast,
|
||||
goNext,
|
||||
isPrevEnabled,
|
||||
isNextEnabled,
|
||||
pageButtons,
|
||||
isFirstEnabled,
|
||||
isLastEnabled,
|
||||
rangeDisplay,
|
||||
numberOnPage,
|
||||
goToPage,
|
||||
currentPage,
|
||||
total,
|
||||
pages,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user