perf(ui): slightly more efficient gallery pagination componsts

This commit is contained in:
psychedelicious
2025-02-15 22:14:51 +10:00
parent 3c5e829c72
commit acb7ef8837
4 changed files with 96 additions and 105 deletions

View File

@@ -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}

View File

@@ -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 (
<>

View File

@@ -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';

View File

@@ -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,
};
};