mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip boards via rtk-query
This commit is contained in:
parent
661a94b3de
commit
cfda128e06
@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { imageAddedToBoard } from '../../services/thunks/board';
|
||||
import { useAddImageToBoardMutation } from 'services/apiSlice';
|
||||
|
||||
export type ImageUsage = {
|
||||
isInitialImage: boolean;
|
||||
@ -43,6 +44,8 @@ export const AddImageToBoardContextProvider = (props: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const [addImageToBoard, result] = useAddImageToBoardMutation();
|
||||
|
||||
// Clean up after deleting or dismissing the modal
|
||||
const closeAndClearImageToDelete = useCallback(() => {
|
||||
setImageToMove(undefined);
|
||||
@ -63,18 +66,14 @@ export const AddImageToBoardContextProvider = (props: Props) => {
|
||||
const handleAddToBoard = useCallback(
|
||||
(boardId: string) => {
|
||||
if (imageToMove) {
|
||||
dispatch(
|
||||
imageAddedToBoard({
|
||||
requestBody: {
|
||||
board_id: boardId,
|
||||
image_name: imageToMove.image_name,
|
||||
},
|
||||
})
|
||||
);
|
||||
addImageToBoard({
|
||||
board_id: boardId,
|
||||
image_name: imageToMove.image_name,
|
||||
});
|
||||
closeAndClearImageToDelete();
|
||||
}
|
||||
},
|
||||
[closeAndClearImageToDelete, dispatch, imageToMove]
|
||||
[addImageToBoard, closeAndClearImageToDelete, imageToMove]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -73,6 +73,10 @@ import { addImageCategoriesChangedListener } from './listeners/imageCategoriesCh
|
||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
||||
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
|
||||
import {
|
||||
addImageAddedToBoardFulfilledListener,
|
||||
addImageAddedToBoardRejectedListener,
|
||||
} from './listeners/imageAddedToBoard';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -183,3 +187,7 @@ addControlNetAutoProcessListener();
|
||||
|
||||
// Update image URLs on connect
|
||||
addUpdateImageUrlsOnConnectListener();
|
||||
|
||||
// Boards
|
||||
addImageAddedToBoardFulfilledListener();
|
||||
addImageAddedToBoardRejectedListener();
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/thunks/image';
|
||||
import { api } from 'services/apiSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
export const addImageAddedToBoardFulfilledListener = () => {
|
||||
startAppListening({
|
||||
matcher: api.endpoints.addImageToBoard.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Image added to board'
|
||||
);
|
||||
|
||||
dispatch(
|
||||
imageMetadataReceived({
|
||||
imageName: image_name,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addImageAddedToBoardRejectedListener = () => {
|
||||
startAppListening({
|
||||
matcher: api.endpoints.addImageToBoard.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Problem adding image to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -13,6 +13,7 @@ import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { api } from 'services/apiSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||
|
||||
@ -22,7 +23,7 @@ const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||
export const addRequestedImageDeletionListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: requestedImageDeletion,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
effect: async (action, { dispatch, getState, condition }) => {
|
||||
const { image, imageUsage } = action.payload;
|
||||
|
||||
const { image_name } = image;
|
||||
@ -30,7 +31,7 @@ export const addRequestedImageDeletionListener = () => {
|
||||
const state = getState();
|
||||
const selectedImage = state.gallery.selectedImage;
|
||||
|
||||
if (selectedImage && selectedImage.image_name === image_name) {
|
||||
if (selectedImage && selectedImage === image_name) {
|
||||
const ids = selectImagesIds(state);
|
||||
const entities = selectImagesEntities(state);
|
||||
|
||||
@ -51,7 +52,7 @@ export const addRequestedImageDeletionListener = () => {
|
||||
const newSelectedImage = entities[newSelectedImageId];
|
||||
|
||||
if (newSelectedImageId) {
|
||||
dispatch(imageSelected(newSelectedImage));
|
||||
dispatch(imageSelected(newSelectedImageId));
|
||||
} else {
|
||||
dispatch(imageSelected());
|
||||
}
|
||||
@ -79,7 +80,19 @@ export const addRequestedImageDeletionListener = () => {
|
||||
dispatch(imageRemoved(image_name));
|
||||
|
||||
// Delete from server
|
||||
dispatch(imageDeleted({ imageName: image_name }));
|
||||
const { requestId } = dispatch(imageDeleted({ imageName: image_name }));
|
||||
|
||||
// Wait for successful deletion, then trigger boards to re-fetch
|
||||
const wasImageDeleted = await condition(
|
||||
(action) => action.meta.requestId === requestId,
|
||||
30000
|
||||
);
|
||||
|
||||
if (wasImageDeleted) {
|
||||
dispatch(
|
||||
api.util.invalidateTags([{ type: 'Board', id: image.board_id }])
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -33,6 +33,7 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
|
||||
import { serialize } from './enhancers/reduxRemember/serialize';
|
||||
import { unserialize } from './enhancers/reduxRemember/unserialize';
|
||||
import { LOCALSTORAGE_PREFIX } from './constants';
|
||||
import { api } from 'services/apiSlice';
|
||||
|
||||
const allReducers = {
|
||||
canvas: canvasReducer,
|
||||
@ -49,6 +50,7 @@ const allReducers = {
|
||||
images: imagesReducer,
|
||||
controlNet: controlNetReducer,
|
||||
boards: boardsReducer,
|
||||
[api.reducerPath]: api.reducer,
|
||||
// session: sessionReducer,
|
||||
};
|
||||
|
||||
@ -87,6 +89,7 @@ export const store = configureStore({
|
||||
immutableCheck: false,
|
||||
serializableCheck: false,
|
||||
})
|
||||
.concat(api.middleware)
|
||||
.concat(dynamicMiddlewares)
|
||||
.prepend(listenerMiddleware.middleware),
|
||||
devTools: {
|
||||
|
@ -1,19 +1,20 @@
|
||||
import { Flex, Icon, Text } from '@chakra-ui/react';
|
||||
import { Flex, Icon, Spinner, Text } from '@chakra-ui/react';
|
||||
import { useCallback } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { useAppDispatch } from '../../../../app/store/storeHooks';
|
||||
import { boardCreated } from '../../../../services/thunks/board';
|
||||
import { useCreateBoardMutation } from 'services/apiSlice';
|
||||
|
||||
const DEFAULT_BOARD_NAME = 'My Board';
|
||||
|
||||
const AddBoardButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [createBoard, { isLoading }] = useCreateBoardMutation();
|
||||
|
||||
const handleCreateBoard = useCallback(() => {
|
||||
dispatch(boardCreated({ requestBody: 'My Board' }));
|
||||
}, [dispatch]);
|
||||
createBoard(DEFAULT_BOARD_NAME);
|
||||
}, [createBoard]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onClick={handleCreateBoard}
|
||||
onClick={isLoading ? undefined : handleCreateBoard}
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
@ -36,7 +37,11 @@ const AddBoardButton = () => {
|
||||
aspectRatio: '1/1',
|
||||
}}
|
||||
>
|
||||
<Icon boxSize={8} color="base.700" as={FaPlus} />
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon boxSize={8} color="base.700" as={FaPlus} />
|
||||
)}
|
||||
</Flex>
|
||||
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text>
|
||||
</Flex>
|
||||
|
@ -25,6 +25,7 @@ import { searchBoardsSelector } from '../../store/boardSelectors';
|
||||
import { useSelector } from 'react-redux';
|
||||
import IAICollapse from '../../../../common/components/IAICollapse';
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { useListBoardsQuery } from 'services/apiSlice';
|
||||
|
||||
const selector = createSelector(
|
||||
[selectBoardsAll, boardsSelector],
|
||||
@ -40,9 +41,17 @@ const selector = createSelector(
|
||||
const BoardsList = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { selectedBoard, searchText } = useAppSelector(selector);
|
||||
const filteredBoards = useSelector(searchBoardsSelector);
|
||||
// const filteredBoards = useSelector(searchBoardsSelector);
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
|
||||
const { data } = useListBoardsQuery({ offset: 0, limit: 8 });
|
||||
|
||||
const filteredBoards = searchText
|
||||
? data?.items.filter((board) =>
|
||||
board.board_name.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
: data.items;
|
||||
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
|
||||
const handleBoardSearch = (searchTerm: string) => {
|
||||
@ -100,13 +109,14 @@ const BoardsList = () => {
|
||||
<AllImagesBoard isSelected={!selectedBoard} />
|
||||
</>
|
||||
)}
|
||||
{filteredBoards.map((board) => (
|
||||
<HoverableBoard
|
||||
key={board.board_id}
|
||||
board={board}
|
||||
isSelected={selectedBoard?.board_id === board.board_id}
|
||||
/>
|
||||
))}
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board) => (
|
||||
<HoverableBoard
|
||||
key={board.board_id}
|
||||
board={board}
|
||||
isSelected={selectedBoard?.board_id === board.board_id}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
</>
|
||||
|
@ -26,6 +26,10 @@ import IAIDndImage from '../../../../common/components/IAIDndImage';
|
||||
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../../../app/store/store';
|
||||
import {
|
||||
useDeleteBoardMutation,
|
||||
useUpdateBoardMutation,
|
||||
} from 'services/apiSlice';
|
||||
|
||||
const coverImageSelector = (imageName: string | undefined) =>
|
||||
createSelector(
|
||||
@ -59,19 +63,20 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
dispatch(boardIdSelected(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
|
||||
const handleDeleteBoard = useCallback(() => {
|
||||
dispatch(boardDeleted(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
||||
useUpdateBoardMutation();
|
||||
|
||||
const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
|
||||
useDeleteBoardMutation();
|
||||
|
||||
const handleUpdateBoardName = (newBoardName: string) => {
|
||||
dispatch(
|
||||
boardUpdated({
|
||||
boardId: board_id,
|
||||
requestBody: { board_name: newBoardName },
|
||||
})
|
||||
);
|
||||
updateBoard({ board_id, changes: { board_name: newBoardName } });
|
||||
};
|
||||
|
||||
const handleDeleteBoard = useCallback(() => {
|
||||
deleteBoard(board_id);
|
||||
}, [board_id, deleteBoard]);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (droppedImage.board_id === board_id) {
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
Divider,
|
||||
Flex,
|
||||
Select,
|
||||
Spinner,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
@ -19,9 +20,11 @@ import { useSelector } from 'react-redux';
|
||||
import { selectBoardsAll } from '../../store/boardSlice';
|
||||
import IAISelect from '../../../../common/components/IAISelect';
|
||||
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
||||
import { useListAllBoardsQuery } from 'services/apiSlice';
|
||||
|
||||
const UpdateImageBoardModal = () => {
|
||||
const boards = useSelector(selectBoardsAll);
|
||||
// const boards = useSelector(selectBoardsAll);
|
||||
const { data: boards, isFetching } = useListAllBoardsQuery();
|
||||
const { isOpen, onClose, handleAddToBoard, image } = useContext(
|
||||
AddImageToBoardContext
|
||||
);
|
||||
@ -29,9 +32,9 @@ const UpdateImageBoardModal = () => {
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const currentBoard = boards.filter(
|
||||
const currentBoard = boards?.find(
|
||||
(board) => board.board_id === image?.board_id
|
||||
)[0];
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
@ -55,15 +58,19 @@ const UpdateImageBoardModal = () => {
|
||||
<strong>{currentBoard.board_name}</strong> to
|
||||
</Text>
|
||||
)}
|
||||
<IAIMantineSelect
|
||||
placeholder="Select Board"
|
||||
onChange={(v) => setSelectedBoard(v)}
|
||||
value={selectedBoard}
|
||||
data={boards.map((board) => ({
|
||||
label: board.board_name,
|
||||
value: board.board_id,
|
||||
}))}
|
||||
/>
|
||||
{isFetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<IAIMantineSelect
|
||||
placeholder="Select Board"
|
||||
onChange={(v) => setSelectedBoard(v)}
|
||||
value={selectedBoard}
|
||||
data={(boards ?? []).map((board) => ({
|
||||
label: board.board_name,
|
||||
value: board.board_id,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</AlertDialogBody>
|
||||
@ -73,7 +80,9 @@ const UpdateImageBoardModal = () => {
|
||||
isDisabled={!selectedBoard}
|
||||
colorScheme="accent"
|
||||
onClick={() => {
|
||||
if (selectedBoard) handleAddToBoard(selectedBoard);
|
||||
if (selectedBoard) {
|
||||
handleAddToBoard(selectedBoard);
|
||||
}
|
||||
}}
|
||||
ml={3}
|
||||
>
|
||||
|
@ -51,9 +51,12 @@ import { useAppToaster } from 'app/components/Toaster';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
|
||||
import { DeleteImageButton } from './DeleteImageModal';
|
||||
import { selectImagesById } from '../store/imagesSlice';
|
||||
import { RootState } from 'app/store/store';
|
||||
|
||||
const currentImageButtonsSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state,
|
||||
systemSelector,
|
||||
gallerySelector,
|
||||
postprocessingSelector,
|
||||
@ -61,7 +64,7 @@ const currentImageButtonsSelector = createSelector(
|
||||
lightboxSelector,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(system, gallery, postprocessing, ui, lightbox, activeTabName) => {
|
||||
(state, system, gallery, postprocessing, ui, lightbox, activeTabName) => {
|
||||
const {
|
||||
isProcessing,
|
||||
isConnected,
|
||||
@ -81,6 +84,8 @@ const currentImageButtonsSelector = createSelector(
|
||||
shouldShowProgressInViewer,
|
||||
} = ui;
|
||||
|
||||
const imageDTO = selectImagesById(state, gallery.selectedImage ?? '');
|
||||
|
||||
const { selectedImage } = gallery;
|
||||
|
||||
return {
|
||||
@ -97,10 +102,10 @@ const currentImageButtonsSelector = createSelector(
|
||||
activeTabName,
|
||||
isLightboxOpen,
|
||||
shouldHidePreview,
|
||||
image: selectedImage,
|
||||
seed: selectedImage?.metadata?.seed,
|
||||
prompt: selectedImage?.metadata?.positive_conditioning,
|
||||
negativePrompt: selectedImage?.metadata?.negative_conditioning,
|
||||
image: imageDTO,
|
||||
seed: imageDTO?.metadata?.seed,
|
||||
prompt: imageDTO?.metadata?.positive_conditioning,
|
||||
negativePrompt: imageDTO?.metadata?.negative_conditioning,
|
||||
shouldShowProgressInViewer,
|
||||
};
|
||||
},
|
||||
|
@ -15,6 +15,8 @@ import { imageSelected } from '../store/gallerySlice';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { selectImagesById } from '../store/imagesSlice';
|
||||
|
||||
export const imagesSelector = createSelector(
|
||||
[uiSelector, gallerySelector, systemSelector],
|
||||
@ -29,7 +31,7 @@ export const imagesSelector = createSelector(
|
||||
return {
|
||||
shouldShowImageDetails,
|
||||
shouldHidePreview,
|
||||
image: selectedImage,
|
||||
selectedImage,
|
||||
progressImage,
|
||||
shouldShowProgressInViewer,
|
||||
shouldAntialiasProgressImage,
|
||||
@ -45,11 +47,16 @@ export const imagesSelector = createSelector(
|
||||
const CurrentImagePreview = () => {
|
||||
const {
|
||||
shouldShowImageDetails,
|
||||
image,
|
||||
selectedImage,
|
||||
progressImage,
|
||||
shouldShowProgressInViewer,
|
||||
shouldAntialiasProgressImage,
|
||||
} = useAppSelector(imagesSelector);
|
||||
|
||||
const image = useAppSelector((state: RootState) =>
|
||||
selectImagesById(state, selectedImage ?? '')
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDrop = useCallback(
|
||||
@ -57,7 +64,7 @@ const CurrentImagePreview = () => {
|
||||
if (droppedImage.image_name === image?.image_name) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageSelected(droppedImage));
|
||||
dispatch(imageSelected(droppedImage.image_name));
|
||||
},
|
||||
[dispatch, image?.image_name]
|
||||
);
|
||||
|
@ -72,17 +72,10 @@ interface HoverableImageProps {
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const memoEqualityCheck = (
|
||||
prev: HoverableImageProps,
|
||||
next: HoverableImageProps
|
||||
) =>
|
||||
prev.image.image_name === next.image.image_name &&
|
||||
prev.isSelected === next.isSelected;
|
||||
|
||||
/**
|
||||
* Gallery image component with delete/use all/use seed buttons on hover.
|
||||
*/
|
||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const HoverableImage = (props: HoverableImageProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
activeTabName,
|
||||
@ -121,7 +114,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
|
||||
const handleSelectImage = useCallback(() => {
|
||||
dispatch(imageSelected(image));
|
||||
dispatch(imageSelected(image.image_name));
|
||||
}, [image, dispatch]);
|
||||
|
||||
// Recall parameters handlers
|
||||
@ -260,7 +253,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
|
||||
Add to Board
|
||||
{image.board_id ? 'Change Board' : 'Add to Board'}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.300' }}
|
||||
@ -357,8 +350,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
</ContextMenu>
|
||||
</Box>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
};
|
||||
|
||||
HoverableImage.displayName = 'HoverableImage';
|
||||
|
||||
export default HoverableImage;
|
||||
export default memo(HoverableImage);
|
||||
|
@ -201,12 +201,6 @@ const ImageGalleryContent = () => {
|
||||
dispatch(setGalleryView('boards'));
|
||||
}, [dispatch]);
|
||||
|
||||
const [newBoardName, setNewBoardName] = useState('');
|
||||
|
||||
const handleCreateNewBoard = () => {
|
||||
dispatch(boardCreated({ requestBody: newBoardName }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
@ -323,9 +317,7 @@ const ImageGalleryContent = () => {
|
||||
<HoverableImage
|
||||
key={`${item.image_name}-${item.thumbnail_url}`}
|
||||
image={item}
|
||||
isSelected={
|
||||
selectedImage?.image_name === item?.image_name
|
||||
}
|
||||
isSelected={selectedImage === item?.image_name}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
@ -344,9 +336,7 @@ const ImageGalleryContent = () => {
|
||||
<HoverableImage
|
||||
key={`${item.image_name}-${item.thumbnail_url}`}
|
||||
image={item}
|
||||
isSelected={
|
||||
selectedImage?.image_name === item?.image_name
|
||||
}
|
||||
isSelected={selectedImage === item?.image_name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -93,19 +93,11 @@ type ImageMetadataViewerProps = {
|
||||
image: ImageDTO;
|
||||
};
|
||||
|
||||
// TODO: I don't know if this is needed.
|
||||
const memoEqualityCheck = (
|
||||
prev: ImageMetadataViewerProps,
|
||||
next: ImageMetadataViewerProps
|
||||
) => prev.image.image_name === next.image.image_name;
|
||||
|
||||
// TODO: Show more interesting information in this component.
|
||||
|
||||
/**
|
||||
* Image metadata viewer overlays currently selected image and provides
|
||||
* access to any of its metadata for use in processing.
|
||||
*/
|
||||
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
recallBothPrompts,
|
||||
@ -333,8 +325,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
};
|
||||
|
||||
ImageMetadataViewer.displayName = 'ImageMetadataViewer';
|
||||
|
||||
export default ImageMetadataViewer;
|
||||
export default memo(ImageMetadataViewer);
|
||||
|
@ -42,7 +42,7 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
}
|
||||
|
||||
const currentImageIndex = filteredImageIds.findIndex(
|
||||
(i) => i === selectedImage.image_name
|
||||
(i) => i === selectedImage
|
||||
);
|
||||
|
||||
const nextImageIndex = clamp(
|
||||
@ -71,6 +71,8 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
nextImage,
|
||||
prevImage,
|
||||
nextImageId,
|
||||
prevImageId,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -84,7 +86,7 @@ const NextPrevImageButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isOnFirstImage, isOnLastImage, nextImage, prevImage } =
|
||||
const { isOnFirstImage, isOnLastImage, nextImageId, prevImageId } =
|
||||
useAppSelector(nextPrevImageButtonsSelector);
|
||||
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||
@ -99,19 +101,19 @@ const NextPrevImageButtons = () => {
|
||||
}, []);
|
||||
|
||||
const handlePrevImage = useCallback(() => {
|
||||
dispatch(imageSelected(prevImage));
|
||||
}, [dispatch, prevImage]);
|
||||
dispatch(imageSelected(prevImageId));
|
||||
}, [dispatch, prevImageId]);
|
||||
|
||||
const handleNextImage = useCallback(() => {
|
||||
dispatch(imageSelected(nextImage));
|
||||
}, [dispatch, nextImage]);
|
||||
dispatch(imageSelected(nextImageId));
|
||||
}, [dispatch, nextImageId]);
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
handlePrevImage();
|
||||
},
|
||||
[prevImage]
|
||||
[prevImageId]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@ -119,7 +121,7 @@ const NextPrevImageButtons = () => {
|
||||
() => {
|
||||
handleNextImage();
|
||||
},
|
||||
[nextImage]
|
||||
[nextImageId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -7,7 +7,7 @@ import { imageUrlsReceived } from 'services/thunks/image';
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
export interface GalleryState {
|
||||
selectedImage?: ImageDTO;
|
||||
selectedImage?: string;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||
shouldAutoSwitchToNewImages: boolean;
|
||||
@ -27,7 +27,7 @@ export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState: initialGalleryState,
|
||||
reducers: {
|
||||
imageSelected: (state, action: PayloadAction<ImageDTO | undefined>) => {
|
||||
imageSelected: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.selectedImage = action.payload;
|
||||
// TODO: if the user selects an image, disable the auto switch?
|
||||
// state.shouldAutoSwitchToNewImages = false;
|
||||
@ -63,17 +63,17 @@ export const gallerySlice = createSlice({
|
||||
state.shouldAutoSwitchToNewImages &&
|
||||
action.payload.image_category === 'general'
|
||||
) {
|
||||
state.selectedImage = action.payload;
|
||||
state.selectedImage = action.payload.image_name;
|
||||
}
|
||||
});
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_url, thumbnail_url } = action.payload;
|
||||
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
// const { image_name, image_url, thumbnail_url } = action.payload;
|
||||
|
||||
if (state.selectedImage?.image_name === image_name) {
|
||||
state.selectedImage.image_url = image_url;
|
||||
state.selectedImage.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
});
|
||||
// if (state.selectedImage?.image_name === image_name) {
|
||||
// state.selectedImage.image_url = image_url;
|
||||
// state.selectedImage.thumbnail_url = thumbnail_url;
|
||||
// }
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -23,13 +23,9 @@ export type BoardDTO = {
|
||||
*/
|
||||
updated_at: string;
|
||||
/**
|
||||
* The name of the cover image of the board.
|
||||
* The name of the board's cover image.
|
||||
*/
|
||||
cover_image_name?: string;
|
||||
/**
|
||||
* The URL of the thumbnail of the board's cover image.
|
||||
*/
|
||||
cover_image_url?: string;
|
||||
/**
|
||||
* The number of images in the board.
|
||||
*/
|
||||
|
@ -17,13 +17,18 @@ export class BoardsService {
|
||||
/**
|
||||
* List Boards
|
||||
* Gets a list of boards
|
||||
* @returns OffsetPaginatedResults_BoardDTO_ Successful Response
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static listBoards({
|
||||
all,
|
||||
offset,
|
||||
limit = 10,
|
||||
limit,
|
||||
}: {
|
||||
/**
|
||||
* Whether to list all boards
|
||||
*/
|
||||
all?: boolean,
|
||||
/**
|
||||
* The page offset
|
||||
*/
|
||||
@ -32,11 +37,12 @@ export class BoardsService {
|
||||
* The number of boards per page
|
||||
*/
|
||||
limit?: number,
|
||||
}): CancelablePromise<OffsetPaginatedResults_BoardDTO_> {
|
||||
}): CancelablePromise<(OffsetPaginatedResults_BoardDTO_ | Array<BoardDTO>)> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/v1/boards/',
|
||||
query: {
|
||||
'all': all,
|
||||
'offset': offset,
|
||||
'limit': limit,
|
||||
},
|
||||
@ -53,15 +59,19 @@ export class BoardsService {
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createBoard({
|
||||
requestBody,
|
||||
boardName,
|
||||
}: {
|
||||
requestBody: string,
|
||||
/**
|
||||
* The name of the board to create
|
||||
*/
|
||||
boardName: string,
|
||||
}): CancelablePromise<BoardDTO> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/v1/boards/',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
query: {
|
||||
'board_name': boardName,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
|
144
invokeai/frontend/web/src/services/apiSlice.ts
Normal file
144
invokeai/frontend/web/src/services/apiSlice.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { BoardDTO } from './api/models/BoardDTO';
|
||||
import { OffsetPaginatedResults_BoardDTO_ } from './api/models/OffsetPaginatedResults_BoardDTO_';
|
||||
import { BoardChanges } from './api/models/BoardChanges';
|
||||
import { OffsetPaginatedResults_ImageDTO_ } from './api/models/OffsetPaginatedResults_ImageDTO_';
|
||||
|
||||
type ListBoardsArg = { offset: number; limit: number };
|
||||
type UpdateBoardArg = { board_id: string; changes: BoardChanges };
|
||||
type AddImageToBoardArg = { board_id: string; image_name: string };
|
||||
type RemoveImageFromBoardArg = { board_id: string; image_name: string };
|
||||
type ListBoardImagesArg = { board_id: string; offset: number; limit: number };
|
||||
|
||||
export const api = createApi({
|
||||
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5173/api/v1/' }),
|
||||
reducerPath: 'api',
|
||||
tagTypes: ['Board'],
|
||||
endpoints: (build) => ({
|
||||
/**
|
||||
* Boards Queries
|
||||
*/
|
||||
listBoards: build.query<OffsetPaginatedResults_BoardDTO_, ListBoardsArg>({
|
||||
query: (arg) => ({ url: 'boards/', params: arg }),
|
||||
providesTags: (result, error, arg) => {
|
||||
if (!result) {
|
||||
// Provide the broad 'Board' tag until there is a response
|
||||
return ['Board'];
|
||||
}
|
||||
|
||||
// Provide the broad 'Board' tab, and individual tags for each board
|
||||
return [
|
||||
...result.items.map(({ board_id }) => ({
|
||||
type: 'Board' as const,
|
||||
id: board_id,
|
||||
})),
|
||||
'Board',
|
||||
];
|
||||
},
|
||||
}),
|
||||
|
||||
listAllBoards: build.query<Array<BoardDTO>, void>({
|
||||
query: () => ({
|
||||
url: 'boards/',
|
||||
params: { all: true },
|
||||
}),
|
||||
providesTags: (result, error, arg) => {
|
||||
if (!result) {
|
||||
// Provide the broad 'Board' tag until there is a response
|
||||
return ['Board'];
|
||||
}
|
||||
|
||||
// Provide the broad 'Board' tab, and individual tags for each board
|
||||
return [
|
||||
...result.map(({ board_id }) => ({
|
||||
type: 'Board' as const,
|
||||
id: board_id,
|
||||
})),
|
||||
'Board',
|
||||
];
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Boards Mutations
|
||||
*/
|
||||
|
||||
createBoard: build.mutation<BoardDTO, string>({
|
||||
query: (board_name) => ({
|
||||
url: `boards/`,
|
||||
method: 'POST',
|
||||
params: { board_name },
|
||||
}),
|
||||
invalidatesTags: ['Board'],
|
||||
}),
|
||||
|
||||
updateBoard: build.mutation<BoardDTO, UpdateBoardArg>({
|
||||
query: ({ board_id, changes }) => ({
|
||||
url: `boards/${board_id}`,
|
||||
method: 'PATCH',
|
||||
body: changes,
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
],
|
||||
}),
|
||||
|
||||
deleteBoard: build.mutation<void, string>({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Board Images Queries
|
||||
*/
|
||||
|
||||
listBoardImages: build.query<
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
ListBoardImagesArg
|
||||
>({
|
||||
query: ({ board_id, offset, limit }) => ({
|
||||
url: `board_images/${board_id}`,
|
||||
method: 'DELETE',
|
||||
body: { offset, limit },
|
||||
}),
|
||||
}),
|
||||
|
||||
/**
|
||||
* Board Images Mutations
|
||||
*/
|
||||
|
||||
addImageToBoard: build.mutation<void, AddImageToBoardArg>({
|
||||
query: ({ board_id, image_name }) => ({
|
||||
url: `board_images/`,
|
||||
method: 'POST',
|
||||
body: { board_id, image_name },
|
||||
}),
|
||||
invalidatesTags: ['Board'],
|
||||
// invalidatesTags: (result, error, arg) => [
|
||||
// { type: 'Board', id: arg.board_id },
|
||||
// ],
|
||||
}),
|
||||
|
||||
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
|
||||
query: ({ board_id, image_name }) => ({
|
||||
url: `board_images/`,
|
||||
method: 'DELETE',
|
||||
body: { board_id, image_name },
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useListBoardsQuery,
|
||||
useListAllBoardsQuery,
|
||||
useCreateBoardMutation,
|
||||
useUpdateBoardMutation,
|
||||
useDeleteBoardMutation,
|
||||
useAddImageToBoardMutation,
|
||||
useRemoveImageFromBoardMutation,
|
||||
useListBoardImagesQuery,
|
||||
} = api;
|
@ -42,7 +42,7 @@ export const boardUpdated = createAppAsyncThunk(
|
||||
|
||||
type ImageAddedToBoardArg = Parameters<
|
||||
(typeof BoardsService)['createBoardImage']
|
||||
>[0];
|
||||
>[0]['requestBody'];
|
||||
|
||||
export const imageAddedToBoard = createAppAsyncThunk(
|
||||
'api/imageAddedToBoard',
|
||||
|
Loading…
Reference in New Issue
Block a user