wip pagination

This commit is contained in:
psychedelicious 2024-06-03 21:33:18 +10:00
parent 6b24424727
commit 131e429a0f
17 changed files with 349 additions and 268 deletions

View File

@ -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) {

View File

@ -1,49 +1,40 @@
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 = {
minW: 0,
svg: {
transitionProperty: 'common',
transitionDuration: 'normal',
fill: 'base.100',
_hover: { fill: 'base.50' },
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
},
};
type Props = Omit<IconButtonProps, 'aria-label' | 'onClick' | 'tooltip'> & {
onClick: (event: MouseEvent<HTMLButtonElement>) => void; onClick: (event: MouseEvent<HTMLButtonElement>) => void;
tooltip: string; tooltip: string;
icon?: ReactElement;
styleOverrides?: SystemStyleObject;
}; };
const IAIDndImageIcon = (props: Props) => { const IAIDndImageIcon = (props: Props) => {
const { onClick, tooltip, icon, styleOverrides } = props; const { onClick, tooltip, icon, ...rest } = props;
const sx = useMemo(
() => ({
position: 'absolute',
top: 1,
insetInlineEnd: 1,
p: 0,
minW: 0,
svg: {
transitionProperty: 'common',
transitionDuration: 'normal',
fill: 'base.100',
_hover: { fill: 'base.50' },
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
},
...styleOverrides,
}),
[styleOverrides]
);
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]
); );
}; };

View File

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

View File

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

View File

@ -43,9 +43,14 @@ export const useMultiselect = (imageDTO?: ImageDTO) => {
[dispatch, imageDTO, isMultiSelectEnabled] [dispatch, imageDTO, isMultiSelectEnabled]
); );
return { const api = useMemo(
areMultiplesSelected, () => ({
isSelected, areMultiplesSelected,
handleClick, isSelected,
}; handleClick,
}),
[areMultiplesSelected, isSelected, handleClick]
);
return api;
}; };

View File

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

View File

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

View File

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

View File

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