Merge branch 'main' into release/3-0-0

This commit is contained in:
Lincoln Stein 2023-07-21 07:38:02 -04:00 committed by GitHub
commit eb4ca4042e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1536 additions and 1053 deletions

View File

@ -24,7 +24,7 @@ title: Home
[![CI checks on main badge]][ci checks on main link] [![CI checks on main badge]][ci checks on main link]
[![CI checks on dev badge]][ci checks on dev link] [![CI checks on dev badge]][ci checks on dev link]
[![latest commit to dev badge]][latest commit to dev link] <!-- [![latest commit to dev badge]][latest commit to dev link] -->
[![github open issues badge]][github open issues link] [![github open issues badge]][github open issues link]
[![github open prs badge]][github open prs link] [![github open prs badge]][github open prs link]
@ -54,10 +54,10 @@ title: Home
[github stars badge]: [github stars badge]:
https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github
[github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers [github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers
[latest commit to dev badge]: <!-- [latest commit to dev badge]:
https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900 https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900
[latest commit to dev link]: [latest commit to dev link]:
https://github.com/invoke-ai/InvokeAI/commits/development https://github.com/invoke-ai/InvokeAI/commits/main -->
[latest release badge]: [latest release badge]:
https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github
[latest release link]: https://github.com/invoke-ai/InvokeAI/releases [latest release link]: https://github.com/invoke-ai/InvokeAI/releases
@ -82,6 +82,25 @@ Q&A</a>]
This fork is rapidly evolving. Please use the [Issues tab](https://github.com/invoke-ai/InvokeAI/issues) to report bugs and make feature requests. Be sure to use the provided templates. They will help aid diagnose issues faster. This fork is rapidly evolving. Please use the [Issues tab](https://github.com/invoke-ai/InvokeAI/issues) to report bugs and make feature requests. Be sure to use the provided templates. They will help aid diagnose issues faster.
## :octicons-package-dependencies-24: Installation
This fork is supported across Linux, Windows and Macintosh. Linux users can use
either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
driver).
### [Installation Getting Started Guide](installation)
#### **[Automated Installer](installation/010_INSTALL_AUTOMATED.md)**
✅ This is the recommended installation method for first-time users.
#### [Manual Installation](installation/020_INSTALL_MANUAL.md)
This method is recommended for experienced users and developers
#### [Docker Installation](installation/040_INSTALL_DOCKER.md)
This method is recommended for those familiar with running Docker containers
### Other Installation Guides
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
- [XFormers](installation/070_INSTALL_XFORMERS.md)
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
## :fontawesome-solid-computer: Hardware Requirements ## :fontawesome-solid-computer: Hardware Requirements
### :octicons-cpu-24: System ### :octicons-cpu-24: System
@ -107,24 +126,6 @@ images in full-precision mode:
- At least 18 GB of free disk space for the machine learning model, Python, and - At least 18 GB of free disk space for the machine learning model, Python, and
all its dependencies. all its dependencies.
## :octicons-package-dependencies-24: Installation
This fork is supported across Linux, Windows and Macintosh. Linux users can use
either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
driver).
### [Installation Getting Started Guide](installation)
#### [Automated Installer](installation/010_INSTALL_AUTOMATED.md)
This method is recommended for 1st time users
#### [Manual Installation](installation/020_INSTALL_MANUAL.md)
This method is recommended for experienced users and developers
#### [Docker Installation](installation/040_INSTALL_DOCKER.md)
This method is recommended for those familiar with running Docker containers
### Other Installation Guides
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
- [XFormers](installation/070_INSTALL_XFORMERS.md)
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
## :octicons-gift-24: InvokeAI Features ## :octicons-gift-24: InvokeAI Features

View File

@ -124,9 +124,9 @@ experimental versions later.
[latest release](https://github.com/invoke-ai/InvokeAI/releases/latest), [latest release](https://github.com/invoke-ai/InvokeAI/releases/latest),
and look for a file named: and look for a file named:
- InvokeAI-installer-v2.X.X.zip - InvokeAI-installer-v3.X.X.zip
where "2.X.X" is the latest released version. The file is located where "3.X.X" is the latest released version. The file is located
at the very bottom of the release page, under **Assets**. at the very bottom of the release page, under **Assets**.
4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new 4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new

View File

@ -15,7 +15,7 @@ See the [troubleshooting
section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated
install guide for frequently-encountered installation issues. install guide for frequently-encountered installation issues.
## Main Application ## Installation options
1. [Automated Installer](010_INSTALL_AUTOMATED.md) 1. [Automated Installer](010_INSTALL_AUTOMATED.md)
@ -24,6 +24,9 @@ install guide for frequently-encountered installation issues.
"developer console" which will help us debug problems with you and "developer console" which will help us debug problems with you and
give you to access experimental features. give you to access experimental features.
✅ This is the recommended option for first time users.
2. [Manual Installation](020_INSTALL_MANUAL.md) 2. [Manual Installation](020_INSTALL_MANUAL.md)
In this method you will manually run the commands needed to install In this method you will manually run the commands needed to install

View File

@ -1,13 +1,17 @@
# Community Nodes # Community Nodes
These are nodes that have been developed by the community for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md). These are nodes that have been developed by the community, for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md).
If you'd like to submit a node for the community, please refer to the [node creation overview](overview.md). If you'd like to submit a node for the community, please refer to the [node creation overview](./overview.md#contributing-nodes).
To download a node, simply download the `.py` node file from the link and add it to the `invokeai/app/invocations/` folder in your Invoke AI install location. Along with the node, an example node graph should be provided to help you get started with the node. To download a node, simply download the `.py` node file from the link and add it to the `invokeai/app/invocations/` folder in your Invoke AI install location. Along with the node, an example node graph should be provided to help you get started with the node.
To use a community node graph, download the the `.json` node graph file and load it into Invoke AI via the **Load Nodes** button on the Node Editor. To use a community node graph, download the the `.json` node graph file and load it into Invoke AI via the **Load Nodes** button on the Node Editor.
## Disclaimer
The nodes linked below have been developed and contributed by members of the Invoke AI community. While we strive to ensure the quality and safety of these contributions, we do not guarantee the reliability or security of the nodes. If you have issues or concerns with any of the nodes below, please raise it on GitHub or in the Discord.
## List of Nodes ## List of Nodes
-------------------------------- --------------------------------

View File

@ -1,4 +1,5 @@
# Nodes # Nodes
## What are Nodes? ## What are Nodes?
An Node is simply a single operation that takes in some inputs and gives An Node is simply a single operation that takes in some inputs and gives
out some outputs. We can then chain multiple nodes together to create more out some outputs. We can then chain multiple nodes together to create more
@ -10,7 +11,7 @@ You can read more about nodes and the node editor [here](../features/NODES.md).
## Downloading Nodes ## Downloading Nodes
To download a new node, visit our list of [Community Nodes](communityNodes.md). These are codes that have been created by the community, for the community. To download a new node, visit our list of [Community Nodes](communityNodes.md). These are nodes that have been created by the community, for the community.
## Contributing Nodes ## Contributing Nodes
@ -18,10 +19,10 @@ To download a new node, visit our list of [Community Nodes](communityNodes.md).
To learn about creating a new node, please visit our [Node creation documenation](../contributing/INVOCATIONS.md). To learn about creating a new node, please visit our [Node creation documenation](../contributing/INVOCATIONS.md).
Once youve created a node and confirmed that it behaves as expected locally, follow these steps: Once youve created a node and confirmed that it behaves as expected locally, follow these steps:
- Make sure the node is contained in a new Python (.py) file * Make sure the node is contained in a new Python (.py) file
- Submit a pull request with a link to your node in GitHub against the `nodes` branch to add the node to the [Community Nodes](Community Nodes) list * Submit a pull request with a link to your node in GitHub against the `nodes` branch to add the node to the [Community Nodes](Community Nodes) list
- Make sure you are following the template below and have provided all relevant details about the node and what it does. * Make sure you are following the template below and have provided all relevant details about the node and what it does.
- A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you might be asked for permission to include it in the core project. * A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you might be asked for permission to include it in the core project.
### Community Node Template ### Community Node Template

View File

@ -40,9 +40,15 @@ async def upload_image(
response: Response, response: Response,
image_category: ImageCategory = Query(description="The category of the image"), image_category: ImageCategory = Query(description="The category of the image"),
is_intermediate: bool = Query(description="Whether this is an intermediate image"), is_intermediate: bool = Query(description="Whether this is an intermediate image"),
board_id: Optional[str] = Query(
default=None, description="The board to add this image to, if any"
),
session_id: Optional[str] = Query( session_id: Optional[str] = Query(
default=None, description="The session ID associated with this upload, if any" default=None, description="The session ID associated with this upload, if any"
), ),
crop_visible: Optional[bool] = Query(
default=False, description="Whether to crop the image"
),
) -> ImageDTO: ) -> ImageDTO:
"""Uploads an image""" """Uploads an image"""
if not file.content_type.startswith("image"): if not file.content_type.startswith("image"):
@ -52,6 +58,9 @@ async def upload_image(
try: try:
pil_image = Image.open(io.BytesIO(contents)) pil_image = Image.open(io.BytesIO(contents))
if crop_visible:
bbox = pil_image.getbbox()
pil_image = pil_image.crop(bbox)
except: except:
# Error opening the image # Error opening the image
raise HTTPException(status_code=415, detail="Failed to read image") raise HTTPException(status_code=415, detail="Failed to read image")
@ -62,6 +71,7 @@ async def upload_image(
image_origin=ResourceOrigin.EXTERNAL, image_origin=ResourceOrigin.EXTERNAL,
image_category=image_category, image_category=image_category,
session_id=session_id, session_id=session_id,
board_id=board_id,
is_intermediate=is_intermediate, is_intermediate=is_intermediate,
) )

View File

@ -52,6 +52,7 @@ class ImageServiceABC(ABC):
image_category: ImageCategory, image_category: ImageCategory,
node_id: Optional[str] = None, node_id: Optional[str] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
board_id: Optional[str] = None,
is_intermediate: bool = False, is_intermediate: bool = False,
metadata: Optional[dict] = None, metadata: Optional[dict] = None,
) -> ImageDTO: ) -> ImageDTO:
@ -174,6 +175,7 @@ class ImageService(ImageServiceABC):
image_category: ImageCategory, image_category: ImageCategory,
node_id: Optional[str] = None, node_id: Optional[str] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
board_id: Optional[str] = None,
is_intermediate: bool = False, is_intermediate: bool = False,
metadata: Optional[dict] = None, metadata: Optional[dict] = None,
) -> ImageDTO: ) -> ImageDTO:
@ -215,6 +217,11 @@ class ImageService(ImageServiceABC):
session_id=session_id, session_id=session_id,
) )
if board_id is not None:
self._services.board_image_records.add_image_to_board(
board_id=board_id, image_name=image_name
)
self._services.image_files.save( self._services.image_files.save(
image_name=image_name, image=image, metadata=metadata, graph=graph image_name=image_name, image=image, metadata=metadata, graph=graph
) )

View File

@ -1,4 +1,6 @@
import math
import torch import torch
import diffusers
if torch.backends.mps.is_available(): if torch.backends.mps.is_available():
@ -61,3 +63,150 @@ def new_torch_interpolate(input, size=None, scale_factor=None, mode='nearest', a
return _torch_interpolate(input, size, scale_factor, mode, align_corners, recompute_scale_factor, antialias) return _torch_interpolate(input, size, scale_factor, mode, align_corners, recompute_scale_factor, antialias)
torch.nn.functional.interpolate = new_torch_interpolate torch.nn.functional.interpolate = new_torch_interpolate
# TODO: refactor it
_SlicedAttnProcessor = diffusers.models.attention_processor.SlicedAttnProcessor
class ChunkedSlicedAttnProcessor:
r"""
Processor for implementing sliced attention.
Args:
slice_size (`int`, *optional*):
The number of steps to compute attention. Uses as many slices as `attention_head_dim // slice_size`, and
`attention_head_dim` must be a multiple of the `slice_size`.
"""
def __init__(self, slice_size):
assert isinstance(slice_size, int)
slice_size = 1 # TODO: maybe implement chunking in batches too when enough memory
self.slice_size = slice_size
self._sliced_attn_processor = _SlicedAttnProcessor(slice_size)
def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None):
if self.slice_size != 1:
return self._sliced_attn_processor(attn, hidden_states, encoder_hidden_states, attention_mask)
residual = hidden_states
input_ndim = hidden_states.ndim
if input_ndim == 4:
batch_size, channel, height, width = hidden_states.shape
hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2)
batch_size, sequence_length, _ = (
hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape
)
attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size)
if attn.group_norm is not None:
hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
query = attn.to_q(hidden_states)
dim = query.shape[-1]
query = attn.head_to_batch_dim(query)
if encoder_hidden_states is None:
encoder_hidden_states = hidden_states
elif attn.norm_cross:
encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states)
key = attn.to_k(encoder_hidden_states)
value = attn.to_v(encoder_hidden_states)
key = attn.head_to_batch_dim(key)
value = attn.head_to_batch_dim(value)
batch_size_attention, query_tokens, _ = query.shape
hidden_states = torch.zeros(
(batch_size_attention, query_tokens, dim // attn.heads), device=query.device, dtype=query.dtype
)
chunk_tmp_tensor = torch.empty(self.slice_size, query.shape[1], key.shape[1], dtype=query.dtype, device=query.device)
for i in range(batch_size_attention // self.slice_size):
start_idx = i * self.slice_size
end_idx = (i + 1) * self.slice_size
query_slice = query[start_idx:end_idx]
key_slice = key[start_idx:end_idx]
attn_mask_slice = attention_mask[start_idx:end_idx] if attention_mask is not None else None
self.get_attention_scores_chunked(attn, query_slice, key_slice, attn_mask_slice, hidden_states[start_idx:end_idx], value[start_idx:end_idx], chunk_tmp_tensor)
hidden_states = attn.batch_to_head_dim(hidden_states)
# linear proj
hidden_states = attn.to_out[0](hidden_states)
# dropout
hidden_states = attn.to_out[1](hidden_states)
if input_ndim == 4:
hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width)
if attn.residual_connection:
hidden_states = hidden_states + residual
hidden_states = hidden_states / attn.rescale_output_factor
return hidden_states
def get_attention_scores_chunked(self, attn, query, key, attention_mask, hidden_states, value, chunk):
# batch size = 1
assert query.shape[0] == 1
assert key.shape[0] == 1
assert value.shape[0] == 1
assert hidden_states.shape[0] == 1
dtype = query.dtype
if attn.upcast_attention:
query = query.float()
key = key.float()
#out_item_size = query.dtype.itemsize
#if attn.upcast_attention:
# out_item_size = torch.float32.itemsize
out_item_size = query.element_size()
if attn.upcast_attention:
out_item_size = 4
chunk_size = 2 ** 29
out_size = query.shape[1] * key.shape[1] * out_item_size
chunks_count = min(query.shape[1], math.ceil((out_size - 1) / chunk_size))
chunk_step = max(1, int(query.shape[1] / chunks_count))
key = key.transpose(-1, -2)
def _get_chunk_view(tensor, start, length):
if start + length > tensor.shape[1]:
length = tensor.shape[1] - start
#print(f"view: [{tensor.shape[0]},{tensor.shape[1]},{tensor.shape[2]}] - start: {start}, length: {length}")
return tensor[:,start:start+length]
for chunk_pos in range(0, query.shape[1], chunk_step):
if attention_mask is not None:
torch.baddbmm(
_get_chunk_view(attention_mask, chunk_pos, chunk_step),
_get_chunk_view(query, chunk_pos, chunk_step),
key,
beta=1,
alpha=attn.scale,
out=chunk,
)
else:
torch.baddbmm(
torch.zeros((1,1,1), device=query.device, dtype=query.dtype),
_get_chunk_view(query, chunk_pos, chunk_step),
key,
beta=0,
alpha=attn.scale,
out=chunk,
)
chunk = chunk.softmax(dim=-1)
torch.bmm(chunk, value, out=_get_chunk_view(hidden_states, chunk_pos, chunk_step))
#del chunk
diffusers.models.attention_processor.SlicedAttnProcessor = ChunkedSlicedAttnProcessor

View File

@ -175,9 +175,7 @@ export const isValidDrop = (
const destinationBoard = overData.context.boardId; const destinationBoard = overData.context.boardId;
const isSameBoard = currentBoard === destinationBoard; const isSameBoard = currentBoard === destinationBoard;
const isDestinationValid = !currentBoard const isDestinationValid = !currentBoard ? destinationBoard : true;
? destinationBoard !== 'no_board'
: true;
return !isSameBoard && isDestinationValid; return !isSameBoard && isDestinationValid;
} }

View File

@ -19,10 +19,10 @@ export const addFirstListImagesListener = () => {
action, action,
{ getState, dispatch, unsubscribe, cancelActiveListeners } { getState, dispatch, unsubscribe, cancelActiveListeners }
) => { ) => {
// Only run this listener on the first listImages request for `images` categories // Only run this listener on the first listImages request for no-board images
if ( if (
action.meta.arg.queryCacheKey !== action.meta.arg.queryCacheKey !==
getListImagesUrl({ categories: IMAGE_CATEGORIES }) getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })
) { ) {
return; return;
} }

View File

@ -1,20 +1,20 @@
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { import {
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
boardIdSelected, boardIdSelected,
galleryViewChanged,
imageSelected, imageSelected,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import {
getBoardIdQueryParamForBoard,
getCategoriesQueryParamForBoard,
} from 'features/gallery/store/util';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..'; import { startAppListening } from '..';
import { isAnyOf } from '@reduxjs/toolkit';
const moduleLog = log.child({ namespace: 'boards' }); const moduleLog = log.child({ namespace: 'boards' });
export const addBoardIdSelectedListener = () => { export const addBoardIdSelectedListener = () => {
startAppListening({ startAppListening({
actionCreator: boardIdSelected, matcher: isAnyOf(boardIdSelected, galleryViewChanged),
effect: async ( effect: async (
action, action,
{ getState, dispatch, condition, cancelActiveListeners } { getState, dispatch, condition, cancelActiveListeners }
@ -22,12 +22,21 @@ export const addBoardIdSelectedListener = () => {
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board // Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
cancelActiveListeners(); cancelActiveListeners();
const _board_id = action.payload; const state = getState();
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
const categories = getCategoriesQueryParamForBoard(_board_id); const board_id = boardIdSelected.match(action)
const board_id = getBoardIdQueryParamForBoard(_board_id); ? action.payload
const queryArgs = { board_id, categories }; : state.gallery.selectedBoardId;
const galleryView = galleryViewChanged.match(action)
? action.payload
: state.gallery.galleryView;
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
const categories =
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
const queryArgs = { board_id: board_id ?? 'none', categories };
// wait until the board has some images - maybe it already has some from a previous fetch // wait until the board has some images - maybe it already has some from a previous fetch
// must use getState() to ensure we do not have stale state // must use getState() to ensure we do not have stale state
@ -35,7 +44,7 @@ export const addBoardIdSelectedListener = () => {
() => () =>
imagesApi.endpoints.listImages.select(queryArgs)(getState()) imagesApi.endpoints.listImages.select(queryArgs)(getState())
.isSuccess, .isSuccess,
1000 5000
); );
if (isSuccess) { if (isSuccess) {

View File

@ -45,7 +45,7 @@ export const addCanvasMergedListener = () => {
relativeTo: canvasBaseLayer.getParent(), relativeTo: canvasBaseLayer.getParent(),
}); });
const imageUploadedRequest = dispatch( const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({ imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'mergedCanvas.png', { file: new File([blob], 'mergedCanvas.png', {
type: 'image/png', type: 'image/png',
@ -57,17 +57,10 @@ export const addCanvasMergedListener = () => {
toastOptions: { title: 'Canvas Merged' }, toastOptions: { title: 'Canvas Merged' },
}, },
}) })
); ).unwrap();
const [{ payload }] = await take(
(uploadedImageAction) =>
imagesApi.endpoints.uploadImage.matchFulfilled(uploadedImageAction) &&
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
);
// TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here // TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
const { image_name } = const { image_name } = imageDTO;
payload as typeof imagesApi.endpoints.uploadImage.Types.ResultType;
dispatch( dispatch(
setMergedCanvas({ setMergedCanvas({

View File

@ -34,6 +34,8 @@ export const addCanvasSavedToGalleryListener = () => {
}), }),
image_category: 'general', image_category: 'general',
is_intermediate: false, is_intermediate: false,
board_id: state.gallery.autoAddBoardId,
crop_visible: true,
postUploadAction: { postUploadAction: {
type: 'TOAST', type: 'TOAST',
toastOptions: { title: 'Canvas Saved to Gallery' }, toastOptions: { title: 'Canvas Saved to Gallery' },

View File

@ -156,14 +156,13 @@ export const addImageDroppedListener = () => {
if ( if (
overData.actionType === 'MOVE_BOARD' && overData.actionType === 'MOVE_BOARD' &&
activeData.payloadType === 'IMAGE_DTO' && activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO && activeData.payload.imageDTO
overData.context.boardId
) { ) {
const { imageDTO } = activeData.payload; const { imageDTO } = activeData.payload;
const { boardId } = overData.context; const { boardId } = overData.context;
// if the board is "No Board", this is a remove action // image was droppe on the "NoBoardBoard"
if (boardId === 'no_board') { if (!boardId) {
dispatch( dispatch(
imagesApi.endpoints.removeImageFromBoard.initiate({ imagesApi.endpoints.removeImageFromBoard.initiate({
imageDTO, imageDTO,
@ -172,12 +171,7 @@ export const addImageDroppedListener = () => {
return; return;
} }
// Handle adding image to batch // image was dropped on a user board
if (boardId === 'batch') {
// TODO
}
// Otherwise, add the image to the board
dispatch( dispatch(
imagesApi.endpoints.addImageToBoard.initiate({ imagesApi.endpoints.addImageToBoard.initiate({
imageDTO, imageDTO,

View File

@ -5,30 +5,30 @@ import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'image' }); const moduleLog = log.child({ namespace: 'image' });
export const addImageUpdatedFulfilledListener = () => { export const addImageUpdatedFulfilledListener = () => {
startAppListening({ // startAppListening({
matcher: imagesApi.endpoints.updateImage.matchFulfilled, // matcher: imagesApi.endpoints.updateImage.matchFulfilled,
effect: (action, { dispatch, getState }) => { // effect: (action, { dispatch, getState }) => {
moduleLog.debug( // moduleLog.debug(
{ // {
data: { // data: {
oldImage: action.meta.arg.originalArgs, // oldImage: action.meta.arg.originalArgs,
updatedImage: action.payload, // updatedImage: action.payload,
}, // },
}, // },
'Image updated' // 'Image updated'
); // );
}, // },
}); // });
}; };
export const addImageUpdatedRejectedListener = () => { export const addImageUpdatedRejectedListener = () => {
startAppListening({ // startAppListening({
matcher: imagesApi.endpoints.updateImage.matchRejected, // matcher: imagesApi.endpoints.updateImage.matchRejected,
effect: (action, { dispatch }) => { // effect: (action, { dispatch }) => {
moduleLog.debug( // moduleLog.debug(
{ data: action.meta.arg.originalArgs }, // { data: action.meta.arg.originalArgs },
'Image update failed' // 'Image update failed'
); // );
}, // },
}); // });
}; };

View File

@ -8,10 +8,7 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { boardsApi } from 'services/api/endpoints/boards'; import { boardsApi } from 'services/api/endpoints/boards';
import { startAppListening } from '..'; import { startAppListening } from '..';
import { import { imagesApi } from '../../../../../services/api/endpoints/images';
SYSTEM_BOARDS,
imagesApi,
} from '../../../../../services/api/endpoints/images';
const moduleLog = log.child({ namespace: 'image' }); const moduleLog = log.child({ namespace: 'image' });
@ -26,7 +23,7 @@ export const addImageUploadedFulfilledListener = () => {
effect: (action, { dispatch, getState }) => { effect: (action, { dispatch, getState }) => {
const imageDTO = action.payload; const imageDTO = action.payload;
const state = getState(); const state = getState();
const { selectedBoardId } = state.gallery; const { selectedBoardId, autoAddBoardId } = state.gallery;
moduleLog.debug({ arg: '<Blob>', imageDTO }, 'Image uploaded'); moduleLog.debug({ arg: '<Blob>', imageDTO }, 'Image uploaded');
@ -44,13 +41,13 @@ export const addImageUploadedFulfilledListener = () => {
// default action - just upload and alert user // default action - just upload and alert user
if (postUploadAction?.type === 'TOAST') { if (postUploadAction?.type === 'TOAST') {
const { toastOptions } = postUploadAction; const { toastOptions } = postUploadAction;
if (SYSTEM_BOARDS.includes(selectedBoardId)) { if (!autoAddBoardId) {
dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions })); dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
} else { } else {
// Add this image to the board // Add this image to the board
dispatch( dispatch(
imagesApi.endpoints.addImageToBoard.initiate({ imagesApi.endpoints.addImageToBoard.initiate({
board_id: selectedBoardId, board_id: autoAddBoardId,
imageDTO, imageDTO,
}) })
); );
@ -59,10 +56,10 @@ export const addImageUploadedFulfilledListener = () => {
const { data } = boardsApi.endpoints.listAllBoards.select()(state); const { data } = boardsApi.endpoints.listAllBoards.select()(state);
// Fall back to just the board id if we can't find the board for some reason // Fall back to just the board id if we can't find the board for some reason
const board = data?.find((b) => b.board_id === selectedBoardId); const board = data?.find((b) => b.board_id === autoAddBoardId);
const description = board const description = board
? `Added to board ${board.board_name}` ? `Added to board ${board.board_name}`
: `Added to board ${selectedBoardId}`; : `Added to board ${autoAddBoardId}`;
dispatch( dispatch(
addToast({ addToast({

View File

@ -3,6 +3,7 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import { import {
IMAGE_CATEGORIES, IMAGE_CATEGORIES,
boardIdSelected, boardIdSelected,
galleryViewChanged,
imageSelected, imageSelected,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { progressImageSet } from 'features/system/store/systemSlice'; import { progressImageSet } from 'features/system/store/systemSlice';
@ -55,37 +56,16 @@ export const addInvocationCompleteEventListener = () => {
} }
if (!imageDTO.is_intermediate) { if (!imageDTO.is_intermediate) {
// update the cache for 'All Images' /**
dispatch( * Cache updates for when an image result is received
imagesApi.util.updateQueryData( * - *add* to getImageDTO
'listImages', * - IF `autoAddBoardId` is set:
{ * - THEN add it to the board_id/images
categories: IMAGE_CATEGORIES, * - ELSE (`autoAddBoardId` is not set):
}, * - THEN add it to the no_board/images
(draft) => { */
imagesAdapter.addOne(draft, imageDTO);
draft.total = draft.total + 1;
}
)
);
// update the cache for 'No Board'
dispatch(
imagesApi.util.updateQueryData(
'listImages',
{
board_id: 'none',
},
(draft) => {
imagesAdapter.addOne(draft, imageDTO);
draft.total = draft.total + 1;
}
)
);
const { autoAddBoardId } = gallery; const { autoAddBoardId } = gallery;
// add image to the board if auto-add is enabled
if (autoAddBoardId) { if (autoAddBoardId) {
dispatch( dispatch(
imagesApi.endpoints.addImageToBoard.initiate({ imagesApi.endpoints.addImageToBoard.initiate({
@ -93,8 +73,31 @@ export const addInvocationCompleteEventListener = () => {
imageDTO, imageDTO,
}) })
); );
} else {
dispatch(
imagesApi.util.updateQueryData(
'listImages',
{
board_id: 'none',
categories: IMAGE_CATEGORIES,
},
(draft) => {
const oldTotal = draft.total;
const newState = imagesAdapter.addOne(draft, imageDTO);
const delta = newState.total - oldTotal;
draft.total = draft.total + delta;
}
)
);
} }
dispatch(
imagesApi.util.invalidateTags([
{ type: 'BoardImagesTotal', id: autoAddBoardId ?? 'none' },
{ type: 'BoardAssetsTotal', id: autoAddBoardId ?? 'none' },
])
);
const { selectedBoardId, shouldAutoSwitch } = gallery; const { selectedBoardId, shouldAutoSwitch } = gallery;
// If auto-switch is enabled, select the new image // If auto-switch is enabled, select the new image
@ -102,8 +105,9 @@ export const addInvocationCompleteEventListener = () => {
// if auto-add is enabled, switch the board as the image comes in // if auto-add is enabled, switch the board as the image comes in
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) { if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
dispatch(boardIdSelected(autoAddBoardId)); dispatch(boardIdSelected(autoAddBoardId));
dispatch(galleryViewChanged('images'));
} else if (!autoAddBoardId) { } else if (!autoAddBoardId) {
dispatch(boardIdSelected('images')); dispatch(galleryViewChanged('images'));
} }
dispatch(imageSelected(imageDTO.image_name)); dispatch(imageSelected(imageDTO.image_name));
} }

View File

@ -12,25 +12,35 @@ export const addStagingAreaImageSavedListener = () => {
effect: async (action, { dispatch, getState, take }) => { effect: async (action, { dispatch, getState, take }) => {
const { imageDTO } = action.payload; const { imageDTO } = action.payload;
dispatch( try {
imagesApi.endpoints.updateImage.initiate({ const newImageDTO = await dispatch(
imageDTO, imagesApi.endpoints.changeImageIsIntermediate.initiate({
changes: { is_intermediate: false }, imageDTO,
}) is_intermediate: false,
) })
.unwrap() ).unwrap();
.then((image) => {
dispatch(addToast({ title: 'Image Saved', status: 'success' })); // we may need to add it to the autoadd board
}) const { autoAddBoardId } = getState().gallery;
.catch((error) => {
dispatch( if (autoAddBoardId) {
addToast({ await dispatch(
title: 'Image Saving Failed', imagesApi.endpoints.addImageToBoard.initiate({
description: error.message, imageDTO: newImageDTO,
status: 'error', board_id: autoAddBoardId,
}) })
); );
}); }
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
} catch (error) {
dispatch(
addToast({
title: 'Image Saving Failed',
description: (error as Error)?.message,
status: 'error',
})
);
}
}, },
}); });
}; };

View File

@ -73,7 +73,7 @@ export const addUserInvokedCanvasListener = () => {
// For img2img and inpaint/outpaint, we need to upload the init images // For img2img and inpaint/outpaint, we need to upload the init images
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) { if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id // upload the image, saving the request id
const { requestId: initImageUploadedRequestId } = dispatch( canvasInitImage = await dispatch(
imagesApi.endpoints.uploadImage.initiate({ imagesApi.endpoints.uploadImage.initiate({
file: new File([baseBlob], 'canvasInitImage.png', { file: new File([baseBlob], 'canvasInitImage.png', {
type: 'image/png', type: 'image/png',
@ -81,23 +81,13 @@ export const addUserInvokedCanvasListener = () => {
image_category: 'general', image_category: 'general',
is_intermediate: true, is_intermediate: true,
}) })
); ).unwrap();
// Wait for the image to be uploaded, matching by request id
const [{ payload }] = await take(
// TODO: figure out how to narrow this action's type
(action) =>
imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
action.meta.requestId === initImageUploadedRequestId
);
canvasInitImage = payload as ImageDTO;
} }
// For inpaint/outpaint, we also need to upload the mask layer // For inpaint/outpaint, we also need to upload the mask layer
if (['inpaint', 'outpaint'].includes(generationMode)) { if (['inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id // upload the image, saving the request id
const { requestId: maskImageUploadedRequestId } = dispatch( canvasMaskImage = await dispatch(
imagesApi.endpoints.uploadImage.initiate({ imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', { file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png', type: 'image/png',
@ -105,17 +95,7 @@ export const addUserInvokedCanvasListener = () => {
image_category: 'mask', image_category: 'mask',
is_intermediate: true, is_intermediate: true,
}) })
); ).unwrap();
// Wait for the image to be uploaded, matching by request id
const [{ payload }] = await take(
// TODO: figure out how to narrow this action's type
(action) =>
imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
action.meta.requestId === maskImageUploadedRequestId
);
canvasMaskImage = payload as ImageDTO;
} }
const graph = buildCanvasGraph( const graph = buildCanvasGraph(
@ -141,14 +121,14 @@ export const addUserInvokedCanvasListener = () => {
sessionCreated.fulfilled.match(action) && sessionCreated.fulfilled.match(action) &&
action.meta.requestId === sessionCreatedRequestId action.meta.requestId === sessionCreatedRequestId
); );
const sessionId = sessionCreatedAction.payload.id; const session_id = sessionCreatedAction.payload.id;
// Associate the init image with the session, now that we have the session ID // Associate the init image with the session, now that we have the session ID
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) { if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
dispatch( dispatch(
imagesApi.endpoints.updateImage.initiate({ imagesApi.endpoints.changeImageSessionId.initiate({
imageDTO: canvasInitImage, imageDTO: canvasInitImage,
changes: { session_id: sessionId }, session_id,
}) })
); );
} }
@ -156,9 +136,9 @@ export const addUserInvokedCanvasListener = () => {
// Associate the mask image with the session, now that we have the session ID // Associate the mask image with the session, now that we have the session ID
if (['inpaint'].includes(generationMode) && canvasMaskImage) { if (['inpaint'].includes(generationMode) && canvasMaskImage) {
dispatch( dispatch(
imagesApi.endpoints.updateImage.initiate({ imagesApi.endpoints.changeImageSessionId.initiate({
imageDTO: canvasMaskImage, imageDTO: canvasMaskImage,
changes: { session_id: sessionId }, session_id,
}) })
); );
} }
@ -167,7 +147,7 @@ export const addUserInvokedCanvasListener = () => {
if (!state.canvas.layerState.stagingArea.boundingBox) { if (!state.canvas.layerState.stagingArea.boundingBox) {
dispatch( dispatch(
stagingAreaInitialized({ stagingAreaInitialized({
sessionId, sessionId: session_id,
boundingBox: { boundingBox: {
...state.canvas.boundingBoxCoordinates, ...state.canvas.boundingBoxCoordinates,
...state.canvas.boundingBoxDimensions, ...state.canvas.boundingBoxDimensions,
@ -177,7 +157,7 @@ export const addUserInvokedCanvasListener = () => {
} }
// Flag the session with the canvas session ID // Flag the session with the canvas session ID
dispatch(canvasSessionIdChanged(sessionId)); dispatch(canvasSessionIdChanged(session_id));
// We are ready to invoke the session! // We are ready to invoke the session!
dispatch(sessionReadyToInvoke()); dispatch(sessionReadyToInvoke());

View File

@ -92,7 +92,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
sx={{ sx={{
p: 4, p: 4,
borderBottomRadius: 'base', borderBottomRadius: 'base',
bg: mode('base.100', 'base.800')(colorMode), bg: 'base.100',
_dark: {
bg: 'base.800',
},
}} }}
> >
{children} {children}

View File

@ -18,12 +18,20 @@ import {
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react'; import {
MouseEvent,
ReactElement,
SyntheticEvent,
memo,
useCallback,
useState,
} from 'react';
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa'; import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
import { ImageDTO, PostUploadAction } from 'services/api/types'; import { ImageDTO, PostUploadAction } from 'services/api/types';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
import IAIDraggable from './IAIDraggable'; import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable'; import IAIDroppable from './IAIDroppable';
import SelectionOverlay from './SelectionOverlay';
type IAIDndImageProps = { type IAIDndImageProps = {
imageDTO: ImageDTO | undefined; imageDTO: ImageDTO | undefined;
@ -49,6 +57,7 @@ type IAIDndImageProps = {
thumbnail?: boolean; thumbnail?: boolean;
noContentFallback?: ReactElement; noContentFallback?: ReactElement;
useThumbailFallback?: boolean; useThumbailFallback?: boolean;
withHoverOverlay?: boolean;
}; };
const IAIDndImage = (props: IAIDndImageProps) => { const IAIDndImage = (props: IAIDndImageProps) => {
@ -75,9 +84,17 @@ const IAIDndImage = (props: IAIDndImageProps) => {
resetIcon = <FaUndo />, resetIcon = <FaUndo />,
noContentFallback = <IAINoContentFallback icon={FaImage} />, noContentFallback = <IAINoContentFallback icon={FaImage} />,
useThumbailFallback, useThumbailFallback,
withHoverOverlay = false,
} = props; } = props;
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction, postUploadAction,
@ -105,6 +122,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
{(ref) => ( {(ref) => (
<Flex <Flex
ref={ref} ref={ref}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
sx={{ sx={{
width: 'full', width: 'full',
height: 'full', height: 'full',
@ -147,14 +166,14 @@ const IAIDndImage = (props: IAIDndImageProps) => {
maxW: 'full', maxW: 'full',
maxH: 'full', maxH: 'full',
borderRadius: 'base', borderRadius: 'base',
shadow: isSelected ? 'selected.light' : undefined,
_dark: {
shadow: isSelected ? 'selected.dark' : undefined,
},
...imageSx, ...imageSx,
}} }}
/> />
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />} {withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
<SelectionOverlay
isSelected={isSelected}
isHovered={withHoverOverlay ? isHovered : false}
/>
</Flex> </Flex>
)} )}
{!imageDTO && !isUploadDisabled && ( {!imageDTO && !isUploadDisabled && (

View File

@ -19,10 +19,11 @@ import { useUploadImageMutation } from 'services/api/endpoints/images';
import { PostUploadAction } from 'services/api/types'; import { PostUploadAction } from 'services/api/types';
import ImageUploadOverlay from './ImageUploadOverlay'; import ImageUploadOverlay from './ImageUploadOverlay';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { stateSelector } from 'app/store/store';
const selector = createSelector( const selector = createSelector(
[activeTabNameSelector], [stateSelector, activeTabNameSelector],
(activeTabName) => { ({ gallery }, activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' }; let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'unifiedCanvas') { if (activeTabName === 'unifiedCanvas') {
@ -33,7 +34,10 @@ const selector = createSelector(
postUploadAction = { type: 'SET_INITIAL_IMAGE' }; postUploadAction = { type: 'SET_INITIAL_IMAGE' };
} }
const { autoAddBoardId } = gallery;
return { return {
autoAddBoardId,
postUploadAction, postUploadAction,
}; };
}, },
@ -46,7 +50,7 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => { const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props; const { children } = props;
const { postUploadAction } = useAppSelector(selector); const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
@ -74,9 +78,10 @@ const ImageUploader = (props: ImageUploaderProps) => {
image_category: 'user', image_category: 'user',
is_intermediate: false, is_intermediate: false,
postUploadAction, postUploadAction,
board_id: autoAddBoardId,
}); });
}, },
[postUploadAction, uploadImage] [autoAddBoardId, postUploadAction, uploadImage]
); );
const onDrop = useCallback( const onDrop = useCallback(

View File

@ -0,0 +1,42 @@
import { Box } from '@chakra-ui/react';
type Props = {
isSelected: boolean;
isHovered: boolean;
};
const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
return (
<Box
className="selection-box"
sx={{
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
opacity: isSelected ? 1 : 0.7,
transitionProperty: 'common',
transitionDuration: '0.1s',
shadow: isSelected
? isHovered
? 'hoverSelected.light'
: 'selected.light'
: isHovered
? 'hoverUnselected.light'
: undefined,
_dark: {
shadow: isSelected
? isHovered
? 'hoverSelected.dark'
: 'selected.dark'
: isHovered
? 'hoverUnselected.dark'
: undefined,
},
}}
/>
);
};
export default SelectionOverlay;

View File

@ -1,3 +1,4 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { useUploadImageMutation } from 'services/api/endpoints/images'; import { useUploadImageMutation } from 'services/api/endpoints/images';
@ -31,6 +32,9 @@ export const useImageUploadButton = ({
postUploadAction, postUploadAction,
isDisabled, isDisabled,
}: UseImageUploadButtonArgs) => { }: UseImageUploadButtonArgs) => {
const autoAddBoardId = useAppSelector(
(state) => state.gallery.autoAddBoardId
);
const [uploadImage] = useUploadImageMutation(); const [uploadImage] = useUploadImageMutation();
const onDropAccepted = useCallback( const onDropAccepted = useCallback(
(files: File[]) => { (files: File[]) => {
@ -45,9 +49,10 @@ export const useImageUploadButton = ({
image_category: 'user', image_category: 'user',
is_intermediate: false, is_intermediate: false,
postUploadAction: postUploadAction ?? { type: 'TOAST' }, postUploadAction: postUploadAction ?? { type: 'TOAST' },
board_id: autoAddBoardId,
}); });
}, },
[postUploadAction, uploadImage] [autoAddBoardId, postUploadAction, uploadImage]
); );
const { const {

View File

@ -98,12 +98,16 @@ const ParamEmbeddingPopover = (props: Props) => {
sx={{ p: 0, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }} sx={{ p: 0, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }}
> >
{data.length === 0 ? ( {data.length === 0 ? (
<Flex sx={{ justifyContent: 'center', p: 2 }}> <Flex
<Text sx={{
sx={{ fontSize: 'sm', color: 'base.500', _dark: 'base.700' }} justifyContent: 'center',
> p: 2,
No Embeddings Loaded fontSize: 'sm',
</Text> color: 'base.500',
_dark: { color: 'base.700' },
}}
>
<Text>No Embeddings Loaded</Text>
</Flex> </Flex>
) : ( ) : (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect

View File

@ -0,0 +1,23 @@
import { Badge, Flex } from '@chakra-ui/react';
const AutoAddIcon = () => {
return (
<Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
top: 0,
p: 1,
}}
>
<Badge
variant="solid"
sx={{ bg: 'accent.400', _dark: { bg: 'accent.500' } }}
>
auto
</Badge>
</Flex>
);
};
export default AutoAddIcon;

View File

@ -52,7 +52,7 @@ const BoardAutoAddSelect = () => {
return; return;
} }
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v)); dispatch(autoAddBoardIdChanged(v === 'none' ? undefined : v));
}, },
[dispatch] [dispatch]
); );

View File

@ -1,17 +1,23 @@
import { Box, MenuItem, MenuList } from '@chakra-ui/react'; import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import {
import { memo, useCallback } from 'react'; autoAddBoardIdChanged,
import { FaFolder } from 'react-icons/fa'; boardIdSelected,
} from 'features/gallery/store/gallerySlice';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { FaFolder, FaPlus } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types'; import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu'; import { menuListMotionProps } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems'; import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems'; import NoBoardContextMenuItems from './NoBoardContextMenuItems';
import { useBoardName } from 'services/api/hooks/useBoardName';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
type Props = { type Props = {
board?: BoardDTO; board?: BoardDTO;
board_id: string; board_id?: string;
children: ContextMenuProps<HTMLDivElement>['children']; children: ContextMenuProps<HTMLDivElement>['children'];
setBoardToDelete?: (board?: BoardDTO) => void; setBoardToDelete?: (board?: BoardDTO) => void;
}; };
@ -19,9 +25,32 @@ type Props = {
const BoardContextMenu = memo( const BoardContextMenu = memo(
({ board, board_id, setBoardToDelete, children }: Props) => { ({ board, board_id, setBoardToDelete, children }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(stateSelector, ({ gallery }) => {
const isSelected = gallery.selectedBoardId === board_id;
const isAutoAdd = gallery.autoAddBoardId === board_id;
return { isSelected, isAutoAdd };
}),
[board_id]
);
const { isSelected, isAutoAdd } = useAppSelector(selector);
const boardName = useBoardName(board_id);
const handleSelectBoard = useCallback(() => { const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board?.board_id ?? board_id)); dispatch(boardIdSelected(board_id));
}, [board?.board_id, board_id, dispatch]); }, [board_id, dispatch]);
const handleSetAutoAdd = useCallback(() => {
dispatch(autoAddBoardIdChanged(board_id));
}, [board_id, dispatch]);
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
}, []);
return ( return (
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
@ -33,17 +62,24 @@ const BoardContextMenu = memo(
<MenuList <MenuList
sx={{ visibility: 'visible !important' }} sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps} motionProps={menuListMotionProps}
onContextMenu={skipEvent}
> >
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}> <MenuGroup title={boardName}>
Select Board <MenuItem
</MenuItem> icon={<FaPlus />}
{!board && <SystemBoardContextMenuItems board_id={board_id} />} isDisabled={isAutoAdd}
{board && ( onClick={handleSetAutoAdd}
<GalleryBoardContextMenuItems >
board={board} Auto-add to this Board
setBoardToDelete={setBoardToDelete} </MenuItem>
/> {!board && <NoBoardContextMenuItems />}
)} {board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuGroup>
</MenuList> </MenuList>
)} )}
> >

View File

@ -1,51 +0,0 @@
import {
ASSETS_CATEGORIES,
INITIAL_IMAGE_LIMIT,
boardIdSelected,
} from 'features/gallery/store/gallerySlice';
import { FaFileImage } from 'react-icons/fa';
import { useDispatch } from 'react-redux';
import {
ListImagesArgs,
useListImagesQuery,
} from 'services/api/endpoints/images';
import GenericBoard from './GenericBoard';
const baseQueryArg: ListImagesArgs = {
categories: ASSETS_CATEGORIES,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
is_intermediate: false,
};
const AllAssetsBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch();
const handleClick = () => {
dispatch(boardIdSelected('assets'));
};
const { total } = useListImagesQuery(baseQueryArg, {
selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
});
// TODO: Do we support making 'images' 'assets? if yes, we need to handle this
// const droppableData: MoveBoardDropData = {
// id: 'all-images-board',
// actionType: 'MOVE_BOARD',
// context: { boardId: 'assets' },
// };
return (
<GenericBoard
board_id="assets"
onClick={handleClick}
isSelected={isSelected}
icon={FaFileImage}
label="All Assets"
badgeCount={total}
/>
);
};
export default AllAssetsBoard;

View File

@ -1,51 +0,0 @@
import {
IMAGE_CATEGORIES,
INITIAL_IMAGE_LIMIT,
boardIdSelected,
} from 'features/gallery/store/gallerySlice';
import { FaImages } from 'react-icons/fa';
import { useDispatch } from 'react-redux';
import {
ListImagesArgs,
useListImagesQuery,
} from 'services/api/endpoints/images';
import GenericBoard from './GenericBoard';
const baseQueryArg: ListImagesArgs = {
categories: IMAGE_CATEGORIES,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
is_intermediate: false,
};
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch();
const handleClick = () => {
dispatch(boardIdSelected('images'));
};
const { total } = useListImagesQuery(baseQueryArg, {
selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
});
// TODO: Do we support making 'images' 'assets? if yes, we need to handle this
// const droppableData: MoveBoardDropData = {
// id: 'all-images-board',
// actionType: 'MOVE_BOARD',
// context: { boardId: 'images' },
// };
return (
<GenericBoard
board_id="images"
onClick={handleClick}
isSelected={isSelected}
icon={FaImages}
label="All Images"
badgeCount={total}
/>
);
};
export default AllImagesBoard;

View File

@ -16,6 +16,7 @@ import AddBoardButton from './AddBoardButton';
import BoardsSearch from './BoardsSearch'; import BoardsSearch from './BoardsSearch';
import GalleryBoard from './GalleryBoard'; import GalleryBoard from './GalleryBoard';
import SystemBoardButton from './SystemBoardButton'; import SystemBoardButton from './SystemBoardButton';
import NoBoardBoard from './NoBoardBoard';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -42,10 +43,6 @@ const BoardsList = (props: Props) => {
) )
: boards; : boards;
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>(); const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
const [isSearching, setIsSearching] = useState(false);
const handleClickSearchIcon = useCallback(() => {
setIsSearching((v) => !v);
}, []);
return ( return (
<> <>
@ -61,54 +58,7 @@ const BoardsList = (props: Props) => {
}} }}
> >
<Flex sx={{ gap: 2, alignItems: 'center' }}> <Flex sx={{ gap: 2, alignItems: 'center' }}>
<AnimatePresence mode="popLayout"> <BoardsSearch />
{isSearching ? (
<motion.div
key="boards-search"
initial={{
opacity: 0,
}}
exit={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
style={{ width: '100%' }}
>
<BoardsSearch setIsSearching={setIsSearching} />
</motion.div>
) : (
<motion.div
key="system-boards-select"
initial={{
opacity: 0,
}}
exit={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
style={{ width: '100%' }}
>
<ButtonGroup sx={{ w: 'full', ps: 1.5 }} isAttached>
<SystemBoardButton board_id="images" />
<SystemBoardButton board_id="assets" />
<SystemBoardButton board_id="no_board" />
</ButtonGroup>
</motion.div>
)}
</AnimatePresence>
<IAIIconButton
aria-label="Search Boards"
size="sm"
isChecked={isSearching}
onClick={handleClickSearchIcon}
icon={<FaSearch />}
/>
<AddBoardButton /> <AddBoardButton />
</Flex> </Flex>
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
@ -126,10 +76,13 @@ const BoardsList = (props: Props) => {
<Grid <Grid
className="list-container" className="list-container"
sx={{ sx={{
gridTemplateColumns: `repeat(auto-fill, minmax(96px, 1fr));`, gridTemplateColumns: `repeat(auto-fill, minmax(108px, 1fr));`,
maxH: 346, maxH: 346,
}} }}
> >
<GridItem sx={{ p: 1.5 }}>
<NoBoardBoard isSelected={selectedBoardId === undefined} />
</GridItem>
{filteredBoards && {filteredBoards &&
filteredBoards.map((board) => ( filteredBoards.map((board) => (
<GridItem key={board.board_id} sx={{ p: 1.5 }}> <GridItem key={board.board_id} sx={{ p: 1.5 }}>

View File

@ -28,12 +28,7 @@ const selector = createSelector(
defaultSelectorOptions defaultSelectorOptions
); );
type Props = { const BoardsSearch = () => {
setIsSearching: (isSearching: boolean) => void;
};
const BoardsSearch = (props: Props) => {
const { setIsSearching } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { searchText } = useAppSelector(selector); const { searchText } = useAppSelector(selector);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -47,8 +42,7 @@ const BoardsSearch = (props: Props) => {
const clearBoardSearch = useCallback(() => { const clearBoardSearch = useCallback(() => {
dispatch(setBoardSearchText('')); dispatch(setBoardSearchText(''));
setIsSearching(false); }, [dispatch]);
}, [dispatch, setIsSearching]);
const handleKeydown = useCallback( const handleKeydown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {

View File

@ -19,16 +19,14 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { FaFolder } from 'react-icons/fa'; import { FaUser } from 'react-icons/fa';
import { useUpdateBoardMutation } from 'services/api/endpoints/boards'; import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import { BoardDTO } from 'services/api/types'; import { BoardDTO } from 'services/api/types';
import AutoAddIcon from '../AutoAddIcon';
import BoardContextMenu from '../BoardContextMenu'; import BoardContextMenu from '../BoardContextMenu';
import SelectionOverlay from 'common/components/SelectionOverlay';
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'accent.200',
color: 'blackAlpha.900',
};
const BASE_BADGE_STYLES: ChakraProps['sx'] = { const BASE_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'base.500', bg: 'base.500',
@ -59,11 +57,19 @@ const GalleryBoard = memo(
); );
const { isSelectedForAutoAdd } = useAppSelector(selector); const { isSelectedForAutoAdd } = useAppSelector(selector);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { currentData: coverImage } = useGetImageDTOQuery( const { currentData: coverImage } = useGetImageDTOQuery(
board.cover_image_name ?? skipToken board.cover_image_name ?? skipToken
); );
const { totalImages, totalAssets } = useBoardTotal(board.board_id);
const { board_name, board_id } = board; const { board_name, board_id } = board;
const [localBoardName, setLocalBoardName] = useState(board_name); const [localBoardName, setLocalBoardName] = useState(board_name);
@ -84,26 +90,30 @@ const GalleryBoard = memo(
); );
const handleSubmit = useCallback( const handleSubmit = useCallback(
(newBoardName: string) => { async (newBoardName: string) => {
if (!newBoardName) { // empty strings are not allowed
// empty strings are not allowed if (!newBoardName.trim()) {
setLocalBoardName(board_name); setLocalBoardName(board_name);
return; return;
} }
// don't updated the board name if it hasn't changed
if (newBoardName === board_name) { if (newBoardName === board_name) {
// don't updated the board name if it hasn't changed
return; return;
} }
updateBoard({ board_id, changes: { board_name: newBoardName } })
.unwrap() try {
.then((response) => { const { board_name } = await updateBoard({
// update local state board_id,
setLocalBoardName(response.board_name); changes: { board_name: newBoardName },
}) }).unwrap();
.catch(() => {
// revert on error // update local state
setLocalBoardName(board_name); setLocalBoardName(board_name);
}); } catch {
// revert on error
setLocalBoardName(board_name);
}
}, },
[board_id, board_name, updateBoard] [board_id, board_name, updateBoard]
); );
@ -117,6 +127,8 @@ const GalleryBoard = memo(
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }} sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
> >
<Flex <Flex
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
sx={{ sx={{
position: 'relative', position: 'relative',
justifyContent: 'center', justifyContent: 'center',
@ -143,57 +155,49 @@ const GalleryBoard = memo(
alignItems: 'center', alignItems: 'center',
borderRadius: 'base', borderRadius: 'base',
cursor: 'pointer', cursor: 'pointer',
bg: 'base.200',
_dark: {
bg: 'base.800',
},
}} }}
> >
<Flex {coverImage?.thumbnail_url ? (
sx={{ <Image
w: 'full', src={coverImage?.thumbnail_url}
h: 'full', draggable={false}
justifyContent: 'center', sx={{
alignItems: 'center', objectFit: 'cover',
borderRadius: 'base', w: 'full',
bg: 'base.200', h: 'full',
_dark: { maxH: 'full',
bg: 'base.800', borderRadius: 'base',
}, borderBottomRadius: 'lg',
}} }}
> />
{coverImage?.thumbnail_url ? ( ) : (
<Image <Flex
src={coverImage?.thumbnail_url} sx={{
draggable={false} w: 'full',
h: 'full',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Icon
boxSize={12}
as={FaUser}
sx={{ sx={{
maxW: 'full', mt: -6,
maxH: 'full', opacity: 0.7,
borderRadius: 'base', color: 'base.500',
borderBottomRadius: 'lg', _dark: {
color: 'base.500',
},
}} }}
/> />
) : ( </Flex>
<Flex )}
sx={{ {/* <Flex
w: 'full',
h: 'full',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Icon
boxSize={12}
as={FaFolder}
sx={{
mt: -3,
opacity: 0.7,
color: 'base.500',
_dark: {
color: 'base.500',
},
}}
/>
</Flex>
)}
</Flex>
<Flex
sx={{ sx={{
position: 'absolute', position: 'absolute',
insetInlineEnd: 0, insetInlineEnd: 0,
@ -201,33 +205,14 @@ const GalleryBoard = memo(
p: 1, p: 1,
}} }}
> >
<Badge <Badge variant="solid" sx={BASE_BADGE_STYLES}>
variant="solid" {totalImages}/{totalAssets}
sx={
isSelectedForAutoAdd
? AUTO_ADD_BADGE_STYLES
: BASE_BADGE_STYLES
}
>
{board.image_count}
</Badge> </Badge>
</Flex> </Flex> */}
<Box {isSelectedForAutoAdd && <AutoAddIcon />}
className="selection-box" <SelectionOverlay
sx={{ isSelected={isSelected}
position: 'absolute', isHovered={isHovered}
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: 'common',
shadow: isSelected ? 'selected.light' : undefined,
_dark: {
shadow: isSelected ? 'selected.dark' : undefined,
},
}}
/> />
<Flex <Flex
sx={{ sx={{

View File

@ -1,54 +1,179 @@
import { Text } from '@chakra-ui/react'; import { Box, ChakraProps, Flex, Image, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd'; import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
import { import { stateSelector } from 'app/store/store';
INITIAL_IMAGE_LIMIT, import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
boardIdSelected, import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
} from 'features/gallery/store/gallerySlice'; import InvokeAILogoImage from 'assets/images/logo.png';
import { FaFolderOpen } from 'react-icons/fa'; import IAIDroppable from 'common/components/IAIDroppable';
import { useDispatch } from 'react-redux'; import SelectionOverlay from 'common/components/SelectionOverlay';
import { import { boardIdSelected } from 'features/gallery/store/gallerySlice';
ListImagesArgs, import { memo, useCallback, useMemo, useState } from 'react';
useListImagesQuery, import { useBoardName } from 'services/api/hooks/useBoardName';
} from 'services/api/endpoints/images'; import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import GenericBoard from './GenericBoard'; import AutoAddIcon from '../AutoAddIcon';
import BoardContextMenu from '../BoardContextMenu';
const baseQueryArg: ListImagesArgs = { const BASE_BADGE_STYLES: ChakraProps['sx'] = {
board_id: 'none', bg: 'base.500',
offset: 0, color: 'whiteAlpha.900',
limit: INITIAL_IMAGE_LIMIT,
is_intermediate: false,
}; };
interface Props {
isSelected: boolean;
}
const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => { const selector = createSelector(
const dispatch = useDispatch(); stateSelector,
({ gallery }) => {
const { autoAddBoardId } = gallery;
return { autoAddBoardId };
},
defaultSelectorOptions
);
const handleClick = () => { const NoBoardBoard = memo(({ isSelected }: Props) => {
dispatch(boardIdSelected('no_board')); const dispatch = useAppDispatch();
}; const { totalImages, totalAssets } = useBoardTotal(undefined);
const { autoAddBoardId } = useAppSelector(selector);
const boardName = useBoardName(undefined);
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(undefined));
}, [dispatch]);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { total } = useListImagesQuery(baseQueryArg, { const droppableData: MoveBoardDropData = useMemo(
selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }), () => ({
}); id: 'no_board',
actionType: 'MOVE_BOARD',
// TODO: Do we support making 'images' 'assets? if yes, we need to handle this context: { boardId: undefined },
const droppableData: MoveBoardDropData = { }),
id: 'all-images-board', []
actionType: 'MOVE_BOARD', );
context: { boardId: 'no_board' },
};
return ( return (
<GenericBoard <Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
board_id="no_board" <Flex
droppableData={droppableData} onMouseOver={handleMouseOver}
dropLabel={<Text fontSize="md">Move</Text>} onMouseOut={handleMouseOut}
onClick={handleClick} sx={{
isSelected={isSelected} position: 'relative',
icon={FaFolderOpen} justifyContent: 'center',
label="No Board" alignItems: 'center',
badgeCount={total} aspectRatio: '1/1',
/> borderRadius: 'base',
w: 'full',
h: 'full',
}}
>
<BoardContextMenu>
{(ref) => (
<Flex
ref={ref}
onClick={handleSelectBoard}
sx={{
w: 'full',
h: 'full',
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
cursor: 'pointer',
bg: 'base.200',
_dark: {
bg: 'base.800',
},
}}
>
<Flex
sx={{
w: 'full',
h: 'full',
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* <Icon
boxSize={12}
as={FaBucket}
sx={{
opacity: 0.7,
color: 'base.500',
_dark: {
color: 'base.500',
},
}}
/> */}
<Image
src={InvokeAILogoImage}
alt="invoke-ai-logo"
sx={{
opacity: 0.4,
filter: 'grayscale(1)',
mt: -6,
w: 16,
h: 16,
minW: 16,
minH: 16,
userSelect: 'none',
}}
/>
</Flex>
{/* <Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
top: 0,
p: 1,
}}
>
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
{totalImages}/{totalAssets}
</Badge>
</Flex> */}
{!autoAddBoardId && <AutoAddIcon />}
<Flex
sx={{
position: 'absolute',
bottom: 0,
left: 0,
p: 1,
justifyContent: 'center',
alignItems: 'center',
w: 'full',
maxW: 'full',
borderBottomRadius: 'base',
bg: isSelected ? 'accent.400' : 'base.500',
color: isSelected ? 'base.50' : 'base.100',
_dark: {
bg: isSelected ? 'accent.500' : 'base.600',
color: isSelected ? 'base.50' : 'base.100',
},
lineHeight: 'short',
fontSize: 'xs',
fontWeight: isSelected ? 700 : 500,
}}
>
{boardName}
</Flex>
<SelectionOverlay isSelected={isSelected} isHovered={isHovered} />
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
/>
</Flex>
)}
</BoardContextMenu>
</Flex>
</Box>
); );
}; });
NoBoardBoard.displayName = 'HoverableBoard';
export default NoBoardBoard; export default NoBoardBoard;

View File

@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice'; import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { FaMinus, FaPlus, FaTrash } from 'react-icons/fa'; import { FaPlus, FaTrash } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types'; import { BoardDTO } from 'services/api/types';
type Props = { type Props = {
@ -42,7 +42,7 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
const handleToggleAutoAdd = useCallback(() => { const handleToggleAutoAdd = useCallback(() => {
dispatch( dispatch(
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id) autoAddBoardIdChanged(isSelectedForAutoAdd ? undefined : board.board_id)
); );
}, [board.board_id, dispatch, isSelectedForAutoAdd]); }, [board.board_id, dispatch, isSelectedForAutoAdd]);
@ -59,16 +59,15 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
</MenuItem> */} </MenuItem> */}
</> </>
)} )}
<MenuItem {/* {!isSelectedForAutoAdd && (
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />} <MenuItem icon={<FaPlus />} onClick={handleToggleAutoAdd}>
onClickCapture={handleToggleAutoAdd} Auto-add to this Board
> </MenuItem>
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'} )} */}
</MenuItem>
<MenuItem <MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }} sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />} icon={<FaTrash />}
onClickCapture={handleDelete} onClick={handleDelete}
> >
Delete Board Delete Board
</MenuItem> </MenuItem>

View File

@ -0,0 +1,28 @@
import { MenuItem } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
const NoBoardContextMenuItems = () => {
const dispatch = useAppDispatch();
const autoAddBoardId = useAppSelector(
(state) => state.gallery.autoAddBoardId
);
const handleDisableAutoAdd = useCallback(() => {
dispatch(autoAddBoardIdChanged(undefined));
}, [dispatch]);
return (
<>
{/* {autoAddBoardId && (
<MenuItem icon={<FaPlus />} onClick={handleDisableAutoAdd}>
Auto-add to this Board
</MenuItem>
)} */}
</>
);
};
export default memo(NoBoardContextMenuItems);

View File

@ -1,12 +0,0 @@
import { BoardId } from 'features/gallery/store/gallerySlice';
import { memo } from 'react';
type Props = {
board_id: BoardId;
};
const SystemBoardContextMenuItems = ({ board_id }: Props) => {
return <></>;
};
export default memo(SystemBoardContextMenuItems);

View File

@ -1,12 +1,11 @@
import { ChevronUpIcon } from '@chakra-ui/icons'; import { ChevronUpIcon } from '@chakra-ui/icons';
import { Box, Button, Flex, Spacer, Text } from '@chakra-ui/react'; import { Button, Flex, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -27,52 +26,64 @@ const GalleryBoardName = (props: Props) => {
const { isOpen, onToggle } = props; const { isOpen, onToggle } = props;
const { selectedBoardId } = useAppSelector(selector); const { selectedBoardId } = useAppSelector(selector);
const boardName = useBoardName(selectedBoardId); const boardName = useBoardName(selectedBoardId);
const numOfBoardImages = useBoardTotal(selectedBoardId); // const { totalImages, totalAssets } = useBoardTotal(selectedBoardId);
const formattedBoardName = useMemo(() => { const formattedBoardName = useMemo(() => {
if (!boardName) return '';
if (boardName && !numOfBoardImages) return boardName;
if (boardName.length > 20) { if (boardName.length > 20) {
return `${boardName.substring(0, 20)}... (${numOfBoardImages})`; return `${boardName.substring(0, 20)}...`;
} }
return `${boardName} (${numOfBoardImages})`; return boardName;
}, [boardName, numOfBoardImages]); // if (!boardName) {
// return '';
// }
// if (boardName && (totalImages === undefined || totalAssets === undefined)) {
// return boardName;
// }
// const count = `${totalImages}/${totalAssets}`;
// if (boardName.length > 20) {
// return `${boardName.substring(0, 20)}... (${count})`;
// }
// return `${boardName} (${count})`;
}, [boardName]);
return ( return (
<Flex <Flex
as={Button} as={Button}
onClick={onToggle} onClick={onToggle}
size="sm" size="sm"
variant="ghost" // variant="ghost"
sx={{ sx={{
position: 'relative', position: 'relative',
gap: 2, gap: 2,
w: 'full', w: 'full',
justifyContent: 'center', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
px: 2, px: 2,
_hover: { // bg: 'base.100',
bg: 'base.100', // _dark: { bg: 'base.800' },
_dark: { bg: 'base.800' }, // _hover: {
}, // bg: 'base.200',
// _dark: { bg: 'base.700' },
// },
}} }}
> >
<Spacer /> <Text
<Box position="relative"> noOfLines={1}
<Text sx={{
noOfLines={1} fontWeight: 600,
sx={{ w: '100%',
fontWeight: 600, textAlign: 'center',
color: 'base.800', color: 'base.800',
_dark: { _dark: {
color: 'base.200', color: 'base.200',
}, },
}} }}
> >
{formattedBoardName} {formattedBoardName}
</Text> </Text>
</Box>
<Spacer />
<ChevronUpIcon <ChevronUpIcon
sx={{ sx={{
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)', transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',

View File

@ -35,6 +35,8 @@ import {
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext'; import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
import { useDebounce } from 'use-debounce';
import { skipToken } from '@reduxjs/toolkit/dist/query';
type SingleSelectionMenuItemsProps = { type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO; imageDTO: ImageDTO;
@ -70,7 +72,16 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { onClickAddToBoard } = useContext(AddImageToBoardContext); const { onClickAddToBoard } = useContext(AddImageToBoardContext);
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name); const [debouncedMetadataQueryArg, debounceState] = useDebounce(
imageDTO.image_name,
500
);
const { currentData } = useGetImageMetadataQuery(
debounceState.isPending()
? skipToken
: debouncedMetadataQueryArg ?? skipToken
);
const { isClipboardAPIAvailable, copyImageToClipboard } = const { isClipboardAPIAvailable, copyImageToClipboard } =
useCopyImageToClipboard(); useCopyImageToClipboard();

View File

@ -1,23 +1,38 @@
import { Box, Flex, VStack, useDisclosure } from '@chakra-ui/react'; import {
Box,
Button,
ButtonGroup,
Flex,
Spacer,
Tab,
TabList,
Tabs,
VStack,
useDisclosure,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { memo, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import BoardsList from './Boards/BoardsList/BoardsList'; import BoardsList from './Boards/BoardsList/BoardsList';
import GalleryBoardName from './GalleryBoardName'; import GalleryBoardName from './GalleryBoardName';
import GalleryPinButton from './GalleryPinButton'; import GalleryPinButton from './GalleryPinButton';
import GallerySettingsPopover from './GallerySettingsPopover'; import GallerySettingsPopover from './GallerySettingsPopover';
import BatchImageGrid from './ImageGrid/BatchImageGrid'; import BatchImageGrid from './ImageGrid/BatchImageGrid';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid'; import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import IAIButton from 'common/components/IAIButton';
import { FaImages, FaServer } from 'react-icons/fa';
import { galleryViewChanged } from '../store/gallerySlice';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
(state) => { (state) => {
const { selectedBoardId } = state.gallery; const { selectedBoardId, galleryView } = state.gallery;
return { return {
selectedBoardId, selectedBoardId,
galleryView,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -26,10 +41,19 @@ const selector = createSelector(
const ImageGalleryContent = () => { const ImageGalleryContent = () => {
const resizeObserverRef = useRef<HTMLDivElement>(null); const resizeObserverRef = useRef<HTMLDivElement>(null);
const galleryGridRef = useRef<HTMLDivElement>(null); const galleryGridRef = useRef<HTMLDivElement>(null);
const { selectedBoardId } = useAppSelector(selector); const { selectedBoardId, galleryView } = useAppSelector(selector);
const dispatch = useAppDispatch();
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } = const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
useDisclosure(); useDisclosure();
const handleClickImages = useCallback(() => {
dispatch(galleryViewChanged('images'));
}, [dispatch]);
const handleClickAssets = useCallback(() => {
dispatch(galleryViewChanged('assets'));
}, [dispatch]);
return ( return (
<VStack <VStack
sx={{ sx={{
@ -48,11 +72,11 @@ const ImageGalleryContent = () => {
gap: 2, gap: 2,
}} }}
> >
<GallerySettingsPopover />
<GalleryBoardName <GalleryBoardName
isOpen={isBoardListOpen} isOpen={isBoardListOpen}
onToggle={onToggleBoardList} onToggle={onToggleBoardList}
/> />
<GallerySettingsPopover />
<GalleryPinButton /> <GalleryPinButton />
</Flex> </Flex>
<Box> <Box>
@ -60,6 +84,55 @@ const ImageGalleryContent = () => {
</Box> </Box>
</Box> </Box>
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full"> <Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
<Flex
sx={{
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
}}
>
<Tabs
index={galleryView === 'images' ? 0 : 1}
variant="unstyled"
size="sm"
sx={{ w: 'full' }}
>
<TabList>
<ButtonGroup
isAttached
sx={{
w: 'full',
}}
>
<Tab
as={IAIButton}
size="sm"
isChecked={galleryView === 'images'}
onClick={handleClickImages}
sx={{
w: 'full',
}}
leftIcon={<FaImages />}
>
Images
</Tab>
<Tab
as={IAIButton}
size="sm"
isChecked={galleryView === 'assets'}
onClick={handleClickAssets}
sx={{
w: 'full',
}}
leftIcon={<FaServer />}
>
Assets
</Tab>
</ButtonGroup>
</TabList>
</Tabs>
</Flex>
{selectedBoardId === 'batch' ? ( {selectedBoardId === 'batch' ? (
<BatchImageGrid /> <BatchImageGrid />
) : ( ) : (

View File

@ -106,6 +106,7 @@ const GalleryImage = (props: HoverableImageProps) => {
isDropDisabled={true} isDropDisabled={true}
isUploadDisabled={true} isUploadDisabled={true}
thumbnail={true} thumbnail={true}
withHoverOverlay
// resetIcon={<FaTrash />} // resetIcon={<FaTrash />}
// resetTooltip="Delete image" // resetTooltip="Delete image"
// withResetIcon // removed bc it's too easy to accidentally delete images // withResetIcon // removed bc it's too easy to accidentally delete images

View File

@ -1,8 +1,9 @@
import { Box, Spinner } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { IMAGE_LIMIT } from 'features/gallery//store/gallerySlice'; import { IMAGE_LIMIT } from 'features/gallery//store/gallerySlice';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
import { import {
UseOverlayScrollbarsParams, UseOverlayScrollbarsParams,
useOverlayScrollbars, useOverlayScrollbars,
@ -15,10 +16,10 @@ import {
useLazyListImagesQuery, useLazyListImagesQuery,
useListImagesQuery, useListImagesQuery,
} from 'services/api/endpoints/images'; } from 'services/api/endpoints/images';
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import GalleryImage from './GalleryImage'; import GalleryImage from './GalleryImage';
import ImageGridItemContainer from './ImageGridItemContainer'; import ImageGridItemContainer from './ImageGridItemContainer';
import ImageGridListContainer from './ImageGridListContainer'; import ImageGridListContainer from './ImageGridListContainer';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = { const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
defer: true, defer: true,
@ -40,7 +41,10 @@ const GalleryImageGrid = () => {
const [initialize, osInstance] = useOverlayScrollbars( const [initialize, osInstance] = useOverlayScrollbars(
overlayScrollbarsConfig overlayScrollbarsConfig
); );
const selectedBoardId = useAppSelector(
(state) => state.gallery.selectedBoardId
);
const { currentViewTotal } = useBoardTotal(selectedBoardId);
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs); const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
const { currentData, isFetching, isSuccess, isError } = const { currentData, isFetching, isSuccess, isError } =
@ -49,19 +53,23 @@ const GalleryImageGrid = () => {
const [listImages] = useLazyListImagesQuery(); const [listImages] = useLazyListImagesQuery();
const areMoreAvailable = useMemo(() => { const areMoreAvailable = useMemo(() => {
if (!currentData) { if (!currentData || !currentViewTotal) {
return false; return false;
} }
return currentData.ids.length < currentData.total; return currentData.ids.length < currentViewTotal;
}, [currentData]); }, [currentData, currentViewTotal]);
const handleLoadMoreImages = useCallback(() => { const handleLoadMoreImages = useCallback(() => {
if (!areMoreAvailable) {
return;
}
listImages({ listImages({
...queryArgs, ...queryArgs,
offset: currentData?.ids.length ?? 0, offset: currentData?.ids.length ?? 0,
limit: IMAGE_LIMIT, limit: IMAGE_LIMIT,
}); });
}, [listImages, queryArgs, currentData?.ids.length]); }, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]);
useEffect(() => { useEffect(() => {
// Initialize the gallery's custom scrollbar // Initialize the gallery's custom scrollbar
@ -79,20 +87,34 @@ const GalleryImageGrid = () => {
if (!currentData) { if (!currentData) {
return ( return (
<Box sx={{ w: 'full', h: 'full' }}> <Flex
<Spinner size="2xl" opacity={0.5} /> sx={{
</Box> w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IAINoContentFallback label="Loading..." icon={FaImage} />
</Flex>
); );
} }
if (isSuccess && currentData?.ids.length === 0) { if (isSuccess && currentData?.ids.length === 0) {
return ( return (
<Box sx={{ w: 'full', h: 'full' }}> <Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IAINoContentFallback <IAINoContentFallback
label={t('gallery.noImagesInGallery')} label={t('gallery.noImagesInGallery')}
icon={FaImage} icon={FaImage}
/> />
</Box> </Flex>
); );
} }
@ -121,9 +143,7 @@ const GalleryImageGrid = () => {
loadingText="Loading" loadingText="Loading"
flexShrink={0} flexShrink={0}
> >
{areMoreAvailable {`Load More (${currentData.ids.length} of ${currentViewTotal})`}
? t('gallery.loadMore')
: t('gallery.allImagesLoaded')}
</IAIButton> </IAIButton>
</> </>
); );

View File

@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
IMAGE_LIMIT, IMAGE_LIMIT,
imageSelected, imageSelected,
selectImagesById,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { clamp, isEqual } from 'lodash-es'; import { clamp, isEqual } from 'lodash-es';
import { useCallback } from 'react'; import { useCallback } from 'react';
@ -53,8 +52,8 @@ export const nextPrevImageButtonsSelector = createSelector(
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1); const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
const nextImageId = images[nextImageIndex].image_name; const nextImageId = images[nextImageIndex]?.image_name;
const prevImageId = images[prevImageIndex].image_name; const prevImageId = images[prevImageIndex]?.image_name;
const nextImage = selectors.selectById(data, nextImageId); const nextImage = selectors.selectById(data, nextImageId);
const prevImage = selectors.selectById(data, prevImageId); const prevImage = selectors.selectById(data, prevImageId);
@ -65,7 +64,7 @@ export const nextPrevImageButtonsSelector = createSelector(
isOnFirstImage: currentImageIndex === 0, isOnFirstImage: currentImageIndex === 0,
isOnLastImage: isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1, !isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
areMoreImagesAvailable: data?.total ?? 0 > imagesLength, areMoreImagesAvailable: (data?.total ?? 0) > imagesLength,
isFetching: status === 'pending', isFetching: status === 'pending',
nextImage, nextImage,
prevImage, prevImage,

View File

@ -2,11 +2,11 @@ import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { ListImagesArgs } from 'services/api/endpoints/images'; import { ListImagesArgs } from 'services/api/endpoints/images';
import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
import { import {
getBoardIdQueryParamForBoard, ASSETS_CATEGORIES,
getCategoriesQueryParamForBoard, IMAGE_CATEGORIES,
} from './util'; INITIAL_IMAGE_LIMIT,
} from './gallerySlice';
export const gallerySelector = (state: RootState) => state.gallery; export const gallerySelector = (state: RootState) => state.gallery;
@ -19,14 +19,13 @@ export const selectLastSelectedImage = createSelector(
export const selectListImagesBaseQueryArgs = createSelector( export const selectListImagesBaseQueryArgs = createSelector(
[(state: RootState) => state], [(state: RootState) => state],
(state) => { (state) => {
const { selectedBoardId } = state.gallery; const { selectedBoardId, galleryView } = state.gallery;
const categories =
const categories = getCategoriesQueryParamForBoard(selectedBoardId); galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
const listImagesBaseQueryArgs: ListImagesArgs = { const listImagesBaseQueryArgs: ListImagesArgs = {
board_id: selectedBoardId ?? 'none',
categories, categories,
board_id,
offset: 0, offset: 0,
limit: INITIAL_IMAGE_LIMIT, limit: INITIAL_IMAGE_LIMIT,
is_intermediate: false, is_intermediate: false,

View File

@ -1,5 +1,5 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import { uniq } from 'lodash-es'; import { uniq } from 'lodash-es';
import { boardsApi } from 'services/api/endpoints/boards'; import { boardsApi } from 'services/api/endpoints/boards';
import { ImageCategory } from 'services/api/types'; import { ImageCategory } from 'services/api/types';
@ -14,20 +14,17 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
export const INITIAL_IMAGE_LIMIT = 100; export const INITIAL_IMAGE_LIMIT = 100;
export const IMAGE_LIMIT = 20; export const IMAGE_LIMIT = 20;
// export type GalleryView = 'images' | 'assets'; export type GalleryView = 'images' | 'assets';
export type BoardId = // export type BoardId = 'no_board' | (string & Record<never, never>);
| 'images' export type BoardId = string | undefined;
| 'assets'
| 'no_board'
| 'batch'
| (string & Record<never, never>);
type GalleryState = { type GalleryState = {
selection: string[]; selection: string[];
shouldAutoSwitch: boolean; shouldAutoSwitch: boolean;
autoAddBoardId: string | null; autoAddBoardId: string | undefined;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
selectedBoardId: BoardId; selectedBoardId: BoardId;
galleryView: GalleryView;
batchImageNames: string[]; batchImageNames: string[];
isBatchEnabled: boolean; isBatchEnabled: boolean;
}; };
@ -35,9 +32,10 @@ type GalleryState = {
export const initialGalleryState: GalleryState = { export const initialGalleryState: GalleryState = {
selection: [], selection: [],
shouldAutoSwitch: true, shouldAutoSwitch: true,
autoAddBoardId: null, autoAddBoardId: undefined,
galleryImageMinimumWidth: 96, galleryImageMinimumWidth: 96,
selectedBoardId: 'images', selectedBoardId: undefined,
galleryView: 'images',
batchImageNames: [], batchImageNames: [],
isBatchEnabled: false, isBatchEnabled: false,
}; };
@ -46,14 +44,8 @@ export const gallerySlice = createSlice({
name: 'gallery', name: 'gallery',
initialState: initialGalleryState, initialState: initialGalleryState,
reducers: { reducers: {
imagesRemoved: (state, action: PayloadAction<string[]>) => {
// TODO: port all instances of this to use RTK Query cache
// imagesAdapter.removeMany(state, action.payload);
// state.batchImageNames = state.batchImageNames.filter(
// (name) => !action.payload.includes(name)
// );
},
imageRangeEndSelected: (state, action: PayloadAction<string>) => { imageRangeEndSelected: (state, action: PayloadAction<string>) => {
// MULTI SELECT LOGIC
// const rangeEndImageName = action.payload; // const rangeEndImageName = action.payload;
// const lastSelectedImage = state.selection[state.selection.length - 1]; // const lastSelectedImage = state.selection[state.selection.length - 1];
// const filteredImages = selectFilteredImagesLocal(state); // const filteredImages = selectFilteredImagesLocal(state);
@ -74,6 +66,7 @@ export const gallerySlice = createSlice({
// } // }
}, },
imageSelectionToggled: (state, action: PayloadAction<string>) => { imageSelectionToggled: (state, action: PayloadAction<string>) => {
// MULTI SELECT LOGIC
// if ( // if (
// state.selection.includes(action.payload) && // state.selection.includes(action.payload) &&
// state.selection.length > 1 // state.selection.length > 1
@ -96,6 +89,7 @@ export const gallerySlice = createSlice({
}, },
boardIdSelected: (state, action: PayloadAction<BoardId>) => { boardIdSelected: (state, action: PayloadAction<BoardId>) => {
state.selectedBoardId = action.payload; state.selectedBoardId = action.payload;
state.galleryView = 'images';
}, },
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => { isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
state.isBatchEnabled = action.payload; state.isBatchEnabled = action.payload;
@ -125,23 +119,27 @@ export const gallerySlice = createSlice({
state.batchImageNames = []; state.batchImageNames = [];
state.selection = []; state.selection = [];
}, },
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => { autoAddBoardIdChanged: (
state,
action: PayloadAction<string | undefined>
) => {
state.autoAddBoardId = action.payload; state.autoAddBoardId = action.payload;
}, },
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
state.galleryView = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addMatcher( builder.addMatcher(isAnyBoardDeleted, (state, action) => {
boardsApi.endpoints.deleteBoard.matchFulfilled, const deletedBoardId = action.meta.arg.originalArgs;
(state, action) => { if (deletedBoardId === state.selectedBoardId) {
const deletedBoardId = action.meta.arg.originalArgs; state.selectedBoardId = undefined;
if (deletedBoardId === state.selectedBoardId) { state.galleryView = 'images';
state.selectedBoardId = 'images';
}
if (deletedBoardId === state.autoAddBoardId) {
state.autoAddBoardId = null;
}
} }
); if (deletedBoardId === state.autoAddBoardId) {
state.autoAddBoardId = undefined;
}
});
builder.addMatcher( builder.addMatcher(
boardsApi.endpoints.listAllBoards.matchFulfilled, boardsApi.endpoints.listAllBoards.matchFulfilled,
(state, action) => { (state, action) => {
@ -151,7 +149,7 @@ export const gallerySlice = createSlice({
} }
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) { if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
state.autoAddBoardId = null; state.autoAddBoardId = undefined;
} }
} }
); );
@ -170,6 +168,12 @@ export const {
imagesAddedToBatch, imagesAddedToBatch,
imagesRemovedFromBatch, imagesRemovedFromBatch,
autoAddBoardIdChanged, autoAddBoardIdChanged,
galleryViewChanged,
} = gallerySlice.actions; } = gallerySlice.actions;
export default gallerySlice.reducer; export default gallerySlice.reducer;
const isAnyBoardDeleted = isAnyOf(
boardsApi.endpoints.deleteBoard.matchFulfilled,
boardsApi.endpoints.deleteBoardAndImages.matchFulfilled
);

View File

@ -1,7 +1,6 @@
import { SYSTEM_BOARDS } from 'services/api/endpoints/images';
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
import { ImageCategory } from 'services/api/types';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { ImageCategory, ImageDTO } from 'services/api/types';
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
export const getCategoriesQueryParamForBoard = ( export const getCategoriesQueryParamForBoard = (
board_id: BoardId board_id: BoardId
@ -20,16 +19,11 @@ export const getCategoriesQueryParamForBoard = (
export const getBoardIdQueryParamForBoard = ( export const getBoardIdQueryParamForBoard = (
board_id: BoardId board_id: BoardId
): string | undefined => { ): string | null => {
if (board_id === 'no_board') { if (board_id === undefined) {
return 'none'; return 'none';
} }
// system boards besides 'no_board'
if (SYSTEM_BOARDS.includes(board_id)) {
return undefined;
}
// user boards // user boards
return board_id; return board_id;
}; };
@ -52,3 +46,10 @@ export const getBoardIdFromBoardAndCategoriesQueryParam = (
return board_id ?? 'UNKNOWN_BOARD'; return board_id ?? 'UNKNOWN_BOARD';
}; };
export const getCategories = (imageDTO: ImageDTO) => {
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
return IMAGE_CATEGORIES;
}
return ASSETS_CATEGORIES;
};

View File

@ -78,7 +78,6 @@ const ParametersDrawer = () => {
}} }}
> >
<Flex <Flex
paddingTop={1.5}
paddingBottom={4} paddingBottom={4}
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"

View File

@ -164,7 +164,7 @@ const ResizableDrawer = ({
sx={{ sx={{
borderColor: mode('base.200', 'base.800')(colorMode), borderColor: mode('base.200', 'base.800')(colorMode),
p: 4, p: 4,
bg: mode('base.100', 'base.900')(colorMode), bg: mode('base.50', 'base.900')(colorMode),
height: 'full', height: 'full',
shadow: isOpen ? 'dark-lg' : undefined, shadow: isOpen ? 'dark-lg' : undefined,
...containerStyles, ...containerStyles,

View File

@ -1,52 +1,36 @@
import { ImageDTO, OffsetPaginatedResults_ImageDTO_ } from 'services/api/types'; import { api } from '..';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema';
import { BoardId } from 'features/gallery/store/gallerySlice';
type ListBoardImagesArg =
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
type AddImageToBoardArg =
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
type RemoveImageFromBoardArg =
paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'];
export const boardImagesApi = api.injectEndpoints({ export const boardImagesApi = api.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
/** /**
* Board Images Queries * Board Images Queries
*/ */
// listBoardImages: build.query<
listBoardImages: build.query< // OffsetPaginatedResults_ImageDTO_,
OffsetPaginatedResults_ImageDTO_, // ListBoardImagesArg
ListBoardImagesArg // >({
>({ // query: ({ board_id, offset, limit }) => ({
query: ({ board_id, offset, limit }) => ({ // url: `board_images/${board_id}`,
url: `board_images/${board_id}`, // method: 'GET',
method: 'GET', // }),
}), // providesTags: (result, error, arg) => {
providesTags: (result, error, arg) => { // // any list of boardimages
// any list of boardimages // const tags: ApiFullTagDescription[] = [
const tags: ApiFullTagDescription[] = [ // { type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
{ type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` }, // ];
]; // if (result) {
// // and individual tags for each boardimage
if (result) { // tags.push(
// and individual tags for each boardimage // ...result.items.map(({ board_id, image_name }) => ({
tags.push( // type: 'BoardImage' as const,
...result.items.map(({ board_id, image_name }) => ({ // id: `${board_id}_${image_name}`,
type: 'BoardImage' as const, // }))
id: `${board_id}_${image_name}`, // );
})) // }
); // return tags;
} // },
// }),
return tags;
},
}),
}), }),
}); });
export const { useListBoardImagesQuery } = boardImagesApi; // export const { useListBoardImagesQuery } = boardImagesApi;

View File

@ -109,10 +109,25 @@ export const boardsApi = api.injectEndpoints({
deleteBoard: build.mutation<DeleteBoardResult, string>({ deleteBoard: build.mutation<DeleteBoardResult, string>({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }), query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [ invalidatesTags: (result, error, board_id) => [
{ type: 'Board', id: arg }, { type: 'Board', id: LIST_TAG },
// invalidate the 'No Board' cache // invalidate the 'No Board' cache
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) }, {
type: 'ImageList',
id: getListImagesUrl({
board_id: 'none',
categories: IMAGE_CATEGORIES,
}),
},
{
type: 'ImageList',
id: getListImagesUrl({
board_id: 'none',
categories: ASSETS_CATEGORIES,
}),
},
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
], ],
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) { async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
/** /**
@ -167,24 +182,14 @@ export const boardsApi = api.injectEndpoints({
'listImages', 'listImages',
queryArgs, queryArgs,
(draft) => { (draft) => {
const oldCount = imagesAdapter const oldTotal = draft.total;
.getSelectors()
.selectTotal(draft);
const newState = imagesAdapter.updateMany(draft, updates); const newState = imagesAdapter.updateMany(draft, updates);
const newCount = imagesAdapter const delta = newState.total - oldTotal;
.getSelectors() draft.total = draft.total + delta;
.selectTotal(newState);
draft.total = Math.max(
draft.total - (oldCount - newCount),
0
);
} }
) )
); );
}); });
// after deleting a board, select the 'All Images' board
dispatch(boardIdSelected('images'));
} catch { } catch {
//no-op //no-op
} }
@ -197,9 +202,24 @@ export const boardsApi = api.injectEndpoints({
method: 'DELETE', method: 'DELETE',
params: { include_images: true }, params: { include_images: true },
}), }),
invalidatesTags: (result, error, arg) => [ invalidatesTags: (result, error, board_id) => [
{ type: 'Board', id: arg }, { type: 'Board', id: LIST_TAG },
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) }, {
type: 'ImageList',
id: getListImagesUrl({
board_id: 'none',
categories: IMAGE_CATEGORIES,
}),
},
{
type: 'ImageList',
id: getListImagesUrl({
board_id: 'none',
categories: ASSETS_CATEGORIES,
}),
},
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
], ],
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) { async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
/** /**
@ -231,27 +251,17 @@ export const boardsApi = api.injectEndpoints({
'listImages', 'listImages',
queryArgs, queryArgs,
(draft) => { (draft) => {
const oldCount = imagesAdapter const oldTotal = draft.total;
.getSelectors()
.selectTotal(draft);
const newState = imagesAdapter.removeMany( const newState = imagesAdapter.removeMany(
draft, draft,
deleted_images deleted_images
); );
const newCount = imagesAdapter const delta = newState.total - oldTotal;
.getSelectors() draft.total = draft.total + delta;
.selectTotal(newState);
draft.total = Math.max(
draft.total - (oldCount - newCount),
0
);
} }
) )
); );
}); });
// after deleting a board, select the 'All Images' board
dispatch(boardIdSelected('images'));
} catch { } catch {
//no-op //no-op
} }

View File

@ -6,18 +6,17 @@ import {
BoardId, BoardId,
IMAGE_CATEGORIES, IMAGE_CATEGORIES,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { omit } from 'lodash-es'; import { getCategories } from 'features/gallery/store/util';
import queryString from 'query-string'; import queryString from 'query-string';
import { ApiFullTagDescription, api } from '..'; import { ApiFullTagDescription, api } from '..';
import { components, paths } from '../schema'; import { components, paths } from '../schema';
import { import {
ImageCategory, ImageCategory,
ImageChanges,
ImageDTO, ImageDTO,
OffsetPaginatedResults_ImageDTO_, OffsetPaginatedResults_ImageDTO_,
PostUploadAction, PostUploadAction,
} from '../types'; } from '../types';
import { getCacheAction } from './util'; import { getIsImageInDateRange } from './util';
export type ListImagesArgs = NonNullable< export type ListImagesArgs = NonNullable<
paths['/api/v1/images/']['get']['parameters']['query'] paths['/api/v1/images/']['get']['parameters']['query']
@ -51,8 +50,6 @@ export const imagesSelectors = imagesAdapter.getSelectors();
export const getListImagesUrl = (queryArgs: ListImagesArgs) => export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`; `images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
export const SYSTEM_BOARDS = ['images', 'assets', 'no_board', 'batch'];
export const imagesApi = api.injectEndpoints({ export const imagesApi = api.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
/** /**
@ -155,6 +152,42 @@ export const imagesApi = api.injectEndpoints({
}, },
keepUnusedDataFor: 86400, // 24 hours keepUnusedDataFor: 86400, // 24 hours
}), }),
getBoardImagesTotal: build.query<number, string | undefined>({
query: (board_id) => ({
url: getListImagesUrl({
board_id: board_id ?? 'none',
categories: IMAGE_CATEGORIES,
is_intermediate: false,
limit: 0,
offset: 0,
}),
method: 'GET',
}),
providesTags: (result, error, arg) => [
{ type: 'BoardImagesTotal', id: arg ?? 'none' },
],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
return response.total;
},
}),
getBoardAssetsTotal: build.query<number, string | undefined>({
query: (board_id) => ({
url: getListImagesUrl({
board_id: board_id ?? 'none',
categories: ASSETS_CATEGORIES,
is_intermediate: false,
limit: 0,
offset: 0,
}),
method: 'GET',
}),
providesTags: (result, error, arg) => [
{ type: 'BoardAssetsTotal', id: arg ?? 'none' },
],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
return response.total;
},
}),
clearIntermediates: build.mutation<number, void>({ clearIntermediates: build.mutation<number, void>({
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }), query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
invalidatesTags: ['IntermediatesCount'], invalidatesTags: ['IntermediatesCount'],
@ -164,56 +197,42 @@ export const imagesApi = api.injectEndpoints({
url: `images/${image_name}`, url: `images/${image_name}`,
method: 'DELETE', method: 'DELETE',
}), }),
invalidatesTags: (result, error, arg) => [ invalidatesTags: (result, error, { board_id }) => [
{ type: 'Image', id: arg.image_name }, { type: 'BoardImagesTotal', id: board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
], ],
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) { async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
/** /**
* Cache changes for `deleteImage`: * Cache changes for `deleteImage`:
* - *remove* from "All Images" / "All Assets" * - NOT POSSIBLE: *remove* from getImageDTO
* - IF it has a board: * - $cache = [board_id|no_board]/[images|assets]
* - THEN *remove* from it's own board * - *remove* from $cache
* - ELSE *remove* from "No Board"
*/ */
const { image_name, board_id, image_category } = imageDTO; const { image_name, board_id } = imageDTO;
// Figure out the `listImages` caches that we need to update // Store patches so we can undo if the query fails
// That means constructing the possible query args that are serialized into the cache key... const patches: PatchCollection[] = [];
const removeFromCacheKeys: ListImagesArgs[] = [];
// determine `categories`, i.e. do we update "All Images" or "All Assets" // determine `categories`, i.e. do we update "All Images" or "All Assets"
const categories = IMAGE_CATEGORIES.includes(image_category) // $cache = [board_id|no_board]/[images|assets]
? IMAGE_CATEGORIES const categories = getCategories(imageDTO);
: ASSETS_CATEGORIES;
// remove from "All Images" // *remove* from $cache
removeFromCacheKeys.push({ categories }); patches.push(
dispatch(
if (board_id) { imagesApi.util.updateQueryData(
// remove from it's own board 'listImages',
removeFromCacheKeys.push({ board_id }); { board_id: board_id ?? 'none', categories },
} else { (draft) => {
// remove from "No Board" const oldTotal = draft.total;
removeFromCacheKeys.push({ board_id: 'none' }); const newState = imagesAdapter.removeOne(draft, image_name);
} const delta = newState.total - oldTotal;
draft.total = draft.total + delta;
const patches: PatchCollection[] = []; }
removeFromCacheKeys.forEach((cacheKey) => {
patches.push(
dispatch(
imagesApi.util.updateQueryData(
'listImages',
cacheKey,
(draft) => {
imagesAdapter.removeOne(draft, image_name);
draft.total = Math.max(draft.total - 1, 0);
}
)
) )
); )
}); );
try { try {
await queryFulfilled; await queryFulfilled;
@ -222,122 +241,169 @@ export const imagesApi = api.injectEndpoints({
} }
}, },
}), }),
updateImage: build.mutation< /**
* Change an image's `is_intermediate` property.
*/
changeImageIsIntermediate: build.mutation<
ImageDTO, ImageDTO,
{ { imageDTO: ImageDTO; is_intermediate: boolean }
imageDTO: ImageDTO;
// For now, we will not allow image categories to change
changes: Omit<ImageChanges, 'image_category'>;
}
>({ >({
query: ({ imageDTO, changes }) => ({ query: ({ imageDTO, is_intermediate }) => ({
url: `images/${imageDTO.image_name}`, url: `images/${imageDTO.image_name}`,
method: 'PATCH', method: 'PATCH',
body: changes, body: { is_intermediate },
}), }),
invalidatesTags: (result, error, { imageDTO }) => [ invalidatesTags: (result, error, { imageDTO }) => [
{ type: 'Image', id: imageDTO.image_name }, { type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
], ],
async onQueryStarted( async onQueryStarted(
{ imageDTO: oldImageDTO, changes: _changes }, { imageDTO, is_intermediate },
{ dispatch, queryFulfilled, getState } { dispatch, queryFulfilled, getState }
) { ) {
// let's be extra-sure we do not accidentally change categories
const changes = omit(_changes, 'image_category');
/** /**
* Cache changes for "updateImage": * Cache changes for `changeImageIsIntermediate`:
* - *update* "getImageDTO" cache * - *update* getImageDTO
* - for "All Images" || "All Assets": * - $cache = [board_id|no_board]/[images|assets]
* - IF it is not already in the cache * - IF it is being changed to an intermediate:
* - THEN *add* it to "All Images" / "All Assets" and update the total * - remove from $cache
* - ELSE *update* it * - ELSE (it is being changed to a non-intermediate):
* - IF the image has a board: * - IF it eligible for insertion into existing $cache:
* - THEN *update* it's own board * - *upsert* to $cache
* - ELSE *update* the "No Board" board
*/ */
// Store patches so we can undo if the query fails
const patches: PatchCollection[] = []; const patches: PatchCollection[] = [];
const { image_name, board_id, image_category, is_intermediate } =
oldImageDTO;
const isChangingFromIntermediate = changes.is_intermediate === false; // *update* getImageDTO
// do not add intermediates to gallery cache
if (is_intermediate && !isChangingFromIntermediate) {
return;
}
// determine `categories`, i.e. do we update "All Images" or "All Assets"
const categories = IMAGE_CATEGORIES.includes(image_category)
? IMAGE_CATEGORIES
: ASSETS_CATEGORIES;
// update `getImageDTO` cache
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'getImageDTO', 'getImageDTO',
image_name, imageDTO.image_name,
(draft) => { (draft) => {
Object.assign(draft, changes); Object.assign(draft, { is_intermediate });
} }
) )
) )
); );
// Update the "All Image" or "All Assets" board // $cache = [board_id|no_board]/[images|assets]
const queryArgsToUpdate: ListImagesArgs[] = [{ categories }]; const categories = getCategories(imageDTO);
// IF the image has a board: if (is_intermediate) {
if (board_id) { // IF it is being changed to an intermediate:
// THEN update it's own board // remove from $cache
queryArgsToUpdate.push({ board_id }); patches.push(
dispatch(
imagesApi.util.updateQueryData(
'listImages',
{ board_id: imageDTO.board_id ?? 'none', categories },
(draft) => {
const oldTotal = draft.total;
const newState = imagesAdapter.removeOne(
draft,
imageDTO.image_name
);
const delta = newState.total - oldTotal;
draft.total = draft.total + delta;
}
)
)
);
} else { } else {
// ELSE update the "No Board" board // ELSE (it is being changed to a non-intermediate):
queryArgsToUpdate.push({ board_id: 'none' }); console.log(imageDTO);
} const queryArgs = {
board_id: imageDTO.board_id ?? 'none',
categories,
};
queryArgsToUpdate.forEach((queryArg) => { const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
const { data } = imagesApi.endpoints.listImages.select(queryArg)(
getState() getState()
); );
const cacheAction = getCacheAction(data, oldImageDTO); // IF it eligible for insertion into existing $cache
// "eligible" means either:
// - The cache is fully populated, with all images in the db cached
// OR
// - The image's `created_at` is within the range of the cached images
if (['update', 'add'].includes(cacheAction)) { const isCacheFullyPopulated =
currentCache.data &&
currentCache.data.ids.length >= currentCache.data.total;
const isInDateRange = getIsImageInDateRange(
currentCache.data,
imageDTO
);
if (isCacheFullyPopulated || isInDateRange) {
// *upsert* to $cache
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'listImages', 'listImages',
queryArg, queryArgs,
(draft) => { (draft) => {
// One of the common changes is to make a canvas intermediate a non-intermediate, const oldTotal = draft.total;
// i.e. save a canvas image to the gallery. const newState = imagesAdapter.upsertOne(draft, imageDTO);
// If that was the change, need to add the image to the cache instead of updating const delta = newState.total - oldTotal;
// the existing cache entry. draft.total = draft.total + delta;
if (
changes.is_intermediate === false ||
cacheAction === 'add'
) {
// add it to the cache
imagesAdapter.addOne(draft, {
...oldImageDTO,
...changes,
});
draft.total += 1;
} else if (cacheAction === 'update') {
// just update it
imagesAdapter.updateOne(draft, {
id: image_name,
changes,
});
}
} }
) )
) )
); );
} }
}); }
try {
await queryFulfilled;
} catch {
patches.forEach((patchResult) => patchResult.undo());
}
},
}),
/**
* Change an image's `session_id` association.
*/
changeImageSessionId: build.mutation<
ImageDTO,
{ imageDTO: ImageDTO; session_id: string }
>({
query: ({ imageDTO, session_id }) => ({
url: `images/${imageDTO.image_name}`,
method: 'PATCH',
body: { session_id },
}),
invalidatesTags: (result, error, { imageDTO }) => [
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
],
async onQueryStarted(
{ imageDTO, session_id },
{ dispatch, queryFulfilled, getState }
) {
/**
* Cache changes for `changeImageSessionId`:
* - *update* getImageDTO
*/
// Store patches so we can undo if the query fails
const patches: PatchCollection[] = [];
// *update* getImageDTO
patches.push(
dispatch(
imagesApi.util.updateQueryData(
'getImageDTO',
imageDTO.image_name,
(draft) => {
Object.assign(draft, { session_id });
}
)
)
);
try { try {
await queryFulfilled; await queryFulfilled;
@ -354,9 +420,18 @@ export const imagesApi = api.injectEndpoints({
is_intermediate: boolean; is_intermediate: boolean;
postUploadAction?: PostUploadAction; postUploadAction?: PostUploadAction;
session_id?: string; session_id?: string;
board_id?: string;
crop_visible?: boolean;
} }
>({ >({
query: ({ file, image_category, is_intermediate, session_id }) => { query: ({
file,
image_category,
is_intermediate,
session_id,
board_id,
crop_visible,
}) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
return { return {
@ -367,14 +442,32 @@ export const imagesApi = api.injectEndpoints({
image_category, image_category,
is_intermediate, is_intermediate,
session_id, session_id,
board_id,
crop_visible,
}, },
}; };
}, },
async onQueryStarted( async onQueryStarted(
{ file, image_category, is_intermediate, postUploadAction }, {
file,
image_category,
is_intermediate,
postUploadAction,
session_id,
board_id,
},
{ dispatch, queryFulfilled } { dispatch, queryFulfilled }
) { ) {
try { try {
/**
* NOTE: PESSIMISTIC UPDATE
* Cache changes for `uploadImage`:
* - IF the image is an intermediate:
* - BAIL OUT
* - *add* to `getImageDTO`
* - *add* to no_board/assets
*/
const { data: imageDTO } = await queryFulfilled; const { data: imageDTO } = await queryFulfilled;
if (imageDTO.is_intermediate) { if (imageDTO.is_intermediate) {
@ -382,21 +475,42 @@ export const imagesApi = api.injectEndpoints({
return; return;
} }
// determine `categories`, i.e. do we update "All Images" or "All Assets" // *add* to `getImageDTO`
const categories = IMAGE_CATEGORIES.includes(image_category) dispatch(
? IMAGE_CATEGORIES imagesApi.util.upsertQueryData(
: ASSETS_CATEGORIES; 'getImageDTO',
imageDTO.image_name,
imageDTO
)
);
const queryArg = { categories }; const categories = getCategories(imageDTO);
// *add* to no_board/assets
dispatch(
imagesApi.util.updateQueryData(
'listImages',
{
board_id: imageDTO.board_id ?? 'none',
categories,
},
(draft) => {
const oldTotal = draft.total;
const newState = imagesAdapter.addOne(draft, imageDTO);
const delta = newState.total - oldTotal;
draft.total = draft.total + delta;
}
)
);
dispatch( dispatch(
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => { imagesApi.util.invalidateTags([
imagesAdapter.addOne(draft, imageDTO); { type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
draft.total = draft.total + 1; { type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
}) ])
); );
} catch { } catch {
// no-op // query failed, no action needed
} }
}, },
}), }),
@ -412,102 +526,102 @@ export const imagesApi = api.injectEndpoints({
body: { board_id, image_name }, body: { board_id, image_name },
}; };
}, },
invalidatesTags: (result, error, arg) => [ invalidatesTags: (result, error, { board_id, imageDTO }) => [
{ type: 'BoardImage' }, { type: 'Board', id: board_id },
{ type: 'Board', id: arg.board_id }, { type: 'BoardImagesTotal', id: board_id },
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: board_id },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
], ],
async onQueryStarted( async onQueryStarted(
{ board_id, imageDTO: oldImageDTO }, { board_id, imageDTO },
{ dispatch, queryFulfilled, getState } { dispatch, queryFulfilled, getState }
) { ) {
/** /**
* Cache changes for `addImageToBoard`: * Cache changes for `addImageToBoard`:
* - *update* the `getImageDTO` cache * - *update* getImageDTO
* - *remove* from "No Board" * - IF it is intermediate:
* - IF the image has an old `board_id`: * - BAIL OUT ON FURTHER CHANGES
* - THEN *remove* from it's old `board_id` * - IF it has an old board_id:
* - IF the image's `created_at` is within the range of the board's cached images * - THEN *remove* from old board_id/[images|assets]
* - OR the board cache has length of 0 or 1 * - ELSE *remove* from no_board/[images|assets]
* - THEN *add* it to new `board_id` * - $cache = board_id/[images|assets]
* - IF it eligible for insertion into existing $cache:
* - THEN *add* to $cache
*/ */
const { image_name, board_id: old_board_id } = oldImageDTO;
// Figure out the `listImages` caches that we need to update
const removeFromQueryArgs: ListImagesArgs[] = [];
// remove from "No Board"
removeFromQueryArgs.push({ board_id: 'none' });
// remove from old board
if (old_board_id) {
removeFromQueryArgs.push({ board_id: old_board_id });
}
// Store all patch results in case we need to roll back
const patches: PatchCollection[] = []; const patches: PatchCollection[] = [];
const categories = getCategories(imageDTO);
// Updated imageDTO with new board_id // *update* getImageDTO
const newImageDTO = { ...oldImageDTO, board_id };
// Update getImageDTO cache
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'getImageDTO', 'getImageDTO',
image_name, imageDTO.image_name,
(draft) => { (draft) => {
Object.assign(draft, newImageDTO); Object.assign(draft, { board_id });
} }
) )
) )
); );
// Do the "Remove from" cache updates if (!imageDTO.is_intermediate) {
removeFromQueryArgs.forEach((queryArgs) => { // *remove* from [no_board|board_id]/[images|assets]
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'listImages', 'listImages',
queryArgs, {
board_id: imageDTO.board_id ?? 'none',
categories,
},
(draft) => { (draft) => {
// sanity check const oldTotal = draft.total;
if (draft.ids.includes(image_name)) { const newState = imagesAdapter.removeOne(
imagesAdapter.removeOne(draft, image_name); draft,
draft.total = Math.max(draft.total - 1, 0); imageDTO.image_name
} );
const delta = newState.total - oldTotal;
draft.total = draft.total + delta;
} }
) )
) )
); );
});
// We only need to add to the cache if the board is not a system board // $cache = board_id/[images|assets]
if (!SYSTEM_BOARDS.includes(board_id)) { const queryArgs = { board_id: board_id ?? 'none', categories };
const queryArgs = { board_id }; const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(
getState() getState()
); );
const cacheAction = getCacheAction(data, oldImageDTO); // IF it eligible for insertion into existing $cache
// "eligible" means either:
// - The cache is fully populated, with all images in the db cached
// OR
// - The image's `created_at` is within the range of the cached images
if (['add', 'update'].includes(cacheAction)) { const isCacheFullyPopulated =
// Do the "Add to" cache updates currentCache.data &&
currentCache.data.ids.length >= currentCache.data.total;
const isInDateRange = getIsImageInDateRange(
currentCache.data,
imageDTO
);
if (isCacheFullyPopulated || isInDateRange) {
// THEN *add* to $cache
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'listImages', 'listImages',
queryArgs, queryArgs,
(draft) => { (draft) => {
if (cacheAction === 'add') { const oldTotal = draft.total;
imagesAdapter.addOne(draft, newImageDTO); const newState = imagesAdapter.addOne(draft, imageDTO);
draft.total += 1; const delta = newState.total - oldTotal;
} else { draft.total = draft.total + delta;
imagesAdapter.updateOne(draft, {
id: image_name,
changes: { board_id },
});
}
} }
) )
) )
@ -531,87 +645,97 @@ export const imagesApi = api.injectEndpoints({
body: { board_id, image_name }, body: { board_id, image_name },
}; };
}, },
invalidatesTags: (result, error, arg) => [ invalidatesTags: (result, error, { imageDTO }) => [
{ type: 'BoardImage' }, { type: 'Board', id: imageDTO.board_id },
{ type: 'Board', id: arg.imageDTO.board_id }, { type: 'BoardImagesTotal', id: imageDTO.board_id },
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id },
{ type: 'BoardAssetsTotal', id: 'none' },
], ],
async onQueryStarted( async onQueryStarted(
{ imageDTO }, { imageDTO },
{ dispatch, queryFulfilled, getState } { dispatch, queryFulfilled, getState }
) { ) {
/** /**
* Cache changes for `removeImageFromBoard`: * Cache changes for removeImageFromBoard:
* - *update* `getImageDTO` * - *update* getImageDTO
* - IF the image's `created_at` is within the range of the board's cached images * - *remove* from board_id/[images|assets]
* - THEN *add* to "No Board" * - $cache = no_board/[images|assets]
* - *remove* from `old_board_id` * - IF it eligible for insertion into existing $cache:
* - THEN *upsert* to $cache
*/ */
const { image_name, board_id: old_board_id } = imageDTO; const categories = getCategories(imageDTO);
const patches: PatchCollection[] = []; const patches: PatchCollection[] = [];
// Updated imageDTO with new board_id // *update* getImageDTO
const newImageDTO = { ...imageDTO, board_id: undefined };
// Update getImageDTO cache
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'getImageDTO', 'getImageDTO',
image_name, imageDTO.image_name,
(draft) => { (draft) => {
Object.assign(draft, newImageDTO); Object.assign(draft, { board_id: undefined });
} }
) )
) )
); );
// Remove from old board // *remove* from board_id/[images|assets]
if (old_board_id) { patches.push(
const oldBoardQueryArgs = { board_id: old_board_id }; dispatch(
patches.push( imagesApi.util.updateQueryData(
dispatch( 'listImages',
imagesApi.util.updateQueryData( {
'listImages', board_id: imageDTO.board_id ?? 'none',
oldBoardQueryArgs, categories,
(draft) => { },
// sanity check (draft) => {
if (draft.ids.includes(image_name)) { const oldTotal = draft.total;
imagesAdapter.removeOne(draft, image_name); const newState = imagesAdapter.removeOne(
draft.total = Math.max(draft.total - 1, 0); draft,
} imageDTO.image_name
} );
) const delta = newState.total - oldTotal;
draft.total = draft.total + delta;
}
) )
); )
} );
// Add to "No Board" // $cache = no_board/[images|assets]
const noBoardQueryArgs = { board_id: 'none' }; const queryArgs = { board_id: 'none', categories };
const { data } = imagesApi.endpoints.listImages.select( const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
noBoardQueryArgs getState()
)(getState()); );
// Check if we need to make any cache changes // IF it eligible for insertion into existing $cache
const cacheAction = getCacheAction(data, imageDTO); // "eligible" means either:
// - The cache is fully populated, with all images in the db cached
// OR
// - The image's `created_at` is within the range of the cached images
if (['add', 'update'].includes(cacheAction)) { const isCacheFullyPopulated =
currentCache.data &&
currentCache.data.ids.length >= currentCache.data.total;
const isInDateRange = getIsImageInDateRange(
currentCache.data,
imageDTO
);
if (isCacheFullyPopulated || isInDateRange) {
// THEN *upsert* to $cache
patches.push( patches.push(
dispatch( dispatch(
imagesApi.util.updateQueryData( imagesApi.util.updateQueryData(
'listImages', 'listImages',
noBoardQueryArgs, queryArgs,
(draft) => { (draft) => {
if (cacheAction === 'add') { const oldTotal = draft.total;
imagesAdapter.addOne(draft, imageDTO); const newState = imagesAdapter.upsertOne(draft, imageDTO);
draft.total += 1; const delta = newState.total - oldTotal;
} else { draft.total = draft.total + delta;
imagesAdapter.updateOne(draft, {
id: image_name,
changes: { board_id: undefined },
});
}
} }
) )
) )
@ -635,7 +759,8 @@ export const {
useGetImageDTOQuery, useGetImageDTOQuery,
useGetImageMetadataQuery, useGetImageMetadataQuery,
useDeleteImageMutation, useDeleteImageMutation,
useUpdateImageMutation, useGetBoardImagesTotalQuery,
useGetBoardAssetsTotalQuery,
useUploadImageMutation, useUploadImageMutation,
useAddImageToBoardMutation, useAddImageToBoardMutation,
useRemoveImageFromBoardMutation, useRemoveImageFromBoardMutation,

View File

@ -25,27 +25,27 @@ export const getIsImageInDateRange = (
return false; return false;
}; };
/** // /**
* Determines the action we should take when an image may need to be added or updated in a cache. // * Determines the action we should take when an image may need to be added or updated in a cache.
*/ // */
export const getCacheAction = ( // export const getCacheAction = (
data: ImageCache | undefined, // data: ImageCache | undefined,
imageDTO: ImageDTO // imageDTO: ImageDTO
): 'add' | 'update' | 'none' => { // ): 'add' | 'update' | 'none' => {
const isInDateRange = getIsImageInDateRange(data, imageDTO); // const isInDateRange = getIsImageInDateRange(data, imageDTO);
const isCacheFullyPopulated = data && data.total === data.ids.length; // const isCacheFullyPopulated = data && data.total === data.ids.length;
const shouldUpdateCache = // const shouldUpdateCache =
Boolean(isInDateRange) || Boolean(isCacheFullyPopulated); // Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
const isImageInCache = data && data.ids.includes(imageDTO.image_name); // const isImageInCache = data && data.ids.includes(imageDTO.image_name);
if (shouldUpdateCache && isImageInCache) { // if (shouldUpdateCache && isImageInCache) {
return 'update'; // return 'update';
} // }
if (shouldUpdateCache && !isImageInCache) { // if (shouldUpdateCache && !isImageInCache) {
return 'add'; // return 'add';
} // }
return 'none'; // return 'none';
}; // };

View File

@ -4,19 +4,8 @@ import { useListAllBoardsQuery } from '../endpoints/boards';
export const useBoardName = (board_id: BoardId | null | undefined) => { export const useBoardName = (board_id: BoardId | null | undefined) => {
const { boardName } = useListAllBoardsQuery(undefined, { const { boardName } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => { selectFromResult: ({ data }) => {
let boardName = ''; const selectedBoard = data?.find((b) => b.board_id === board_id);
if (board_id === 'images') { const boardName = selectedBoard?.board_name || 'Uncategorized';
boardName = 'Images';
} else if (board_id === 'assets') {
boardName = 'Assets';
} else if (board_id === 'no_board') {
boardName = 'No Board';
} else if (board_id === 'batch') {
boardName = 'Batch';
} else {
const selectedBoard = data?.find((b) => b.board_id === board_id);
boardName = selectedBoard?.board_name || 'Unknown Board';
}
return { boardName }; return { boardName };
}, },

View File

@ -1,53 +1,21 @@
import { skipToken } from '@reduxjs/toolkit/dist/query'; import { useAppSelector } from 'app/store/storeHooks';
import { import { BoardId } from 'features/gallery/store/gallerySlice';
ASSETS_CATEGORIES,
BoardId,
IMAGE_CATEGORIES,
INITIAL_IMAGE_LIMIT,
} from 'features/gallery/store/gallerySlice';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ListImagesArgs, useListImagesQuery } from '../endpoints/images'; import {
useGetBoardAssetsTotalQuery,
useGetBoardImagesTotalQuery,
} from '../endpoints/images';
const baseQueryArg: ListImagesArgs = { export const useBoardTotal = (board_id: BoardId) => {
offset: 0, const galleryView = useAppSelector((state) => state.gallery.galleryView);
limit: INITIAL_IMAGE_LIMIT,
is_intermediate: false, const { data: totalImages } = useGetBoardImagesTotalQuery(board_id);
}; const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
const imagesQueryArg: ListImagesArgs = { const currentViewTotal = useMemo(
categories: IMAGE_CATEGORIES, () => (galleryView === 'images' ? totalImages : totalAssets),
...baseQueryArg, [galleryView, totalAssets, totalImages]
}; );
const assetsQueryArg: ListImagesArgs = { return { totalImages, totalAssets, currentViewTotal };
categories: ASSETS_CATEGORIES,
...baseQueryArg,
};
const noBoardQueryArg: ListImagesArgs = {
board_id: 'none',
...baseQueryArg,
};
export const useBoardTotal = (board_id: BoardId | null | undefined) => {
const queryArg = useMemo(() => {
if (!board_id) {
return;
}
if (board_id === 'images') {
return imagesQueryArg;
} else if (board_id === 'assets') {
return assetsQueryArg;
} else if (board_id === 'no_board') {
return noBoardQueryArg;
} else {
return { board_id, ...baseQueryArg };
}
}, [board_id]);
const { total } = useListImagesQuery(queryArg ?? skipToken, {
selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
});
return total;
}; };

View File

@ -10,6 +10,8 @@ import { $authToken, $baseUrl } from 'services/api/client';
export const tagTypes = [ export const tagTypes = [
'Board', 'Board',
'BoardImagesTotal',
'BoardAssetsTotal',
'Image', 'Image',
'ImageNameList', 'ImageNameList',
'ImageList', 'ImageList',

View File

@ -1305,7 +1305,7 @@ export type components = {
* @description The nodes in this graph * @description The nodes in this graph
*/ */
nodes?: { nodes?: {
[key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined; [key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
}; };
/** /**
* Edges * Edges
@ -1348,7 +1348,7 @@ export type components = {
* @description The results of node executions * @description The results of node executions
*/ */
results: { results: {
[key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined; [key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
}; };
/** /**
* Errors * Errors
@ -5355,6 +5355,12 @@ export type components = {
*/ */
image?: components["schemas"]["ImageField"]; image?: components["schemas"]["ImageField"];
}; };
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/** /**
* StableDiffusionXLModelFormat * StableDiffusionXLModelFormat
* @description An enumeration. * @description An enumeration.
@ -5367,12 +5373,6 @@ export type components = {
* @enum {string} * @enum {string}
*/ */
StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
}; };
responses: never; responses: never;
parameters: never; parameters: never;
@ -5483,7 +5483,7 @@ export type operations = {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]; "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
}; };
}; };
responses: { responses: {
@ -5520,7 +5520,7 @@ export type operations = {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]; "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
}; };
}; };
responses: { responses: {
@ -6046,8 +6046,12 @@ export type operations = {
image_category: components["schemas"]["ImageCategory"]; image_category: components["schemas"]["ImageCategory"];
/** @description Whether this is an intermediate image */ /** @description Whether this is an intermediate image */
is_intermediate: boolean; is_intermediate: boolean;
/** @description The board to add this image to, if any */
board_id?: string;
/** @description The session ID associated with this upload, if any */ /** @description The session ID associated with this upload, if any */
session_id?: string; session_id?: string;
/** @description Whether to crop the image */
crop_visible?: boolean;
}; };
}; };
requestBody: { requestBody: {

View File

@ -64,9 +64,23 @@ const invokeAI = defineStyle((props) => {
}; };
}); });
const invokeAIOutline = defineStyle((props) => {
const { colorScheme: c } = props;
const borderColor = mode(`gray.200`, `whiteAlpha.300`)(props);
return {
border: '1px solid',
borderColor: c === 'gray' ? borderColor : 'currentColor',
'.chakra-button__group[data-attached][data-orientation=horizontal] > &:not(:last-of-type)':
{ marginEnd: '-1px' },
'.chakra-button__group[data-attached][data-orientation=vertical] > &:not(:last-of-type)':
{ marginBottom: '-1px' },
};
});
export const buttonTheme = defineStyleConfig({ export const buttonTheme = defineStyleConfig({
variants: { variants: {
invokeAI, invokeAI,
invokeAIOutline,
}, },
defaultProps: { defaultProps: {
variant: 'invokeAI', variant: 'invokeAI',

View File

@ -78,12 +78,12 @@ export const theme: ThemeOverride = {
hoverSelected: { hoverSelected: {
light: light:
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-500)', '0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-500)',
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-300)', dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-400)',
}, },
hoverUnselected: { hoverUnselected: {
light: light:
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-200)', '0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 3px var(--invokeai-colors-accent-500)',
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-600)', dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 3px var(--invokeai-colors-accent-400)',
}, },
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`, nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`,
}, },

View File

@ -13,7 +13,7 @@
- [ ] No, because: - [ ] No, because:
## Have you updated relevant documentation? ## Have you updated all relevant documentation?
- [ ] Yes - [ ] Yes
- [ ] No - [ ] No