feat(api,ui): wip batch image actions

This commit is contained in:
psychedelicious
2023-07-07 00:49:25 +10:00
parent 0b9aaf1b0b
commit a08179bf34
15 changed files with 433 additions and 187 deletions

View File

@@ -1,7 +1,8 @@
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
from invokeai.app.models.image import AddManyImagesToBoardResult
from invokeai.app.models.image import (AddManyImagesToBoardResult,
RemoveManyImagesFromBoardResult)
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.image_record import ImageDTO
@@ -41,13 +42,14 @@ async def create_board_image(
status_code=201,
)
async def remove_board_image(
board_id: str = Body(description="The id of the board"),
image_name: str = Body(description="The name of the image to remove"),
image_name: str = Body(
description="The name of the image to remove from its board"
),
):
"""Deletes a board_image"""
try:
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(
board_id=board_id, image_name=image_name
image_name=image_name
)
return result
except Exception as e:
@@ -77,9 +79,6 @@ async def list_board_images(
operation_id="create_multiple_board_images",
responses={
201: {"description": "The images were added to the board successfully"},
207: {
"description": "Some images were added to the board successfully, but others failed"
},
},
status_code=201,
)
@@ -95,3 +94,26 @@ async def create_multiple_board_images(
board_id, image_names
)
return results
@board_images_router.post(
"/images",
operation_id="delete_multiple_board_images",
responses={
201: {"description": "The images were removed from their boards successfully"},
},
status_code=201,
)
async def delete_multiple_board_images(
image_names: list[str] = Body(
description="The names of the images to remove from their boards, if they have one"
),
) -> RemoveManyImagesFromBoardResult:
"""Remove many images from their boards, if they have one"""
results = (
ApiDependencies.invoker.services.board_images.remove_many_images_from_board(
image_names
)
)
return results

View File

@@ -107,3 +107,11 @@ class AddManyImagesToBoardResult(BaseModel):
description="The names of the images that were successfully added"
)
total: int = Field(description="The total number of images on the board")
class RemoveManyImagesFromBoardResult(BaseModel):
"""The result of a remove many images from their boards operation."""
removed_images: list[str] = Field(
description="The names of the images that were successfully removed from their boards"
)

View File

@@ -1,13 +1,13 @@
from abc import ABC, abstractmethod
import sqlite3
import threading
from typing import Optional, cast
from ABC import abstractmethod
from invokeai.app.services.board_record_storage import BoardRecord
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.image_record import (
ImageRecord,
deserialize_image_record,
)
ImageRecord, deserialize_image_record)
class BoardImageRecordStorageBase(ABC):
@@ -25,7 +25,6 @@ class BoardImageRecordStorageBase(ABC):
@abstractmethod
def remove_image_from_board(
self,
board_id: str,
image_name: str,
) -> None:
"""Removes an image from a board."""
@@ -154,7 +153,6 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
def remove_image_from_board(
self,
board_id: str,
image_name: str,
) -> None:
try:
@@ -162,9 +160,9 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
self._cursor.execute(
"""--sql
DELETE FROM board_images
WHERE board_id = ? AND image_name = ?;
WHERE image_name = ?;
""",
(board_id, image_name),
(image_name,),
)
self._conn.commit()
except sqlite3.Error as e:

View File

@@ -7,7 +7,8 @@ from invokeai.app.services.board_record_storage import (
BoardRecordStorageBase,
)
from invokeai.app.models.image import AddManyImagesToBoardResult
from invokeai.app.models.image import (AddManyImagesToBoardResult,
RemoveManyImagesFromBoardResult)
from invokeai.app.services.board_image_record_storage import \
BoardImageRecordStorageBase
from invokeai.app.services.board_record_storage import (BoardRecord,
@@ -44,10 +45,17 @@ class BoardImagesServiceABC(ABC):
@abstractmethod
def remove_image_from_board(
self,
board_id: str,
image_name: str,
) -> None:
"""Removes an image from a board."""
"""Removes an image from its board."""
pass
@abstractmethod
def remove_many_images_from_board(
self,
image_names: list[str],
) -> RemoveManyImagesFromBoardResult:
"""Removes many images from their board, if they had one."""
pass
@abstractmethod
@@ -128,10 +136,26 @@ class BoardImagesService(BoardImagesServiceABC):
def remove_image_from_board(
self,
board_id: str,
image_name: str,
) -> None:
self._services.board_image_records.remove_image_from_board(board_id, image_name)
self._services.board_image_records.remove_image_from_board(image_name)
def remove_many_images_from_board(
self,
image_names: list[str],
) -> RemoveManyImagesFromBoardResult:
removed_images: list[str] = []
for image_name in image_names:
try:
self._services.board_image_records.remove_image_from_board(image_name)
removed_images.append(image_name)
except Exception as e:
self._services.logger.exception(e)
return RemoveManyImagesFromBoardResult(
removed_images=removed_images,
)
def get_images_for_board(
self,

View File

@@ -7,6 +7,7 @@ import {
} from '@reduxjs/toolkit';
import type { AppDispatch, RootState } from '../../store';
import { addBoardListeners } from './listeners/addBoardListeners';
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
import { addAppStartedListener } from './listeners/appStarted';
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
@@ -17,10 +18,6 @@ import { addCanvasMergedListener } from './listeners/canvasMerged';
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import {
addImageAddedToBoardFulfilledListener,
addImageAddedToBoardRejectedListener,
} from './listeners/imageAddedToBoard';
import {
addImageDeletedFulfilledListener,
addImageDeletedPendingListener,
@@ -32,10 +29,6 @@ import {
addImageMetadataReceivedFulfilledListener,
addImageMetadataReceivedRejectedListener,
} from './listeners/imageMetadataReceived';
import {
addImageRemovedFromBoardFulfilledListener,
addImageRemovedFromBoardRejectedListener,
} from './listeners/imageRemovedFromBoard';
import { addImageToDeleteSelectedListener } from './listeners/imageToDeleteSelected';
import {
addImageUpdatedFulfilledListener,
@@ -204,10 +197,7 @@ addControlNetAutoProcessListener();
// addUpdateImageUrlsOnConnectListener();
// Boards
addImageAddedToBoardFulfilledListener();
addImageAddedToBoardRejectedListener();
addImageRemovedFromBoardFulfilledListener();
addImageRemovedFromBoardRejectedListener();
addBoardListeners();
addBoardIdSelectedListener();
// Node schemas

View File

@@ -0,0 +1,206 @@
import { createAction } from '@reduxjs/toolkit';
import { log } from 'app/logging/useLogger';
import {
imageUpdatedMany,
imageUpdatedOne,
} from 'features/gallery/store/gallerySlice';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' });
export const addBoardListeners = () => {
// add image to board - fulfilled
startAppListening({
matcher: boardImagesApi.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(
imageUpdatedOne({
id: image_name,
changes: { board_id },
})
);
},
});
// add image to board - rejected
startAppListening({
matcher: boardImagesApi.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'
);
},
});
// remove image from board - fulfilled
startAppListening({
matcher: boardImagesApi.endpoints.removeImageFromBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const image_name = action.meta.arg.originalArgs;
moduleLog.debug({ data: { image_name } }, 'Image removed from board');
dispatch(
imageUpdatedOne({
id: image_name,
changes: { board_id: undefined },
})
);
},
});
// remove image from board - rejected
startAppListening({
matcher: boardImagesApi.endpoints.removeImageFromBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const image_name = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { image_name } },
'Problem removing image from board'
);
},
});
// gallery selection added to board
startAppListening({
actionCreator: gallerySelectionAddedToBoard,
effect: (action, { getState, dispatch }) => {
const { board_id } = action.payload;
const image_names = getState().gallery.selection;
dispatch(
boardImagesApi.endpoints.addManyImagesToBoard.initiate({
board_id,
image_names,
})
);
},
});
// gallery selection removed from board
startAppListening({
actionCreator: gallerySelectionRemovedFromBoard,
effect: (action, { getState, dispatch }) => {
const image_names = getState().gallery.selection;
dispatch(
boardImagesApi.endpoints.removeManyImagesFromBoard.initiate(image_names)
);
},
});
// batch selection added to board
startAppListening({
actionCreator: batchSelectionAddedToBoard,
effect: (action, { getState, dispatch }) => {
const { board_id } = action.payload;
const image_names = getState().batch.selection;
dispatch(
boardImagesApi.endpoints.addManyImagesToBoard.initiate({
board_id,
image_names,
})
);
},
});
// batch selection removed from board
startAppListening({
actionCreator: batchSelectionRemovedFromBoard,
effect: (action, { getState, dispatch }) => {
const image_names = getState().batch.selection;
dispatch(
boardImagesApi.endpoints.removeManyImagesFromBoard.initiate(image_names)
);
},
});
// many images added to board - fulfilled
startAppListening({
matcher: boardImagesApi.endpoints.addManyImagesToBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const { board_id, image_names } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_names } },
'Images added to board'
);
const updates = image_names.map((image_name) => ({
id: image_name,
changes: { board_id },
}));
dispatch(imageUpdatedMany(updates));
},
});
// many images added to board - rejected
startAppListening({
matcher: boardImagesApi.endpoints.addManyImagesToBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const { board_id, image_names } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_names } },
'Problem adding many images to board'
);
},
});
// remove many images from board - fulfilled
startAppListening({
matcher: boardImagesApi.endpoints.removeManyImagesFromBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const image_names = action.meta.arg.originalArgs;
moduleLog.debug({ data: { image_names } }, 'Images removed from board');
const updates = image_names.map((image_name) => ({
id: image_name,
changes: { board_id: undefined },
}));
dispatch(imageUpdatedMany(updates));
},
});
// remove many images from board - rejected
startAppListening({
matcher: boardImagesApi.endpoints.removeManyImagesFromBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const image_names = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { image_names } },
'Problem removing many images from board'
);
},
});
};
export const gallerySelectionAddedToBoard = createAction<{ board_id: string }>(
'boards/gallerySelectionAddedToBoard'
);
export const gallerySelectionRemovedFromBoard = createAction(
'boards/gallerySelectionAddedToBoard'
);
export const batchSelectionAddedToBoard = createAction<{
board_id: string;
}>('boards/batchSelectionAddedToBoard');
export const batchSelectionRemovedFromBoard = createAction(
'boards/batchSelectionAddedToBoard'
);

View File

@@ -1,40 +0,0 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/api/thunks/image';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageAddedToBoardFulfilledListener = () => {
startAppListening({
matcher: boardImagesApi.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({
image_name,
})
);
},
});
};
export const addImageAddedToBoardRejectedListener = () => {
startAppListening({
matcher: boardImagesApi.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'
);
},
});
};

View File

@@ -18,6 +18,12 @@ import {
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { startAppListening } from '../';
import {
batchSelectionAddedToBoard,
batchSelectionRemovedFromBoard,
gallerySelectionAddedToBoard,
gallerySelectionRemovedFromBoard,
} from './addBoardListeners';
const moduleLog = log.child({ namespace: 'dnd' });
@@ -31,12 +37,11 @@ export const addImageDroppedListener = () => {
actionCreator: imageDropped,
effect: (action, { dispatch, getState }) => {
const { activeData, overData } = action.payload;
const { actionType } = overData;
const state = getState();
// set current image
if (
actionType === 'SET_CURRENT_IMAGE' &&
overData.actionType === 'SET_CURRENT_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -45,7 +50,7 @@ export const addImageDroppedListener = () => {
// set initial image
if (
actionType === 'SET_INITIAL_IMAGE' &&
overData.actionType === 'SET_INITIAL_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -54,7 +59,7 @@ export const addImageDroppedListener = () => {
// add image to batch
if (
actionType === 'ADD_TO_BATCH' &&
overData.actionType === 'ADD_TO_BATCH' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -63,7 +68,7 @@ export const addImageDroppedListener = () => {
// add multiple images to batch
if (
actionType === 'ADD_TO_BATCH' &&
overData.actionType === 'ADD_TO_BATCH' &&
activeData.payloadType === 'GALLERY_SELECTION'
) {
dispatch(imagesAddedToBatch(state.gallery.selection));
@@ -71,7 +76,7 @@ export const addImageDroppedListener = () => {
// set control image
if (
actionType === 'SET_CONTROLNET_IMAGE' &&
overData.actionType === 'SET_CONTROLNET_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -86,7 +91,7 @@ export const addImageDroppedListener = () => {
// set canvas image
if (
actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -95,7 +100,7 @@ export const addImageDroppedListener = () => {
// set nodes image
if (
actionType === 'SET_NODES_IMAGE' &&
overData.actionType === 'SET_NODES_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -111,7 +116,7 @@ export const addImageDroppedListener = () => {
// set multiple nodes images (single image handler)
if (
actionType === 'SET_MULTI_NODES_IMAGE' &&
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
@@ -127,7 +132,7 @@ export const addImageDroppedListener = () => {
// set multiple nodes images (multiple images handler)
if (
actionType === 'SET_MULTI_NODES_IMAGE' &&
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
activeData.payloadType === 'GALLERY_SELECTION'
) {
const { fieldName, nodeId } = overData.context;
@@ -142,24 +147,9 @@ export const addImageDroppedListener = () => {
);
}
// remove image from board
// TODO: remove board_id from `removeImageFromBoard()` endpoint
// TODO: handle multiple images
// if (
// actionType === 'MOVE_BOARD' &&
// activeData.payloadType === 'IMAGE_DTO' &&
// activeData.payload.imageDTO &&
// overData.boardId !== null
// ) {
// const { image_name } = activeData.payload.imageDTO;
// dispatch(
// boardImagesApi.endpoints.removeImageFromBoard.initiate({ image_name })
// );
// }
// add image to board
if (
actionType === 'MOVE_BOARD' &&
overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO &&
overData.context.boardId
@@ -174,20 +164,54 @@ export const addImageDroppedListener = () => {
);
}
// remove image from board
if (
overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO &&
overData.context.boardId === null
) {
const { image_name } = activeData.payload.imageDTO;
dispatch(
boardImagesApi.endpoints.removeImageFromBoard.initiate(image_name)
);
}
// add multiple images to board
if (
actionType === 'MOVE_BOARD' &&
overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'GALLERY_SELECTION' &&
overData.context.boardId
) {
const board_id = overData.context.boardId;
const image_names = state.gallery.selection;
dispatch(
boardImagesApi.endpoints.addManyImagesToBoard.initiate({
board_id,
image_names,
})
);
dispatch(gallerySelectionAddedToBoard({ board_id }));
}
if (
overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'BATCH_SELECTION' &&
overData.context.boardId
) {
const board_id = overData.context.boardId;
dispatch(batchSelectionAddedToBoard({ board_id }));
}
// remove multiple images from board
if (
overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'GALLERY_SELECTION' &&
overData.context.boardId === null
) {
dispatch(gallerySelectionRemovedFromBoard());
}
// remove multiple images from board
if (
overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'BATCH_SELECTION' &&
overData.context.boardId === null
) {
dispatch(batchSelectionRemovedFromBoard());
}
},
});

View File

@@ -1,40 +0,0 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/api/thunks/image';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageRemovedFromBoardFulfilledListener = () => {
startAppListening({
matcher: boardImagesApi.endpoints.removeImageFromBoard.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({
image_name,
})
);
},
});
};
export const addImageRemovedFromBoardRejectedListener = () => {
startAppListening({
matcher: boardImagesApi.endpoints.removeImageFromBoard.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'
);
},
});
};

View File

@@ -21,7 +21,13 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa';
import {
FaExpand,
FaFolder,
FaFolderPlus,
FaShare,
FaTrash,
} from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
import { ImageDTO } from 'services/api/types';
@@ -127,9 +133,17 @@ const ImageContextMenu = ({ image, children }: Props) => {
if (!image.board_id) {
return;
}
removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
removeFromBoard(image.image_name);
}, [image.board_id, image.image_name, removeFromBoard]);
const handleAddSelectionToBoard = useCallback(() => {
onClickAddToBoard(image);
}, [image, onClickAddToBoard]);
const handleRemoveSelectionFromBoard = useCallback(() => {
removeFromBoard(image.image_name);
}, [image.image_name, removeFromBoard]);
const handleOpenInNewTab = () => {
window.open(image.image_url, '_blank');
};
@@ -212,13 +226,13 @@ const ImageContextMenu = ({ image, children }: Props) => {
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
)}
{/* <MenuItem
<MenuItem
icon={<FaFolder />}
isDisabled={isInBatch}
onClickCapture={handleAddToBatch}
>
Add to Batch
</MenuItem> */}
</MenuItem>
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
{image.board_id ? 'Change Board' : 'Add to Board'}
</MenuItem>
@@ -247,12 +261,12 @@ const ImageContextMenu = ({ image, children }: Props) => {
>
Move Selection to Board
</MenuItem>
{/* <MenuItem
<MenuItem
icon={<FaFolderPlus />}
onClickCapture={handleAddSelectionToBatch}
>
Add Selection to Batch
</MenuItem> */}
</MenuItem>
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}

View File

@@ -79,6 +79,9 @@ export const gallerySlice = createSlice({
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
imagesAdapter.updateOne(state, action.payload);
},
imageUpdatedMany: (state, action: PayloadAction<Update<ImageDTO>[]>) => {
imagesAdapter.updateMany(state, action.payload);
},
imageRemoved: (state, action: PayloadAction<string>) => {
imagesAdapter.removeOne(state, action.payload);
},
@@ -206,6 +209,7 @@ export const {
export const {
imageUpserted,
imageUpdatedOne,
imageUpdatedMany,
imageRemoved,
imagesRemoved,
imageCategoriesChanged,

View File

@@ -1,22 +1,22 @@
import { Flex, Spacer, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { useCallback, useMemo } from 'react';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { skipToken } from '@reduxjs/toolkit/dist/query';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaLayerGroup, FaUndo, FaUpload } from 'react-icons/fa';
import useImageUploader from 'common/hooks/useImageUploader';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import IAIButton from 'common/components/IAIButton';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import useImageUploader from 'common/hooks/useImageUploader';
import BatchImageContainer from 'features/batch/components/BatchImageContainer';
import {
asInitialImageToggled,
batchReset,
} from 'features/batch/store/batchSlice';
import BatchImageContainer from 'features/batch/components/BatchImageContainer';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { useCallback, useMemo } from 'react';
import { FaLayerGroup, FaUndo, FaUpload } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { PostUploadAction } from 'services/api/thunks/image';
import InitialImage from './InitialImage';
@@ -114,7 +114,7 @@ const InitialImageDisplay = () => {
Initial Image
</Text>
<Spacer />
{/* <IAIButton
<IAIButton
tooltip={useBatchAsInitialImage ? 'Disable Batch' : 'Enable Batch'}
aria-label={useBatchAsInitialImage ? 'Disable Batch' : 'Enable Batch'}
leftIcon={<FaLayerGroup />}
@@ -122,7 +122,7 @@ const InitialImageDisplay = () => {
onClick={handleClickUseBatch}
>
{useBatchAsInitialImage ? 'Batch' : 'Single'}
</IAIButton> */}
</IAIButton>
<IAIIconButton
tooltip={
useBatchAsInitialImage ? 'Upload to Batch' : 'Upload Initial Image'
@@ -146,8 +146,7 @@ const InitialImageDisplay = () => {
isDisabled={isResetButtonDisabled}
/>
</Flex>
<InitialImage />
{/* {useBatchAsInitialImage ? <BatchImageContainer /> : <InitialImage />} */}
{useBatchAsInitialImage ? <BatchImageContainer /> : <InitialImage />}
<input {...getUploadInputProps()} />
</Flex>
);

View File

@@ -24,7 +24,7 @@ import { isEqual } from 'lodash-es';
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaCube, FaFont, FaImage } from 'react-icons/fa';
import { FaCube, FaFont, FaImage, FaLayerGroup } from 'react-icons/fa';
import { MdDeviceHub, MdGridOn } from 'react-icons/md';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize';
@@ -32,6 +32,7 @@ import {
activeTabIndexSelector,
activeTabNameSelector,
} from '../store/uiSelectors';
import BatchTab from './tabs/Batch/BatchTab';
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
import NodesTab from './tabs/Nodes/NodesTab';
@@ -78,11 +79,11 @@ const tabs: InvokeTabInfo[] = [
icon: <Icon as={FaCube} sx={{ boxSize: 6, pointerEvents: 'none' }} />,
content: <ModelManagerTab />,
},
// {
// id: 'batch',
// icon: <Icon as={FaLayerGroup} sx={{ boxSize: 6, pointerEvents: 'none' }} />,
// content: <BatchTab />,
// },
{
id: 'batch',
icon: <Icon as={FaLayerGroup} sx={{ boxSize: 6, pointerEvents: 'none' }} />,
content: <BatchTab />,
},
];
const enabledTabsSelector = createSelector(

View File

@@ -17,6 +17,9 @@ type AddManyImagesToBoardArg =
type RemoveImageFromBoardArg =
paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'];
type RemoveManyBoardImagesArg =
paths['/api/v1/board_images/images']['post']['requestBody']['content']['application/json'];
export const boardImagesApi = api.injectEndpoints({
endpoints: (build) => ({
/**
@@ -104,18 +107,15 @@ export const boardImagesApi = api.injectEndpoints({
}),
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
query: ({ board_id, image_name }) => ({
query: (image_name) => ({
url: `board_images/`,
method: 'DELETE',
body: { board_id, image_name },
body: image_name,
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
{ type: 'Board', id: LIST_TAG },
],
async onQueryStarted(
{ image_name, ...patch },
{ dispatch, queryFulfilled }
) {
async onQueryStarted(image_name, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => {
Object.assign(draft, { board_id: null });
@@ -128,6 +128,35 @@ export const boardImagesApi = api.injectEndpoints({
}
},
}),
removeManyImagesFromBoard: build.mutation<void, RemoveManyBoardImagesArg>({
query: (image_names) => ({
url: `board_images/images`,
method: 'POST',
body: image_names,
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: LIST_TAG },
],
async onQueryStarted(image_names, { dispatch, queryFulfilled }) {
const patches: PatchCollection[] = [];
image_names.forEach((n) => {
const patchResult = dispatch(
imagesApi.util.updateQueryData('getImageDTO', n, (draft) => {
Object.assign(draft, { board_id: null });
})
);
patches.push(patchResult);
});
try {
await queryFulfilled;
} catch {
patches.forEach((p) => p.undo());
}
},
}),
}),
});

View File

@@ -216,6 +216,13 @@ export type paths = {
/** Get Version */
get: operations["app_version"];
};
"/api/v1/board_images/images": {
/**
* Delete Multiple Board Images
* @description Remove many images from their boards, if they have one
*/
post: operations["delete_multiple_board_images"];
};
};
export type webhooks = Record<string, never>;
@@ -4432,18 +4439,18 @@ export type components = {
*/
image?: components["schemas"]["ImageField"];
};
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion1ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
};
responses: never;
parameters: never;
@@ -5421,7 +5428,7 @@ export type operations = {
remove_board_image: {
requestBody: {
content: {
"application/json": components["schemas"]["Body_remove_board_image"];
"application/json": string;
};
};
responses: {