mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(api,ui): wip batch image actions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
@@ -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'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user