This commit is contained in:
psychedelicious 2023-07-10 23:33:21 +10:00
parent 58cb5fefd0
commit 2990fa23fe
10 changed files with 227 additions and 35 deletions

View File

@ -47,7 +47,6 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte
import { addModelSelectedListener } from './listeners/modelSelected'; import { addModelSelectedListener } from './listeners/modelSelected';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
import { addReceivedPageOfImagesListener } from './listeners/receivedPageOfImages'; import { addReceivedPageOfImagesListener } from './listeners/receivedPageOfImages';
import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch';
import { import {
addSessionCanceledFulfilledListener, addSessionCanceledFulfilledListener,
addSessionCanceledPendingListener, addSessionCanceledPendingListener,
@ -201,7 +200,7 @@ addBoardIdSelectedListener();
addReceivedOpenAPISchemaListener(); addReceivedOpenAPISchemaListener();
// Batches // Batches
addSelectionAddedToBatchListener(); // addSelectionAddedToBatchListener();
addAddBoardToBatchListener(); addAddBoardToBatchListener();
// DND // DND

View File

@ -0,0 +1,42 @@
import { Box, Flex, Icon } from '@chakra-ui/react';
import { FaExclamation } from 'react-icons/fa';
const IAIErrorLoadingImageFallback = () => {
return (
<Box
sx={{
position: 'relative',
height: 'full',
width: 'full',
'::before': {
content: "''",
display: 'block',
pt: '100%',
},
}}
>
<Flex
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
height: 'full',
width: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
bg: 'base.100',
color: 'base.500',
_dark: {
color: 'base.700',
bg: 'base.850',
},
}}
>
<Icon as={FaExclamation} boxSize={16} opacity={0.7} />
</Flex>
</Box>
);
};
export default IAIErrorLoadingImageFallback;

View File

@ -0,0 +1,30 @@
import { Box, Skeleton } from '@chakra-ui/react';
const IAIFillSkeleton = () => {
return (
<Skeleton
sx={{
position: 'relative',
height: 'full',
width: 'full',
'::before': {
content: "''",
display: 'block',
pt: '100%',
},
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
height: 'full',
width: 'full',
}}
/>
</Skeleton>
);
};
export default IAIFillSkeleton;

View File

@ -1,10 +1,12 @@
import { Box, Spinner } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd'; import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import IAIErrorLoadingImageFallback from 'common/components/IAIErrorLoadingImageFallback';
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
import { import {
batchImageRangeEndSelected, batchImageRangeEndSelected,
batchImageSelected, batchImageSelected,
@ -32,11 +34,13 @@ type BatchImageProps = {
const BatchImage = (props: BatchImageProps) => { const BatchImage = (props: BatchImageProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { imageName } = props; const { imageName } = props;
const {
const { currentData: imageDTO } = useGetImageDTOQuery(imageName); currentData: imageDTO,
isLoading,
isError,
isSuccess,
} = useGetImageDTOQuery(imageName);
const selector = useMemo(() => makeSelector(imageName), [imageName]); const selector = useMemo(() => makeSelector(imageName), [imageName]);
const { isSelected, selectionCount, selection } = useAppSelector(selector); const { isSelected, selectionCount, selection } = useAppSelector(selector);
@ -76,8 +80,12 @@ const BatchImage = (props: BatchImageProps) => {
} }
}, [imageDTO, selection, selectionCount]); }, [imageDTO, selection, selectionCount]);
if (!imageDTO) { if (isLoading) {
return <Spinner />; return <IAIFillSkeleton />;
}
if (isError || !imageDTO) {
return <IAIErrorLoadingImageFallback />;
} }
return ( return (
@ -108,6 +116,7 @@ const BatchImage = (props: BatchImageProps) => {
isUploadDisabled={true} isUploadDisabled={true}
resetTooltip="Remove from batch" resetTooltip="Remove from batch"
withResetIcon withResetIcon
thumbnail
/> />
</Box> </Box>
)} )}

View File

@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { memo, useEffect, useRef, useState } from 'react'; import { memo, useEffect, useRef, useState } from 'react';
@ -26,7 +26,6 @@ const selector = createSelector(
); );
const BatchGrid = () => { const BatchGrid = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const rootRef = useRef(null); const rootRef = useRef(null);
const [scroller, setScroller] = useState<HTMLElement | null>(null); const [scroller, setScroller] = useState<HTMLElement | null>(null);

View File

@ -0,0 +1,102 @@
import { Box, Spinner } from '@chakra-ui/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { memo, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExclamation, FaImage } from 'react-icons/fa';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import BatchImage from 'features/batch/components/BatchImage';
import { VirtuosoGrid } from 'react-virtuoso';
import { useGetAllBoardImagesForBoardQuery } from 'services/api/endpoints/boardImages';
import ItemContainer from './ItemContainer';
import ListContainer from './ListContainer';
const selector = createSelector(
[stateSelector],
(state) => {
return {
imageNames: state.batch.imageNames,
};
},
defaultSelectorOptions
);
type BoardGridProps = {
board_id: string;
};
const BoardGrid = (props: BoardGridProps) => {
const { board_id } = props;
const { data, isLoading, isError, isSuccess } =
useGetAllBoardImagesForBoardQuery({
board_id,
});
const { t } = useTranslation();
const rootRef = useRef(null);
const [scroller, setScroller] = useState<HTMLElement | null>(null);
const [initialize, osInstance] = useOverlayScrollbars({
defer: true,
options: {
scrollbars: {
visibility: 'auto',
autoHide: 'leave',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
overflow: { x: 'hidden' },
},
});
useEffect(() => {
const { current: root } = rootRef;
if (scroller && root) {
initialize({
target: root,
elements: {
viewport: scroller,
},
});
}
return () => osInstance()?.destroy();
}, [scroller, initialize, osInstance]);
if (isLoading) {
return <Spinner />;
}
if (isError) {
return <FaExclamation />;
}
if (isSuccess && data.image_names) {
return (
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
<VirtuosoGrid
style={{ height: '100%' }}
data={data.image_names}
components={{
Item: ItemContainer,
List: ListContainer,
}}
scrollerRef={setScroller}
itemContent={(index, imageName) => (
<BatchImage key={imageName} imageName={imageName} />
)}
/>
</Box>
);
}
return (
<IAINoContentFallback
label={t('gallery.noImagesInGallery')}
icon={FaImage}
/>
);
};
export default memo(BoardGrid);

View File

@ -1,4 +1,4 @@
import { Box } from '@chakra-ui/react'; import { Box, Spinner } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd'; import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
@ -7,8 +7,12 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice'; import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
import { MouseEvent, memo, useCallback, useMemo } from 'react'; import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { ImageDTO } from 'services/api/types'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { imageSelected } from '../store/gallerySlice'; import {
imageRangeEndSelected,
imageSelected,
imageSelectionToggled,
} from '../store/gallerySlice';
import ImageContextMenu from './ImageContextMenu'; import ImageContextMenu from './ImageContextMenu';
export const makeSelector = (image_name: string) => export const makeSelector = (image_name: string) =>
@ -23,33 +27,29 @@ export const makeSelector = (image_name: string) =>
); );
interface HoverableImageProps { interface HoverableImageProps {
imageDTO: ImageDTO; imageName: string;
} }
const GalleryImage = (props: HoverableImageProps) => { const GalleryImage = (props: HoverableImageProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { imageName } = props;
const { imageDTO } = props; const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
const { image_name } = imageDTO; const localSelector = useMemo(() => makeSelector(imageName), [imageName]);
const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
const { isSelected, selectionCount, selection } = const { isSelected, selectionCount, selection } =
useAppSelector(localSelector); useAppSelector(localSelector);
const handleClick = useCallback( const handleClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => { (e: MouseEvent<HTMLDivElement>) => {
// multiselect disabled for now if (e.shiftKey) {
// if (e.shiftKey) { dispatch(imageRangeEndSelected(imageName));
// dispatch(imageRangeEndSelected(props.imageDTO.image_name)); } else if (e.ctrlKey || e.metaKey) {
// } else if (e.ctrlKey || e.metaKey) { dispatch(imageSelectionToggled(imageName));
// dispatch(imageSelectionToggled(props.imageDTO.image_name)); } else {
// } else { dispatch(imageSelected(imageName));
// dispatch(imageSelected(props.imageDTO.image_name)); }
// }
dispatch(imageSelected(props.imageDTO.image_name));
}, },
[dispatch, props.imageDTO.image_name] [dispatch, imageName]
); );
const handleDelete = useCallback( const handleDelete = useCallback(
@ -81,13 +81,17 @@ const GalleryImage = (props: HoverableImageProps) => {
} }
}, [imageDTO, selection, selectionCount]); }, [imageDTO, selection, selectionCount]);
if (!imageDTO) {
return <Spinner />;
}
return ( return (
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}> <Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
<ImageContextMenu imageDTO={imageDTO}> <ImageContextMenu imageDTO={imageDTO}>
{(ref) => ( {(ref) => (
<Box <Box
position="relative" position="relative"
key={image_name} key={imageName}
userSelect="none" userSelect="none"
ref={ref} ref={ref}
sx={{ sx={{

View File

@ -2,7 +2,6 @@ import { ExternalLinkIcon } from '@chakra-ui/icons';
import { MenuItem, MenuList } from '@chakra-ui/react'; import { MenuItem, MenuList } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { selectionAddedToBatch } from 'app/store/middleware/listenerMiddleware/listeners/selectionAddedToBatch';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
@ -10,6 +9,7 @@ import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import { import {
imageAddedToBatch, imageAddedToBatch,
imageRemovedFromBatch, imageRemovedFromBatch,
imagesAddedToBatch,
} from 'features/batch/store/batchSlice'; } from 'features/batch/store/batchSlice';
import { import {
resizeAndScaleCanvas, resizeAndScaleCanvas,
@ -158,7 +158,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
}, [imageDTO.image_url]); }, [imageDTO.image_url]);
const handleAddSelectionToBatch = useCallback(() => { const handleAddSelectionToBatch = useCallback(() => {
dispatch(selectionAddedToBatch({ images_names: selection })); dispatch(imagesAddedToBatch(selection));
}, [dispatch, selection]); }, [dispatch, selection]);
const handleAddToBatch = useCallback(() => { const handleAddToBatch = useCallback(() => {

View File

@ -38,6 +38,7 @@ import {
import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
import BatchGrid from './BatchGrid'; import BatchGrid from './BatchGrid';
import BoardGrid from './BoardGrid';
import BoardsList from './Boards/BoardsList'; import BoardsList from './Boards/BoardsList';
import ImageGalleryGrid from './ImageGalleryGrid'; import ImageGalleryGrid from './ImageGalleryGrid';
@ -228,7 +229,13 @@ const ImageGalleryContent = () => {
</Box> </Box>
</Box> </Box>
<Flex direction="column" gap={2} h="full" w="full"> <Flex direction="column" gap={2} h="full" w="full">
{selectedBoardId === 'batch' ? <BatchGrid /> : <ImageGalleryGrid />} {selectedBoardId === 'batch' ? (
<BatchGrid />
) : selectedBoardId ? (
<BoardGrid board_id={selectedBoardId} />
) : (
<ImageGalleryGrid />
)}
</Flex> </Flex>
</VStack> </VStack>
); );

View File

@ -160,7 +160,7 @@ const ImageGalleryGrid = () => {
) : ( ) : (
<GalleryImage <GalleryImage
key={`${item.image_name}-${item.thumbnail_url}`} key={`${item.image_name}-${item.thumbnail_url}`}
imageDTO={item} imageName={item.image_name}
/> />
) )
} }