mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
wip pagination
This commit is contained in:
parent
6b24424727
commit
131e429a0f
@ -1,10 +1,9 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImages2QueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { imagesSelectors } from 'services/api/util';
|
|
||||||
|
|
||||||
export const galleryImageClicked = createAction<{
|
export const galleryImageClicked = createAction<{
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
@ -31,16 +30,16 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
|
|||||||
effect: async (action, { dispatch, getState }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
|
const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const queryArgs = selectListImagesQueryArgs(state);
|
const queryArgs = selectListImages2QueryArgs(state);
|
||||||
const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
const queryResult = imagesApi.endpoints.listImages2.select(queryArgs)(state);
|
||||||
|
if (!queryResult.data) {
|
||||||
if (!listImagesData) {
|
|
||||||
// Should never happen if we have clicked a gallery image
|
// Should never happen if we have clicked a gallery image
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageDTOs = imagesSelectors.selectAll(listImagesData);
|
const imageDTOs = queryResult.data.items;
|
||||||
const selection = state.gallery.selection;
|
const selection = state.gallery.selection;
|
||||||
|
console.log({ queryArgs, imageDTOs, selection });
|
||||||
|
|
||||||
if (altKey) {
|
if (altKey) {
|
||||||
if (state.gallery.imageToCompare?.image_name === imageDTO.image_name) {
|
if (state.gallery.imageToCompare?.image_name === imageDTO.image_name) {
|
||||||
|
@ -1,24 +1,9 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import type { MouseEvent, ReactElement } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
type Props = {
|
const sx: SystemStyleObject = {
|
||||||
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
||||||
tooltip: string;
|
|
||||||
icon?: ReactElement;
|
|
||||||
styleOverrides?: SystemStyleObject;
|
|
||||||
};
|
|
||||||
|
|
||||||
const IAIDndImageIcon = (props: Props) => {
|
|
||||||
const { onClick, tooltip, icon, styleOverrides } = props;
|
|
||||||
|
|
||||||
const sx = useMemo(
|
|
||||||
() => ({
|
|
||||||
position: 'absolute',
|
|
||||||
top: 1,
|
|
||||||
insetInlineEnd: 1,
|
|
||||||
p: 0,
|
|
||||||
minW: 0,
|
minW: 0,
|
||||||
svg: {
|
svg: {
|
||||||
transitionProperty: 'common',
|
transitionProperty: 'common',
|
||||||
@ -27,23 +12,29 @@ const IAIDndImageIcon = (props: Props) => {
|
|||||||
_hover: { fill: 'base.50' },
|
_hover: { fill: 'base.50' },
|
||||||
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
|
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
|
||||||
},
|
},
|
||||||
...styleOverrides,
|
};
|
||||||
}),
|
|
||||||
[styleOverrides]
|
type Props = Omit<IconButtonProps, 'aria-label' | 'onClick' | 'tooltip'> & {
|
||||||
);
|
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
tooltip: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAIDndImageIcon = (props: Props) => {
|
||||||
|
const { onClick, tooltip, icon, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label="tooltip"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label={tooltip}
|
|
||||||
tooltip={tooltip}
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="link"
|
variant="link"
|
||||||
sx={sx}
|
sx={sx}
|
||||||
data-testid={tooltip}
|
data-testid={tooltip}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// export default IAIDndImageIcon;
|
||||||
export default memo(IAIDndImageIcon);
|
export default memo(IAIDndImageIcon);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
|
||||||
import { Box, Flex, Spinner } from '@invoke-ai/ui-library';
|
import { Box, Flex, Spinner } from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
@ -185,7 +184,7 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<>
|
<Flex flexDir="column" top={1} insetInlineEnd={1}>
|
||||||
<IAIDndImageIcon
|
<IAIDndImageIcon
|
||||||
onClick={handleResetControlImage}
|
onClick={handleResetControlImage}
|
||||||
icon={controlImage ? <PiArrowCounterClockwiseBold size={16} /> : undefined}
|
icon={controlImage ? <PiArrowCounterClockwiseBold size={16} /> : undefined}
|
||||||
@ -195,15 +194,13 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
|
|||||||
onClick={handleSaveControlImage}
|
onClick={handleSaveControlImage}
|
||||||
icon={controlImage ? <PiFloppyDiskBold size={16} /> : undefined}
|
icon={controlImage ? <PiFloppyDiskBold size={16} /> : undefined}
|
||||||
tooltip={t('controlnet.saveControlImage')}
|
tooltip={t('controlnet.saveControlImage')}
|
||||||
styleOverrides={saveControlImageStyleOverrides}
|
|
||||||
/>
|
/>
|
||||||
<IAIDndImageIcon
|
<IAIDndImageIcon
|
||||||
onClick={handleSetControlImageToDimensions}
|
onClick={handleSetControlImageToDimensions}
|
||||||
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
||||||
tooltip={t('controlnet.setControlImageDimensions')}
|
tooltip={t('controlnet.setControlImageDimensions')}
|
||||||
styleOverrides={setControlImageDimensionsStyleOverrides}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</Flex>
|
||||||
|
|
||||||
{pendingControlImages.includes(id) && (
|
{pendingControlImages.includes(id) && (
|
||||||
<Flex
|
<Flex
|
||||||
@ -226,6 +223,3 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(ControlAdapterImagePreview);
|
export default memo(ControlAdapterImagePreview);
|
||||||
|
|
||||||
const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 };
|
|
||||||
const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 };
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
|
||||||
import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library';
|
import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -180,13 +179,13 @@ export const ControlAdapterImagePreview = memo(
|
|||||||
onClick={handleSaveControlImage}
|
onClick={handleSaveControlImage}
|
||||||
icon={controlImage ? <PiFloppyDiskBold size={16} /> : undefined}
|
icon={controlImage ? <PiFloppyDiskBold size={16} /> : undefined}
|
||||||
tooltip={t('controlnet.saveControlImage')}
|
tooltip={t('controlnet.saveControlImage')}
|
||||||
styleOverrides={saveControlImageStyleOverrides}
|
mt={6}
|
||||||
/>
|
/>
|
||||||
<IAIDndImageIcon
|
<IAIDndImageIcon
|
||||||
onClick={handleSetControlImageToDimensions}
|
onClick={handleSetControlImageToDimensions}
|
||||||
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
||||||
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
||||||
styleOverrides={setControlImageDimensionsStyleOverrides}
|
mt={12}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
@ -212,6 +211,3 @@ export const ControlAdapterImagePreview = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview';
|
ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview';
|
||||||
|
|
||||||
const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 };
|
|
||||||
const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 };
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
|
||||||
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
|
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -100,7 +99,7 @@ export const IPAdapterImagePreview = memo(
|
|||||||
onClick={handleSetControlImageToDimensions}
|
onClick={handleSetControlImageToDimensions}
|
||||||
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
||||||
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
||||||
styleOverrides={setControlImageDimensionsStyleOverrides}
|
mt={6}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -109,5 +108,3 @@ export const IPAdapterImagePreview = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
|
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
|
||||||
|
|
||||||
const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 };
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
|
||||||
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
|
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -86,7 +85,6 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData,
|
|||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
postUploadAction={postUploadAction}
|
postUploadAction={postUploadAction}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<IAIDndImageIcon
|
<IAIDndImageIcon
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
@ -97,7 +95,7 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData,
|
|||||||
onClick={onUseSize}
|
onClick={onUseSize}
|
||||||
icon={imageDTO ? <PiRulerBold size={16} /> : undefined}
|
icon={imageDTO ? <PiRulerBold size={16} /> : undefined}
|
||||||
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
||||||
styleOverrides={useSizeStyleOverrides}
|
mt={6}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -105,5 +103,3 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData,
|
|||||||
});
|
});
|
||||||
|
|
||||||
InitialImagePreview.displayName = 'InitialImagePreview';
|
InitialImagePreview.displayName = 'InitialImagePreview';
|
||||||
|
|
||||||
const useSizeStyleOverrides: SystemStyleObject = { mt: 6 };
|
|
||||||
|
@ -5,10 +5,10 @@ import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
||||||
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
|
||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggableData } from 'features/dnd/types';
|
import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggableData } from 'features/dnd/types';
|
||||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||||
|
import { imageItemContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridItemContainer';
|
||||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||||
import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
@ -16,13 +16,10 @@ import type { MouseEvent } from 'react';
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiStarBold, PiStarFill, PiTrashSimpleFill } from 'react-icons/pi';
|
import { PiStarBold, PiStarFill, PiTrashSimpleFill } from 'react-icons/pi';
|
||||||
import { useGetImageDTOQuery, useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
const imageSx: SystemStyleObject = { w: 'full', h: 'full' };
|
const imageSx: SystemStyleObject = { w: 'full', h: 'full' };
|
||||||
const imageIconStyleOverrides: SystemStyleObject = {
|
|
||||||
bottom: 2,
|
|
||||||
top: 'auto',
|
|
||||||
};
|
|
||||||
const boxSx: SystemStyleObject = {
|
const boxSx: SystemStyleObject = {
|
||||||
containerType: 'inline-size',
|
containerType: 'inline-size',
|
||||||
};
|
};
|
||||||
@ -34,24 +31,22 @@ const badgeSx: SystemStyleObject = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface HoverableImageProps {
|
interface HoverableImageProps {
|
||||||
imageName: string;
|
imageDTO: ImageDTO;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GalleryImage = (props: HoverableImageProps) => {
|
const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { imageName } = props;
|
|
||||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
|
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||||
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
|
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
|
||||||
const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageName);
|
const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageDTO.image_name);
|
||||||
const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO);
|
const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO);
|
||||||
|
|
||||||
const customStarUi = useStore($customStarUI);
|
const customStarUi = useStore($customStarUI);
|
||||||
|
|
||||||
const imageContainerRef = useScrollIntoView(isSelected, props.index, areMultiplesSelected);
|
const imageContainerRef = useScrollIntoView(isSelected, index, areMultiplesSelected);
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -114,32 +109,28 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const starIcon = useMemo(() => {
|
const starIcon = useMemo(() => {
|
||||||
if (imageDTO?.starred) {
|
if (imageDTO.starred) {
|
||||||
return customStarUi ? customStarUi.on.icon : <PiStarFill size="20" />;
|
return customStarUi ? customStarUi.on.icon : <PiStarFill size="20" />;
|
||||||
}
|
}
|
||||||
if (!imageDTO?.starred && isHovered) {
|
if (!imageDTO.starred && isHovered) {
|
||||||
return customStarUi ? customStarUi.off.icon : <PiStarBold size="20" />;
|
return customStarUi ? customStarUi.off.icon : <PiStarBold size="20" />;
|
||||||
}
|
}
|
||||||
}, [imageDTO?.starred, isHovered, customStarUi]);
|
}, [imageDTO.starred, isHovered, customStarUi]);
|
||||||
|
|
||||||
const starTooltip = useMemo(() => {
|
const starTooltip = useMemo(() => {
|
||||||
if (imageDTO?.starred) {
|
if (imageDTO.starred) {
|
||||||
return customStarUi ? customStarUi.off.text : 'Unstar';
|
return customStarUi ? customStarUi.off.text : 'Unstar';
|
||||||
}
|
}
|
||||||
if (!imageDTO?.starred) {
|
if (!imageDTO.starred) {
|
||||||
return customStarUi ? customStarUi.on.text : 'Star';
|
return customStarUi ? customStarUi.on.text : 'Star';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}, [imageDTO?.starred, customStarUi]);
|
}, [imageDTO.starred, customStarUi]);
|
||||||
|
|
||||||
const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO?.image_name), [imageDTO?.image_name]);
|
const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]);
|
||||||
|
|
||||||
if (!imageDTO) {
|
|
||||||
return <IAIFillSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box w="full" h="full" className="gallerygrid-image" data-testid={dataTestId} sx={boxSx}>
|
<Box w="full" h="full" p={1.5} className={imageItemContainerTestId} data-testid={dataTestId} sx={boxSx}>
|
||||||
<Flex
|
<Flex
|
||||||
ref={imageContainerRef}
|
ref={imageContainerRef}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
@ -183,14 +174,23 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
||||||
)}
|
)}
|
||||||
<IAIDndImageIcon onClick={toggleStarredState} icon={starIcon} tooltip={starTooltip} />
|
<IAIDndImageIcon
|
||||||
|
onClick={toggleStarredState}
|
||||||
|
icon={starIcon}
|
||||||
|
tooltip={starTooltip}
|
||||||
|
position="absolute"
|
||||||
|
top={1}
|
||||||
|
insetInlineEnd={1}
|
||||||
|
/>
|
||||||
|
|
||||||
{isHovered && shift && (
|
{isHovered && shift && (
|
||||||
<IAIDndImageIcon
|
<IAIDndImageIcon
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
icon={<PiTrashSimpleFill size="16px" />}
|
icon={<PiTrashSimpleFill size="16px" />}
|
||||||
tooltip={t('gallery.deleteImage', { count: 1 })}
|
tooltip={t('gallery.deleteImage_one')}
|
||||||
styleOverrides={imageIconStyleOverrides}
|
position="absolute"
|
||||||
|
bottom={1}
|
||||||
|
insetInlineEnd={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,120 +1,41 @@
|
|||||||
import { Box, Button, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex, Grid, IconButton } from '@invoke-ai/ui-library';
|
||||||
import type { EntityId } from '@reduxjs/toolkit';
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
|
||||||
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
|
||||||
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
|
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
|
||||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryImages';
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
import { selectListImages2QueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import type { CSSProperties } from 'react';
|
import { memo } from 'react';
|
||||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiImageBold, PiWarningCircleBold } from 'react-icons/pi';
|
import {
|
||||||
import type { GridComponents, ItemContent, ListRange, VirtuosoGridHandle } from 'react-virtuoso';
|
PiCaretDoubleLeftBold,
|
||||||
import { VirtuosoGrid } from 'react-virtuoso';
|
PiCaretDoubleRightBold,
|
||||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
PiCaretLeftBold,
|
||||||
|
PiCaretRightBold,
|
||||||
|
PiImageBold,
|
||||||
|
PiWarningCircleBold,
|
||||||
|
} from 'react-icons/pi';
|
||||||
|
import { useListImages2Query } from 'services/api/endpoints/images';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
import GalleryImage from './GalleryImage';
|
import GalleryImage from './GalleryImage';
|
||||||
import ImageGridItemContainer from './ImageGridItemContainer';
|
|
||||||
import ImageGridListContainer from './ImageGridListContainer';
|
|
||||||
|
|
||||||
const components: GridComponents = {
|
export const imageListContainerTestId = 'image-list-container';
|
||||||
Item: ImageGridItemContainer,
|
export const imageItemContainerTestId = 'image-item-container';
|
||||||
List: ImageGridListContainer,
|
|
||||||
};
|
|
||||||
|
|
||||||
const virtuosoStyles: CSSProperties = { height: '100%' };
|
|
||||||
|
|
||||||
const GalleryImageGrid = () => {
|
const GalleryImageGrid = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
|
||||||
const [initialize, osInstance] = useOverlayScrollbars(overlayScrollbarsParams);
|
|
||||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
|
||||||
const { currentViewTotal } = useBoardTotal(selectedBoardId);
|
|
||||||
const virtuosoRangeRef = useRef<ListRange | null>(null);
|
|
||||||
const virtuosoRef = useRef<VirtuosoGridHandle>(null);
|
|
||||||
const {
|
|
||||||
areMoreImagesAvailable,
|
|
||||||
handleLoadMoreImages,
|
|
||||||
queryResult: { currentData, isFetching, isSuccess, isError },
|
|
||||||
} = useGalleryImages();
|
|
||||||
useGalleryHotkeys();
|
useGalleryHotkeys();
|
||||||
const itemContentFunc: ItemContent<EntityId, void> = useCallback(
|
const { t } = useTranslation();
|
||||||
(index, imageName) => <GalleryImage key={imageName} index={index} imageName={imageName as string} />,
|
const galleryImageMinimumWidth = useAppSelector((s) => s.gallery.galleryImageMinimumWidth);
|
||||||
[]
|
const queryArgs = useAppSelector(selectListImages2QueryArgs);
|
||||||
);
|
const { imageDTOs, isLoading, isSuccess, isError } = useListImages2Query(queryArgs, {
|
||||||
|
selectFromResult: ({ data, isLoading, isSuccess, isError }) => ({
|
||||||
useEffect(() => {
|
imageDTOs: data?.items ?? EMPTY_ARRAY,
|
||||||
// Initialize the gallery's custom scrollbar
|
isLoading,
|
||||||
const { current: root } = rootRef;
|
isSuccess,
|
||||||
if (scroller && root) {
|
isError,
|
||||||
initialize({
|
}),
|
||||||
target: root,
|
|
||||||
elements: {
|
|
||||||
viewport: scroller,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return () => osInstance()?.destroy();
|
|
||||||
}, [scroller, initialize, osInstance]);
|
|
||||||
|
|
||||||
const onRangeChanged = useCallback((range: ListRange) => {
|
|
||||||
virtuosoRangeRef.current = range;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
virtuosoGridRefs.set({ rootRef, virtuosoRangeRef, virtuosoRef });
|
|
||||||
return () => {
|
|
||||||
virtuosoGridRefs.set({});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!currentData) {
|
|
||||||
return (
|
|
||||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
|
||||||
<IAINoContentFallback label={t('gallery.loading')} icon={PiImageBold} />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSuccess && currentData?.ids.length === 0) {
|
|
||||||
return (
|
|
||||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
|
||||||
<IAINoContentFallback label={t('gallery.noImagesInGallery')} icon={PiImageBold} />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSuccess && currentData) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%" id="gallery-grid">
|
|
||||||
<VirtuosoGrid
|
|
||||||
style={virtuosoStyles}
|
|
||||||
data={currentData.ids}
|
|
||||||
endReached={handleLoadMoreImages}
|
|
||||||
components={components}
|
|
||||||
scrollerRef={setScroller}
|
|
||||||
itemContent={itemContentFunc}
|
|
||||||
ref={virtuosoRef}
|
|
||||||
rangeChanged={onRangeChanged}
|
|
||||||
overscan={10}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
onClick={handleLoadMoreImages}
|
|
||||||
isDisabled={!areMoreImagesAvailable}
|
|
||||||
isLoading={isFetching}
|
|
||||||
loadingText={t('gallery.loading')}
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
{`${t('accessibility.loadMore')} (${currentData.ids.length} / ${currentViewTotal})`}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
@ -124,7 +45,63 @@ const GalleryImageGrid = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
|
<IAINoContentFallback label={t('gallery.loading')} icon={PiImageBold} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageDTOs.length === 0) {
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
|
<IAINoContentFallback label={t('gallery.noImagesInGallery')} icon={PiImageBold} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box data-overlayscrollbars="" h="100%" id="gallery-grid">
|
||||||
|
<Grid
|
||||||
|
className="list-container"
|
||||||
|
gridTemplateColumns={`repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr))`}
|
||||||
|
data-testid={imageListContainerTestId}
|
||||||
|
>
|
||||||
|
{imageDTOs.map((imageDTO, index) => (
|
||||||
|
<GalleryImage key={imageDTO.image_name} imageDTO={imageDTO} index={index} />
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<GalleryPagination />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(GalleryImageGrid);
|
export default memo(GalleryImageGrid);
|
||||||
|
|
||||||
|
const GalleryImageContainer = memo(({ imageDTO, index }: { imageDTO: ImageDTO; index: number }) => {
|
||||||
|
return (
|
||||||
|
<Box className="item-container" p={1.5} data-testid={imageItemContainerTestId}>
|
||||||
|
<GalleryImage imageDTO={imageDTO} index={index} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
GalleryImageContainer.displayName = 'GalleryImageContainer';
|
||||||
|
|
||||||
|
const GalleryPagination = memo(() => {
|
||||||
|
const { first, prev, next, last, isFirstEnabled, isPrevEnabled, isNextEnabled, isLastEnabled } =
|
||||||
|
useGalleryPagination();
|
||||||
|
return (
|
||||||
|
<Flex gap={2}>
|
||||||
|
<IconButton aria-label="prev" icon={<PiCaretDoubleLeftBold />} onClick={first} isDisabled={!isFirstEnabled} />
|
||||||
|
<IconButton aria-label="prev" icon={<PiCaretLeftBold />} onClick={prev} isDisabled={!isPrevEnabled} />
|
||||||
|
<IconButton aria-label="next" icon={<PiCaretRightBold />} onClick={next} isDisabled={!isNextEnabled} />
|
||||||
|
<IconButton aria-label="next" icon={<PiCaretDoubleRightBold />} onClick={last} isDisabled={!isLastEnabled} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
GalleryPagination.displayName = 'GalleryPagination';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, Flex, IconButton, Spinner } from '@invoke-ai/ui-library';
|
import { Box, Flex, IconButton, Spinner } from '@invoke-ai/ui-library';
|
||||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
import { useGalleryImages, useGalleryPagination } from 'features/gallery/hooks/useGalleryImages';
|
||||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -15,12 +15,9 @@ const NextPrevImageButtons = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { prevImage, nextImage, isOnFirstImage, isOnLastImage } = useGalleryNavigation();
|
const { prevImage, nextImage, isOnFirstImage, isOnLastImage } = useGalleryNavigation();
|
||||||
|
const { isFetching } = useGalleryImages().queryResult;
|
||||||
|
|
||||||
const {
|
const { isNextEnabled, next } = useGalleryPagination();
|
||||||
areMoreImagesAvailable,
|
|
||||||
handleLoadMoreImages,
|
|
||||||
queryResult: { isFetching },
|
|
||||||
} = useGalleryImages();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pos="relative" h="full" w="full">
|
<Box pos="relative" h="full" w="full">
|
||||||
@ -47,17 +44,17 @@ const NextPrevImageButtons = () => {
|
|||||||
sx={nextPrevButtonStyles}
|
sx={nextPrevButtonStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOnLastImage && areMoreImagesAvailable && !isFetching && (
|
{isOnLastImage && isNextEnabled && !isFetching && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('accessibility.loadMore')}
|
aria-label={t('accessibility.loadMore')}
|
||||||
icon={<PiCaretDoubleRightBold size={64} />}
|
icon={<PiCaretDoubleRightBold size={64} />}
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
onClick={handleLoadMoreImages}
|
onClick={next}
|
||||||
boxSize={16}
|
boxSize={16}
|
||||||
sx={nextPrevButtonStyles}
|
sx={nextPrevButtonStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOnLastImage && areMoreImagesAvailable && isFetching && (
|
{isOnLastImage && isNextEnabled && isFetching && (
|
||||||
<Flex w={16} h={16} alignItems="center" justifyContent="center">
|
<Flex w={16} h={16} alignItems="center" justifyContent="center">
|
||||||
<Spinner opacity={0.5} size="xl" />
|
<Spinner opacity={0.5} size="xl" />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryImages';
|
||||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
import { selectListImages2QueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useListImages2Query } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
import { useGalleryNavigation } from './useGalleryNavigation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers gallery hotkeys. This hook is a singleton.
|
* Registers gallery hotkeys. This hook is a singleton.
|
||||||
@ -17,21 +20,30 @@ export const useGalleryHotkeys = () => {
|
|||||||
return activeTabName !== 'canvas' || !isStaging;
|
return activeTabName !== 'canvas' || !isStaging;
|
||||||
}, [activeTabName, isStaging]);
|
}, [activeTabName, isStaging]);
|
||||||
|
|
||||||
const {
|
const { next, prev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
|
||||||
areMoreImagesAvailable,
|
const queryArgs = useAppSelector(selectListImages2QueryArgs);
|
||||||
handleLoadMoreImages,
|
const queryResult = useListImages2Query(queryArgs);
|
||||||
queryResult: { isFetching },
|
|
||||||
} = useGalleryImages();
|
|
||||||
|
|
||||||
const { handleLeftImage, handleRightImage, handleUpImage, handleDownImage, isOnLastImage, areImagesBelowCurrent } =
|
const {
|
||||||
useGalleryNavigation();
|
handleLeftImage,
|
||||||
|
handleRightImage,
|
||||||
|
handleUpImage,
|
||||||
|
handleDownImage,
|
||||||
|
areImagesBelowCurrent,
|
||||||
|
isOnFirstImageOfView,
|
||||||
|
isOnLastImageOfView,
|
||||||
|
} = useGalleryNavigation();
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
['left', 'alt+left'],
|
['left', 'alt+left'],
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (isOnFirstImageOfView && isPrevEnabled && !queryResult.isFetching) {
|
||||||
|
prev();
|
||||||
|
return;
|
||||||
|
}
|
||||||
canNavigateGallery && handleLeftImage(e.altKey);
|
canNavigateGallery && handleLeftImage(e.altKey);
|
||||||
},
|
},
|
||||||
[handleLeftImage, canNavigateGallery]
|
[handleLeftImage, canNavigateGallery, isOnFirstImageOfView]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -40,15 +52,15 @@ export const useGalleryHotkeys = () => {
|
|||||||
if (!canNavigateGallery) {
|
if (!canNavigateGallery) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
|
if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) {
|
||||||
handleLoadMoreImages();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isOnLastImage) {
|
if (!isOnLastImageOfView) {
|
||||||
handleRightImage(e.altKey);
|
handleRightImage(e.altKey);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isOnLastImage, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleRightImage, canNavigateGallery]
|
[isOnLastImageOfView, next, isNextEnabled, queryResult.isFetching, handleRightImage, canNavigateGallery]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -63,13 +75,13 @@ export const useGalleryHotkeys = () => {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
['down', 'alt+down'],
|
['down', 'alt+down'],
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!areImagesBelowCurrent && areMoreImagesAvailable && !isFetching) {
|
if (!areImagesBelowCurrent && isNextEnabled && !queryResult.isFetching) {
|
||||||
handleLoadMoreImages();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleDownImage(e.altKey);
|
handleDownImage(e.altKey);
|
||||||
},
|
},
|
||||||
{ preventDefault: true },
|
{ preventDefault: true },
|
||||||
[areImagesBelowCurrent, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleDownImage]
|
[areImagesBelowCurrent, next, isNextEnabled, queryResult.isFetching, handleDownImage]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,38 +1,120 @@
|
|||||||
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImages2QueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { moreImagesLoaded } from 'features/gallery/store/gallerySlice';
|
import { offsetChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
import { useListImages2Query } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides access to the gallery images and a way to imperatively fetch more.
|
|
||||||
*/
|
|
||||||
export const useGalleryImages = () => {
|
export const useGalleryImages = () => {
|
||||||
const dispatch = useAppDispatch();
|
const queryArgs = useAppSelector(selectListImages2QueryArgs);
|
||||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
const queryResult = useListImages2Query(queryArgs);
|
||||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
const imageDTOs = useMemo(() => queryResult.data?.items ?? EMPTY_ARRAY, [queryResult.data]);
|
||||||
const queryResult = useListImagesQuery(queryArgs);
|
|
||||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
|
||||||
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(selectedBoardId);
|
|
||||||
const { data: imagesTotal } = useGetBoardImagesTotalQuery(selectedBoardId);
|
|
||||||
const currentViewTotal = useMemo(
|
|
||||||
() => (galleryView === 'images' ? imagesTotal?.total : assetsTotal?.total),
|
|
||||||
[assetsTotal?.total, galleryView, imagesTotal?.total]
|
|
||||||
);
|
|
||||||
const areMoreImagesAvailable = useMemo(() => {
|
|
||||||
if (!currentViewTotal || !queryResult.data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return queryResult.data.ids.length < currentViewTotal;
|
|
||||||
}, [queryResult.data, currentViewTotal]);
|
|
||||||
const handleLoadMoreImages = useCallback(() => {
|
|
||||||
dispatch(moreImagesLoaded());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
areMoreImagesAvailable,
|
imageDTOs,
|
||||||
handleLoadMoreImages,
|
|
||||||
queryResult,
|
queryResult,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(selectListImages2QueryArgs);
|
||||||
|
const { count } = useListImages2Query(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;
|
||||||
|
};
|
||||||
|
@ -11,7 +11,6 @@ import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAli
|
|||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { imagesSelectors } from 'services/api/util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook is used to navigate the gallery using the arrow keys.
|
* This hook is used to navigate the gallery using the arrow keys.
|
||||||
@ -29,12 +28,13 @@ import { imagesSelectors } from 'services/api/util';
|
|||||||
*/
|
*/
|
||||||
const getImagesPerRow = (): number => {
|
const getImagesPerRow = (): number => {
|
||||||
const widthOfGalleryImage =
|
const widthOfGalleryImage =
|
||||||
document.querySelector(`[data-testid="${imageItemContainerTestId}"]`)?.getBoundingClientRect().width ?? 1;
|
document.querySelector(`.${imageItemContainerTestId}`)?.getBoundingClientRect().width ?? 1;
|
||||||
|
|
||||||
const widthOfGalleryGrid =
|
const widthOfGalleryGrid =
|
||||||
document.querySelector(`[data-testid="${imageListContainerTestId}"]`)?.getBoundingClientRect().width ?? 0;
|
document.querySelector(`[data-testid="${imageListContainerTestId}"]`)?.getBoundingClientRect().width ?? 0;
|
||||||
|
|
||||||
const imagesPerRow = Math.round(widthOfGalleryGrid / widthOfGalleryImage);
|
const imagesPerRow = Math.round(widthOfGalleryGrid / widthOfGalleryImage);
|
||||||
|
console.log({ widthOfGalleryImage, widthOfGalleryGrid, imagesPerRow });
|
||||||
|
|
||||||
return imagesPerRow;
|
return imagesPerRow;
|
||||||
};
|
};
|
||||||
@ -86,6 +86,7 @@ const getUpImage = (images: ImageDTO[], currentIndex: number) => {
|
|||||||
const isOnFirstRow = currentIndex < imagesPerRow;
|
const isOnFirstRow = currentIndex < imagesPerRow;
|
||||||
const index = isOnFirstRow ? currentIndex : clamp(currentIndex - imagesPerRow, 0, images.length - 1);
|
const index = isOnFirstRow ? currentIndex : clamp(currentIndex - imagesPerRow, 0, images.length - 1);
|
||||||
const image = images[index];
|
const image = images[index];
|
||||||
|
console.log({ imagesPerRow, isOnFirstRow });
|
||||||
return { index, image };
|
return { index, image };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,6 +116,8 @@ type UseGalleryNavigationReturn = {
|
|||||||
isOnFirstImage: boolean;
|
isOnFirstImage: boolean;
|
||||||
isOnLastImage: boolean;
|
isOnLastImage: boolean;
|
||||||
areImagesBelowCurrent: boolean;
|
areImagesBelowCurrent: boolean;
|
||||||
|
isOnFirstImageOfView: boolean;
|
||||||
|
isOnLastImageOfView: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,23 +137,19 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
|||||||
return lastSelected;
|
return lastSelected;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const {
|
const { imageDTOs } = useGalleryImages();
|
||||||
queryResult: { data },
|
const loadedImagesCount = useMemo(() => imageDTOs.length, [imageDTOs.length]);
|
||||||
} = useGalleryImages();
|
|
||||||
const loadedImagesCount = useMemo(() => data?.ids.length ?? 0, [data?.ids.length]);
|
|
||||||
const lastSelectedImageIndex = useMemo(() => {
|
const lastSelectedImageIndex = useMemo(() => {
|
||||||
if (!data || !lastSelectedImage) {
|
if (imageDTOs.length === 0 || !lastSelectedImage) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return imagesSelectors.selectAll(data).findIndex((i) => i.image_name === lastSelectedImage.image_name);
|
return imageDTOs.findIndex((i) => i.image_name === lastSelectedImage.image_name);
|
||||||
}, [lastSelectedImage, data]);
|
}, [imageDTOs, lastSelectedImage]);
|
||||||
|
|
||||||
const handleNavigation = useCallback(
|
const handleNavigation = useCallback(
|
||||||
(direction: 'left' | 'right' | 'up' | 'down', alt?: boolean) => {
|
(direction: 'left' | 'right' | 'up' | 'down', alt?: boolean) => {
|
||||||
if (!data) {
|
const { index, image } = getImageFuncs[direction](imageDTOs, lastSelectedImageIndex);
|
||||||
return;
|
console.log({ direction, index, image, imageDTOs, lastSelectedImageIndex });
|
||||||
}
|
|
||||||
const { index, image } = getImageFuncs[direction](imagesSelectors.selectAll(data), lastSelectedImageIndex);
|
|
||||||
if (!image || index === lastSelectedImageIndex) {
|
if (!image || index === lastSelectedImageIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -161,7 +160,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
|||||||
}
|
}
|
||||||
scrollToImage(image.image_name, index);
|
scrollToImage(image.image_name, index);
|
||||||
},
|
},
|
||||||
[data, lastSelectedImageIndex, dispatch]
|
[imageDTOs, lastSelectedImageIndex, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOnFirstImage = useMemo(() => lastSelectedImageIndex === 0, [lastSelectedImageIndex]);
|
const isOnFirstImage = useMemo(() => lastSelectedImageIndex === 0, [lastSelectedImageIndex]);
|
||||||
@ -176,6 +175,14 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
|||||||
return lastSelectedImageIndex + imagesPerRow < loadedImagesCount;
|
return lastSelectedImageIndex + imagesPerRow < loadedImagesCount;
|
||||||
}, [lastSelectedImageIndex, loadedImagesCount]);
|
}, [lastSelectedImageIndex, loadedImagesCount]);
|
||||||
|
|
||||||
|
const isOnFirstImageOfView = useMemo(() => {
|
||||||
|
return lastSelectedImageIndex === 0;
|
||||||
|
}, [lastSelectedImageIndex]);
|
||||||
|
|
||||||
|
const isOnLastImageOfView = useMemo(() => {
|
||||||
|
return lastSelectedImageIndex === loadedImagesCount - 1;
|
||||||
|
}, [lastSelectedImageIndex, loadedImagesCount]);
|
||||||
|
|
||||||
const handleLeftImage = useCallback(
|
const handleLeftImage = useCallback(
|
||||||
(alt?: boolean) => {
|
(alt?: boolean) => {
|
||||||
handleNavigation('left', alt);
|
handleNavigation('left', alt);
|
||||||
@ -222,5 +229,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
|||||||
areImagesBelowCurrent,
|
areImagesBelowCurrent,
|
||||||
nextImage,
|
nextImage,
|
||||||
prevImage,
|
prevImage,
|
||||||
|
isOnFirstImageOfView,
|
||||||
|
isOnLastImageOfView,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -43,9 +43,14 @@ export const useMultiselect = (imageDTO?: ImageDTO) => {
|
|||||||
[dispatch, imageDTO, isMultiSelectEnabled]
|
[dispatch, imageDTO, isMultiSelectEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
const api = useMemo(
|
||||||
|
() => ({
|
||||||
areMultiplesSelected,
|
areMultiplesSelected,
|
||||||
isSelected,
|
isSelected,
|
||||||
handleClick,
|
handleClick,
|
||||||
};
|
}),
|
||||||
|
[areMultiplesSelected, isSelected, handleClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
return api;
|
||||||
};
|
};
|
||||||
|
@ -18,3 +18,14 @@ export const selectListImagesQueryArgs = createMemoizedSelector(
|
|||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const selectListImages2QueryArgs = createMemoizedSelector(
|
||||||
|
selectGallerySlice,
|
||||||
|
(gallery): ListImagesArgs => ({
|
||||||
|
board_id: gallery.selectedBoardId,
|
||||||
|
categories: gallery.galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
|
||||||
|
offset: gallery.offset,
|
||||||
|
limit: gallery.limit,
|
||||||
|
is_intermediate: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@ -19,7 +19,7 @@ const initialGalleryState: GalleryState = {
|
|||||||
selectedBoardId: 'none',
|
selectedBoardId: 'none',
|
||||||
galleryView: 'images',
|
galleryView: 'images',
|
||||||
boardSearchText: '',
|
boardSearchText: '',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: 20,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
isImageViewerOpen: true,
|
isImageViewerOpen: true,
|
||||||
imageToCompare: null,
|
imageToCompare: null,
|
||||||
@ -114,6 +114,9 @@ export const gallerySlice = createSlice({
|
|||||||
comparisonFitChanged: (state, action: PayloadAction<'contain' | 'fill'>) => {
|
comparisonFitChanged: (state, action: PayloadAction<'contain' | 'fill'>) => {
|
||||||
state.comparisonFit = action.payload;
|
state.comparisonFit = action.payload;
|
||||||
},
|
},
|
||||||
|
offsetChanged: (state, action: PayloadAction<number>) => {
|
||||||
|
state.offset = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
@ -157,6 +160,7 @@ export const {
|
|||||||
comparedImagesSwapped,
|
comparedImagesSwapped,
|
||||||
comparisonFitChanged,
|
comparisonFitChanged,
|
||||||
comparisonModeCycled,
|
comparisonModeCycled,
|
||||||
|
offsetChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
const isAnyBoardDeleted = isAnyOf(
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
ImageCategory,
|
ImageCategory,
|
||||||
ImageDTO,
|
ImageDTO,
|
||||||
ListImagesArgs,
|
ListImagesArgs,
|
||||||
|
ListImagesResponse,
|
||||||
OffsetPaginatedResults_ImageDTO_,
|
OffsetPaginatedResults_ImageDTO_,
|
||||||
PostUploadAction,
|
PostUploadAction,
|
||||||
} from 'services/api/types';
|
} from 'services/api/types';
|
||||||
@ -50,6 +51,14 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
/**
|
/**
|
||||||
* Image Queries
|
* Image Queries
|
||||||
*/
|
*/
|
||||||
|
listImages2: build.query<ListImagesResponse, ListImagesArgs>({
|
||||||
|
query: (queryArgs) => ({
|
||||||
|
// Use the helper to create the URL.
|
||||||
|
url: getListImagesUrl(queryArgs),
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
providesTags: ['FetchOnReconnect'],
|
||||||
|
}),
|
||||||
listImages: build.query<EntityState<ImageDTO, string>, ListImagesArgs>({
|
listImages: build.query<EntityState<ImageDTO, string>, ListImagesArgs>({
|
||||||
query: (queryArgs) => ({
|
query: (queryArgs) => ({
|
||||||
// Use the helper to create the URL.
|
// Use the helper to create the URL.
|
||||||
@ -1304,6 +1313,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
export const {
|
export const {
|
||||||
useGetIntermediatesCountQuery,
|
useGetIntermediatesCountQuery,
|
||||||
useListImagesQuery,
|
useListImagesQuery,
|
||||||
|
useListImages2Query,
|
||||||
useGetImageDTOQuery,
|
useGetImageDTOQuery,
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataQuery,
|
||||||
useGetImageWorkflowQuery,
|
useGetImageWorkflowQuery,
|
||||||
|
@ -7,6 +7,7 @@ export type S = components['schemas'];
|
|||||||
export type ImageCache = EntityState<ImageDTO, string>;
|
export type ImageCache = EntityState<ImageDTO, string>;
|
||||||
|
|
||||||
export type ListImagesArgs = NonNullable<paths['/api/v1/images/']['get']['parameters']['query']>;
|
export type ListImagesArgs = NonNullable<paths['/api/v1/images/']['get']['parameters']['query']>;
|
||||||
|
export type ListImagesResponse = paths['/api/v1/images/']['get']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
export type DeleteBoardResult =
|
export type DeleteBoardResult =
|
||||||
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
||||||
|
Loading…
Reference in New Issue
Block a user