mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into add-app-version
This commit is contained in:
commit
b27bf7bb0c
@ -1,5 +1,6 @@
|
||||
import { Flex, Grid, Portal } from '@chakra-ui/react';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { PartialAppConfig } from 'app/types/invokeai';
|
||||
import ImageUploader from 'common/components/ImageUploader';
|
||||
@ -46,6 +47,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
dispatch(configChanged(config));
|
||||
}, [dispatch, config, log]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(appStarted());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
|
@ -55,6 +55,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
||||
}
|
||||
|
||||
if (props.dragData.payloadType === 'IMAGE_DTO') {
|
||||
const { thumbnail_url, width, height } = props.dragData.payload.imageDTO;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -72,7 +73,10 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
||||
sx={{
|
||||
...STYLES,
|
||||
}}
|
||||
src={props.dragData.payload.imageDTO.thumbnail_url}
|
||||
objectFit="contain"
|
||||
src={thumbnail_url}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,49 +1,67 @@
|
||||
import type { TypedAddListener, TypedStartListening } from '@reduxjs/toolkit';
|
||||
import {
|
||||
createListenerMiddleware,
|
||||
addListener,
|
||||
ListenerEffect,
|
||||
AnyAction,
|
||||
ListenerEffect,
|
||||
addListener,
|
||||
createListenerMiddleware,
|
||||
} from '@reduxjs/toolkit';
|
||||
import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit';
|
||||
|
||||
import type { RootState, AppDispatch } from '../../store';
|
||||
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
||||
import type { AppDispatch, RootState } from '../../store';
|
||||
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
||||
import { addAppStartedListener } from './listeners/appStarted';
|
||||
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
|
||||
import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
|
||||
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
|
||||
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
|
||||
import { addCanvasMergedListener } from './listeners/canvasMerged';
|
||||
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
|
||||
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||
import {
|
||||
addImageUploadedFulfilledListener,
|
||||
addImageUploadedRejectedListener,
|
||||
} from './listeners/imageUploaded';
|
||||
addImageAddedToBoardFulfilledListener,
|
||||
addImageAddedToBoardRejectedListener,
|
||||
} from './listeners/imageAddedToBoard';
|
||||
import {
|
||||
addImageDeletedFulfilledListener,
|
||||
addImageDeletedPendingListener,
|
||||
addImageDeletedRejectedListener,
|
||||
addRequestedImageDeletionListener,
|
||||
} from './listeners/imageDeleted';
|
||||
import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
|
||||
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
|
||||
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
|
||||
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
|
||||
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
|
||||
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
|
||||
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
|
||||
import { addCanvasMergedListener } from './listeners/canvasMerged';
|
||||
import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress';
|
||||
import { addGraphExecutionStateCompleteEventListener as addGraphExecutionStateCompleteListener } from './listeners/socketio/socketGraphExecutionStateComplete';
|
||||
import { addInvocationCompleteEventListener as addInvocationCompleteListener } from './listeners/socketio/socketInvocationComplete';
|
||||
import { addInvocationErrorEventListener as addInvocationErrorListener } from './listeners/socketio/socketInvocationError';
|
||||
import { addInvocationStartedEventListener as addInvocationStartedListener } from './listeners/socketio/socketInvocationStarted';
|
||||
import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected';
|
||||
import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected';
|
||||
import { addSocketSubscribedEventListener as addSocketSubscribedListener } from './listeners/socketio/socketSubscribed';
|
||||
import { addSocketUnsubscribedEventListener as addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed';
|
||||
import { addSessionReadyToInvokeListener } from './listeners/sessionReadyToInvoke';
|
||||
import { addImageDroppedListener } from './listeners/imageDropped';
|
||||
import {
|
||||
addImageMetadataReceivedFulfilledListener,
|
||||
addImageMetadataReceivedRejectedListener,
|
||||
} from './listeners/imageMetadataReceived';
|
||||
import {
|
||||
addImageRemovedFromBoardFulfilledListener,
|
||||
addImageRemovedFromBoardRejectedListener,
|
||||
} from './listeners/imageRemovedFromBoard';
|
||||
import { addImageToDeleteSelectedListener } from './listeners/imageToDeleteSelected';
|
||||
import {
|
||||
addImageUpdatedFulfilledListener,
|
||||
addImageUpdatedRejectedListener,
|
||||
} from './listeners/imageUpdated';
|
||||
import {
|
||||
addImageUploadedFulfilledListener,
|
||||
addImageUploadedRejectedListener,
|
||||
} from './listeners/imageUploaded';
|
||||
import {
|
||||
addImageUrlsReceivedFulfilledListener,
|
||||
addImageUrlsReceivedRejectedListener,
|
||||
} from './listeners/imageUrlsReceived';
|
||||
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
||||
import { addModelSelectedListener } from './listeners/modelSelected';
|
||||
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
||||
import {
|
||||
addReceivedPageOfImagesFulfilledListener,
|
||||
addReceivedPageOfImagesRejectedListener,
|
||||
} from './listeners/receivedPageOfImages';
|
||||
import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch';
|
||||
import {
|
||||
addSessionCanceledFulfilledListener,
|
||||
addSessionCanceledPendingListener,
|
||||
addSessionCanceledRejectedListener,
|
||||
} from './listeners/sessionCanceled';
|
||||
import {
|
||||
addSessionCreatedFulfilledListener,
|
||||
addSessionCreatedPendingListener,
|
||||
@ -54,39 +72,21 @@ import {
|
||||
addSessionInvokedPendingListener,
|
||||
addSessionInvokedRejectedListener,
|
||||
} from './listeners/sessionInvoked';
|
||||
import {
|
||||
addSessionCanceledFulfilledListener,
|
||||
addSessionCanceledPendingListener,
|
||||
addSessionCanceledRejectedListener,
|
||||
} from './listeners/sessionCanceled';
|
||||
import {
|
||||
addImageUpdatedFulfilledListener,
|
||||
addImageUpdatedRejectedListener,
|
||||
} from './listeners/imageUpdated';
|
||||
import {
|
||||
addReceivedPageOfImagesFulfilledListener,
|
||||
addReceivedPageOfImagesRejectedListener,
|
||||
} from './listeners/receivedPageOfImages';
|
||||
import { addSessionReadyToInvokeListener } from './listeners/sessionReadyToInvoke';
|
||||
import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected';
|
||||
import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected';
|
||||
import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress';
|
||||
import { addGraphExecutionStateCompleteEventListener as addGraphExecutionStateCompleteListener } from './listeners/socketio/socketGraphExecutionStateComplete';
|
||||
import { addInvocationCompleteEventListener as addInvocationCompleteListener } from './listeners/socketio/socketInvocationComplete';
|
||||
import { addInvocationErrorEventListener as addInvocationErrorListener } from './listeners/socketio/socketInvocationError';
|
||||
import { addInvocationStartedEventListener as addInvocationStartedListener } from './listeners/socketio/socketInvocationStarted';
|
||||
import { addSocketSubscribedEventListener as addSocketSubscribedListener } from './listeners/socketio/socketSubscribed';
|
||||
import { addSocketUnsubscribedEventListener as addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed';
|
||||
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
|
||||
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
||||
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
|
||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
||||
import {
|
||||
addImageAddedToBoardFulfilledListener,
|
||||
addImageAddedToBoardRejectedListener,
|
||||
} from './listeners/imageAddedToBoard';
|
||||
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
|
||||
import {
|
||||
addImageRemovedFromBoardFulfilledListener,
|
||||
addImageRemovedFromBoardRejectedListener,
|
||||
} from './listeners/imageRemovedFromBoard';
|
||||
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
||||
import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
|
||||
import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch';
|
||||
import { addImageDroppedListener } from './listeners/imageDropped';
|
||||
import { addImageToDeleteSelectedListener } from './listeners/imageToDeleteSelected';
|
||||
import { addModelSelectedListener } from './listeners/modelSelected';
|
||||
import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
|
||||
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
|
||||
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
|
||||
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -196,9 +196,6 @@ addSessionCanceledRejectedListener();
|
||||
addReceivedPageOfImagesFulfilledListener();
|
||||
addReceivedPageOfImagesRejectedListener();
|
||||
|
||||
// Gallery
|
||||
addImageCategoriesChangedListener();
|
||||
|
||||
// ControlNet
|
||||
addControlNetImageProcessedListener();
|
||||
addControlNetAutoProcessListener();
|
||||
@ -224,3 +221,6 @@ addImageDroppedListener();
|
||||
|
||||
// Models
|
||||
addModelSelectedListener();
|
||||
|
||||
// app startup
|
||||
addAppStartedListener();
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import {
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
isLoadingChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
export const appStarted = createAction('app/appStarted');
|
||||
|
||||
export const addAppStartedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: appStarted,
|
||||
effect: async (
|
||||
action,
|
||||
{ getState, dispatch, unsubscribe, cancelActiveListeners }
|
||||
) => {
|
||||
cancelActiveListeners();
|
||||
unsubscribe();
|
||||
// fill up the gallery tab with images
|
||||
await dispatch(
|
||||
receivedPageOfImages({
|
||||
categories: ['general'],
|
||||
is_intermediate: false,
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
// fill up the assets tab with images
|
||||
await dispatch(
|
||||
receivedPageOfImages({
|
||||
categories: ['control', 'mask', 'user', 'other'],
|
||||
is_intermediate: false,
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(isLoadingChanged(false));
|
||||
},
|
||||
});
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import {
|
||||
imageCategoriesChanged,
|
||||
selectFilteredImages,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'gallery' });
|
||||
|
||||
export const addImageCategoriesChangedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageCategoriesChanged,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const state = getState();
|
||||
const filteredImagesCount = selectFilteredImages(state).length;
|
||||
|
||||
if (!filteredImagesCount) {
|
||||
dispatch(
|
||||
receivedPageOfImages({
|
||||
categories: action.payload,
|
||||
board_id: state.boards.selectedBoardId,
|
||||
is_intermediate: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { Box, Flex, SystemStyleObject } from '@chakra-ui/react';
|
||||
import { Box, Flex, Spinner, SystemStyleObject } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import {
|
||||
@ -8,7 +8,6 @@ import {
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { IAILoadingImageFallback } from 'common/components/IAIImageFallback';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { PostUploadAction } from 'services/api/thunks/image';
|
||||
@ -147,18 +146,28 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
/>
|
||||
</Box>
|
||||
{pendingControlImages.includes(controlNetId) && (
|
||||
<Box
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
objectFit: 'contain',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: 0.8,
|
||||
borderRadius: 'base',
|
||||
bg: 'base.400',
|
||||
_dark: {
|
||||
bg: 'base.900',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IAILoadingImageFallback image={controlImage} />
|
||||
</Box>
|
||||
<Spinner
|
||||
size="xl"
|
||||
sx={{ color: 'base.100', _dark: { color: 'base.400' } }}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
@ -3,72 +3,47 @@ import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Grid,
|
||||
Skeleton,
|
||||
Text,
|
||||
VStack,
|
||||
forwardRef,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import {
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryView,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
|
||||
import {
|
||||
ChangeEvent,
|
||||
PropsWithChildren,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ChangeEvent, memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { FaImage, FaServer, FaWrench } from 'react-icons/fa';
|
||||
import GalleryImage from './GalleryImage';
|
||||
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState, stateSelector } from 'app/store/store';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
imageCategoriesChanged,
|
||||
shouldAutoSwitchChanged,
|
||||
selectFilteredImages,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import BoardsList from './Boards/BoardsList';
|
||||
import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
|
||||
const LOADING_IMAGE_ARRAY = Array(20).fill('loading');
|
||||
import BoardsList from './Boards/BoardsList';
|
||||
import ImageGalleryGrid from './ImageGalleryGrid';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector, selectFilteredImages],
|
||||
(state, filteredImages) => {
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const {
|
||||
categories,
|
||||
total: allImagesTotal,
|
||||
isLoading,
|
||||
selectedBoardId,
|
||||
galleryImageMinimumWidth,
|
||||
galleryView,
|
||||
@ -76,13 +51,7 @@ const selector = createSelector(
|
||||
} = state.gallery;
|
||||
const { shouldPinGallery } = state.ui;
|
||||
|
||||
const images = filteredImages as (ImageDTO | string)[];
|
||||
|
||||
return {
|
||||
images: isLoading ? images.concat(LOADING_IMAGE_ARRAY) : images,
|
||||
allImagesTotal,
|
||||
isLoading,
|
||||
categories,
|
||||
selectedBoardId,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
@ -97,28 +66,10 @@ const ImageGalleryContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||
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' },
|
||||
},
|
||||
});
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const {
|
||||
images,
|
||||
isLoading,
|
||||
allImagesTotal,
|
||||
categories,
|
||||
selectedBoardId,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
@ -132,32 +83,6 @@ const ImageGalleryContent = () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const filteredImagesTotal = useMemo(
|
||||
() => selectedBoard?.image_count ?? allImagesTotal,
|
||||
[allImagesTotal, selectedBoard?.image_count]
|
||||
);
|
||||
|
||||
const areMoreAvailable = useMemo(() => {
|
||||
return images.length < filteredImagesTotal;
|
||||
}, [images.length, filteredImagesTotal]);
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
dispatch(
|
||||
receivedPageOfImages({
|
||||
categories,
|
||||
board_id: selectedBoardId,
|
||||
is_intermediate: false,
|
||||
})
|
||||
);
|
||||
}, [categories, dispatch, selectedBoardId]);
|
||||
|
||||
const handleEndReached = useMemo(() => {
|
||||
if (areMoreAvailable && !isLoading) {
|
||||
return handleLoadMoreImages;
|
||||
}
|
||||
return undefined;
|
||||
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
|
||||
|
||||
const { isOpen: isBoardListOpen, onToggle } = useDisclosure();
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||
@ -169,28 +94,6 @@ const ImageGalleryContent = () => {
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
target: root,
|
||||
elements: {
|
||||
viewport: scroller,
|
||||
},
|
||||
});
|
||||
}
|
||||
return () => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
receivedPageOfImages({
|
||||
categories: ['general'],
|
||||
is_intermediate: false,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickImagesCategory = useCallback(() => {
|
||||
dispatch(imageCategoriesChanged(IMAGE_CATEGORIES));
|
||||
dispatch(setGalleryView('images'));
|
||||
@ -314,80 +217,10 @@ const ImageGalleryContent = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex direction="column" gap={2} h="full" w="full">
|
||||
{images.length || areMoreAvailable ? (
|
||||
<>
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
data={images}
|
||||
endReached={handleEndReached}
|
||||
components={{
|
||||
Item: ItemContainer,
|
||||
List: ListContainer,
|
||||
}}
|
||||
scrollerRef={setScroller}
|
||||
itemContent={(index, item) =>
|
||||
typeof item === 'string' ? (
|
||||
<Skeleton
|
||||
sx={{ w: 'full', h: 'full', aspectRatio: '1/1' }}
|
||||
/>
|
||||
) : (
|
||||
<GalleryImage
|
||||
key={`${item.image_name}-${item.thumbnail_url}`}
|
||||
imageDTO={item}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<IAIButton
|
||||
onClick={handleLoadMoreImages}
|
||||
isDisabled={!areMoreAvailable}
|
||||
isLoading={isLoading}
|
||||
loadingText="Loading"
|
||||
flexShrink={0}
|
||||
>
|
||||
{areMoreAvailable
|
||||
? t('gallery.loadMore')
|
||||
: t('gallery.allImagesLoaded')}
|
||||
</IAIButton>
|
||||
</>
|
||||
) : (
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
)}
|
||||
<ImageGalleryGrid />
|
||||
</Flex>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
<Box className="item-container" ref={ref} p={1.5}>
|
||||
{props.children}
|
||||
</Box>
|
||||
));
|
||||
|
||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
const galleryImageMinimumWidth = useAppSelector(
|
||||
(state: RootState) => state.gallery.galleryImageMinimumWidth
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
className="list-container"
|
||||
ref={ref}
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr));`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(ImageGalleryContent);
|
||||
|
@ -0,0 +1,226 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Grid,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
forwardRef,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { IMAGE_LIMIT } from 'features/gallery/store/gallerySlice';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
|
||||
import {
|
||||
PropsWithChildren,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
import GalleryImage from './GalleryImage';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState, stateSelector } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { selectFilteredImages } from 'features/gallery/store/gallerySlice';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector, selectFilteredImages],
|
||||
(state, filteredImages) => {
|
||||
const {
|
||||
categories,
|
||||
total: allImagesTotal,
|
||||
isLoading,
|
||||
isFetching,
|
||||
selectedBoardId,
|
||||
} = state.gallery;
|
||||
|
||||
let images = filteredImages as (ImageDTO | 'loading')[];
|
||||
|
||||
if (!isLoading && isFetching) {
|
||||
// loading, not not the initial load
|
||||
images = images.concat(Array(IMAGE_LIMIT).fill('loading'));
|
||||
}
|
||||
|
||||
return {
|
||||
images,
|
||||
allImagesTotal,
|
||||
isLoading,
|
||||
categories,
|
||||
selectedBoardId,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const ImageGalleryGrid = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
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' },
|
||||
},
|
||||
});
|
||||
|
||||
const { images, isLoading, allImagesTotal, categories, selectedBoardId } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const { selectedBoard } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => ({
|
||||
selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
|
||||
}),
|
||||
});
|
||||
|
||||
const filteredImagesTotal = useMemo(
|
||||
() => selectedBoard?.image_count ?? allImagesTotal,
|
||||
[allImagesTotal, selectedBoard?.image_count]
|
||||
);
|
||||
|
||||
const areMoreAvailable = useMemo(() => {
|
||||
return images.length < filteredImagesTotal;
|
||||
}, [images.length, filteredImagesTotal]);
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
dispatch(
|
||||
receivedPageOfImages({
|
||||
categories,
|
||||
board_id: selectedBoardId,
|
||||
is_intermediate: false,
|
||||
})
|
||||
);
|
||||
}, [categories, dispatch, selectedBoardId]);
|
||||
|
||||
const handleEndReached = useMemo(() => {
|
||||
if (areMoreAvailable && !isLoading) {
|
||||
return handleLoadMoreImages;
|
||||
}
|
||||
return undefined;
|
||||
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
target: root,
|
||||
elements: {
|
||||
viewport: scroller,
|
||||
},
|
||||
});
|
||||
}
|
||||
return () => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Spinner
|
||||
size="xl"
|
||||
sx={{ color: 'base.300', _dark: { color: 'base.700' } }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (images.length) {
|
||||
return (
|
||||
<>
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
data={images}
|
||||
endReached={handleEndReached}
|
||||
components={{
|
||||
Item: ItemContainer,
|
||||
List: ListContainer,
|
||||
}}
|
||||
scrollerRef={setScroller}
|
||||
itemContent={(index, item) =>
|
||||
typeof item === 'string' ? (
|
||||
<Skeleton sx={{ w: 'full', h: 'full', aspectRatio: '1/1' }} />
|
||||
) : (
|
||||
<GalleryImage
|
||||
key={`${item.image_name}-${item.thumbnail_url}`}
|
||||
imageDTO={item}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<IAIButton
|
||||
onClick={handleLoadMoreImages}
|
||||
isDisabled={!areMoreAvailable}
|
||||
isLoading={isLoading}
|
||||
loadingText="Loading"
|
||||
flexShrink={0}
|
||||
>
|
||||
{areMoreAvailable
|
||||
? t('gallery.loadMore')
|
||||
: t('gallery.allImagesLoaded')}
|
||||
</IAIButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
<Box className="item-container" ref={ref} p={1.5}>
|
||||
{props.children}
|
||||
</Box>
|
||||
));
|
||||
|
||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
const galleryImageMinimumWidth = useAppSelector(
|
||||
(state: RootState) => state.gallery.galleryImageMinimumWidth
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
className="list-container"
|
||||
ref={ref}
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr));`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(ImageGalleryGrid);
|
@ -11,5 +11,8 @@ export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [
|
||||
'limit',
|
||||
'offset',
|
||||
'selectedBoardId',
|
||||
'categories',
|
||||
'galleryView',
|
||||
'total',
|
||||
'isInitialized',
|
||||
];
|
||||
|
@ -28,17 +28,22 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
||||
'other',
|
||||
];
|
||||
|
||||
export const INITIAL_IMAGE_LIMIT = 100;
|
||||
export const IMAGE_LIMIT = 20;
|
||||
|
||||
type AdditionaGalleryState = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
categories: ImageCategory[];
|
||||
selectedBoardId?: string;
|
||||
selection: string[];
|
||||
shouldAutoSwitch: boolean;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryView: 'images' | 'assets' | 'boards';
|
||||
galleryView: 'images' | 'assets';
|
||||
isInitialized: boolean;
|
||||
};
|
||||
|
||||
export const initialGalleryState =
|
||||
@ -47,11 +52,13 @@ export const initialGalleryState =
|
||||
limit: 0,
|
||||
total: 0,
|
||||
isLoading: true,
|
||||
isFetching: true,
|
||||
categories: IMAGE_CATEGORIES,
|
||||
selection: [],
|
||||
shouldAutoSwitch: true,
|
||||
galleryImageMinimumWidth: 64,
|
||||
galleryImageMinimumWidth: 96,
|
||||
galleryView: 'images',
|
||||
isInitialized: false,
|
||||
});
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@ -65,6 +72,8 @@ export const gallerySlice = createSlice({
|
||||
action.payload.image_category === 'general'
|
||||
) {
|
||||
state.selection = [action.payload.image_name];
|
||||
state.galleryView = 'images';
|
||||
state.categories = IMAGE_CATEGORIES;
|
||||
}
|
||||
},
|
||||
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
|
||||
@ -128,38 +137,33 @@ export const gallerySlice = createSlice({
|
||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryImageMinimumWidth = action.payload;
|
||||
},
|
||||
setGalleryView: (
|
||||
state,
|
||||
action: PayloadAction<'images' | 'assets' | 'boards'>
|
||||
) => {
|
||||
setGalleryView: (state, action: PayloadAction<'images' | 'assets'>) => {
|
||||
state.galleryView = action.payload;
|
||||
},
|
||||
boardIdSelected: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.selectedBoardId = action.payload;
|
||||
},
|
||||
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedPageOfImages.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.isFetching = true;
|
||||
});
|
||||
builder.addCase(receivedPageOfImages.rejected, (state) => {
|
||||
state.isLoading = false;
|
||||
state.isFetching = false;
|
||||
});
|
||||
builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.isFetching = false;
|
||||
const { board_id, categories, image_origin, is_intermediate } =
|
||||
action.meta.arg;
|
||||
|
||||
const { items, offset, limit, total } = action.payload;
|
||||
|
||||
const transformedItems = items.map((item) => ({
|
||||
...item,
|
||||
isSelected: false,
|
||||
}));
|
||||
imagesAdapter.upsertMany(state, items);
|
||||
|
||||
imagesAdapter.upsertMany(state, transformedItems);
|
||||
|
||||
if (state.selection.length === 0) {
|
||||
if (state.selection.length === 0 && items.length) {
|
||||
state.selection = [items[0].image_name];
|
||||
}
|
||||
|
||||
@ -170,7 +174,6 @@ export const gallerySlice = createSlice({
|
||||
}
|
||||
|
||||
state.offset = offset;
|
||||
state.limit = limit;
|
||||
state.total = total;
|
||||
});
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
@ -213,6 +216,7 @@ export const {
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryView,
|
||||
boardIdSelected,
|
||||
isLoadingChanged,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
Loading…
Reference in New Issue
Block a user