mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into release/3-0-0
This commit is contained in:
commit
eb4ca4042e
@ -24,7 +24,7 @@ title: Home
|
||||
|
||||
[![CI checks on main badge]][ci checks on main 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 prs badge]][github open prs link]
|
||||
@ -54,10 +54,10 @@ title: Home
|
||||
[github stars badge]:
|
||||
https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github
|
||||
[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
|
||||
[latest commit to dev link]:
|
||||
https://github.com/invoke-ai/InvokeAI/commits/development
|
||||
https://github.com/invoke-ai/InvokeAI/commits/main -->
|
||||
[latest release badge]:
|
||||
https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github
|
||||
[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.
|
||||
|
||||
## :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
|
||||
|
||||
### :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
|
||||
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
|
||||
|
||||
|
@ -124,9 +124,9 @@ experimental versions later.
|
||||
[latest release](https://github.com/invoke-ai/InvokeAI/releases/latest),
|
||||
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**.
|
||||
|
||||
4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new
|
||||
|
@ -15,7 +15,7 @@ See the [troubleshooting
|
||||
section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated
|
||||
install guide for frequently-encountered installation issues.
|
||||
|
||||
## Main Application
|
||||
## Installation options
|
||||
|
||||
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
|
||||
give you to access experimental features.
|
||||
|
||||
|
||||
✅ This is the recommended option for first time users.
|
||||
|
||||
2. [Manual Installation](020_INSTALL_MANUAL.md)
|
||||
|
||||
In this method you will manually run the commands needed to install
|
||||
|
@ -1,13 +1,17 @@
|
||||
# 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 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
|
||||
|
||||
--------------------------------
|
||||
|
@ -1,4 +1,5 @@
|
||||
# Nodes
|
||||
|
||||
## What are Nodes?
|
||||
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
|
||||
@ -10,7 +11,7 @@ You can read more about nodes and the node editor [here](../features/NODES.md).
|
||||
|
||||
|
||||
## 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
|
||||
@ -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).
|
||||
|
||||
Once you’ve 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
|
||||
- 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.
|
||||
- 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.
|
||||
* 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
|
||||
* 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.
|
||||
|
||||
### Community Node Template
|
||||
|
||||
|
@ -40,9 +40,15 @@ async def upload_image(
|
||||
response: Response,
|
||||
image_category: ImageCategory = Query(description="The category of the 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(
|
||||
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:
|
||||
"""Uploads an image"""
|
||||
if not file.content_type.startswith("image"):
|
||||
@ -52,6 +58,9 @@ async def upload_image(
|
||||
|
||||
try:
|
||||
pil_image = Image.open(io.BytesIO(contents))
|
||||
if crop_visible:
|
||||
bbox = pil_image.getbbox()
|
||||
pil_image = pil_image.crop(bbox)
|
||||
except:
|
||||
# Error opening the image
|
||||
raise HTTPException(status_code=415, detail="Failed to read image")
|
||||
@ -62,6 +71,7 @@ async def upload_image(
|
||||
image_origin=ResourceOrigin.EXTERNAL,
|
||||
image_category=image_category,
|
||||
session_id=session_id,
|
||||
board_id=board_id,
|
||||
is_intermediate=is_intermediate,
|
||||
)
|
||||
|
||||
|
@ -52,6 +52,7 @@ class ImageServiceABC(ABC):
|
||||
image_category: ImageCategory,
|
||||
node_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
board_id: Optional[str] = None,
|
||||
is_intermediate: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> ImageDTO:
|
||||
@ -174,6 +175,7 @@ class ImageService(ImageServiceABC):
|
||||
image_category: ImageCategory,
|
||||
node_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
board_id: Optional[str] = None,
|
||||
is_intermediate: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> ImageDTO:
|
||||
@ -215,6 +217,11 @@ class ImageService(ImageServiceABC):
|
||||
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(
|
||||
image_name=image_name, image=image, metadata=metadata, graph=graph
|
||||
)
|
||||
|
@ -1,4 +1,6 @@
|
||||
import math
|
||||
import torch
|
||||
import diffusers
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
@ -175,9 +175,7 @@ export const isValidDrop = (
|
||||
const destinationBoard = overData.context.boardId;
|
||||
|
||||
const isSameBoard = currentBoard === destinationBoard;
|
||||
const isDestinationValid = !currentBoard
|
||||
? destinationBoard !== 'no_board'
|
||||
: true;
|
||||
const isDestinationValid = !currentBoard ? destinationBoard : true;
|
||||
|
||||
return !isSameBoard && isDestinationValid;
|
||||
}
|
||||
|
@ -19,10 +19,10 @@ export const addFirstListImagesListener = () => {
|
||||
action,
|
||||
{ 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 (
|
||||
action.meta.arg.queryCacheKey !==
|
||||
getListImagesUrl({ categories: IMAGE_CATEGORIES })
|
||||
getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
boardIdSelected,
|
||||
galleryViewChanged,
|
||||
imageSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
getBoardIdQueryParamForBoard,
|
||||
getCategoriesQueryParamForBoard,
|
||||
} from 'features/gallery/store/util';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { startAppListening } from '..';
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
export const addBoardIdSelectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: boardIdSelected,
|
||||
matcher: isAnyOf(boardIdSelected, galleryViewChanged),
|
||||
effect: async (
|
||||
action,
|
||||
{ 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
|
||||
cancelActiveListeners();
|
||||
|
||||
const _board_id = action.payload;
|
||||
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
|
||||
const state = getState();
|
||||
|
||||
const categories = getCategoriesQueryParamForBoard(_board_id);
|
||||
const board_id = getBoardIdQueryParamForBoard(_board_id);
|
||||
const queryArgs = { board_id, categories };
|
||||
const board_id = boardIdSelected.match(action)
|
||||
? action.payload
|
||||
: 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
|
||||
// must use getState() to ensure we do not have stale state
|
||||
@ -35,7 +44,7 @@ export const addBoardIdSelectedListener = () => {
|
||||
() =>
|
||||
imagesApi.endpoints.listImages.select(queryArgs)(getState())
|
||||
.isSuccess,
|
||||
1000
|
||||
5000
|
||||
);
|
||||
|
||||
if (isSuccess) {
|
||||
|
@ -45,7 +45,7 @@ export const addCanvasMergedListener = () => {
|
||||
relativeTo: canvasBaseLayer.getParent(),
|
||||
});
|
||||
|
||||
const imageUploadedRequest = dispatch(
|
||||
const imageDTO = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([blob], 'mergedCanvas.png', {
|
||||
type: 'image/png',
|
||||
@ -57,17 +57,10 @@ export const addCanvasMergedListener = () => {
|
||||
toastOptions: { title: 'Canvas Merged' },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const [{ payload }] = await take(
|
||||
(uploadedImageAction) =>
|
||||
imagesApi.endpoints.uploadImage.matchFulfilled(uploadedImageAction) &&
|
||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||
);
|
||||
).unwrap();
|
||||
|
||||
// TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
|
||||
const { image_name } =
|
||||
payload as typeof imagesApi.endpoints.uploadImage.Types.ResultType;
|
||||
const { image_name } = imageDTO;
|
||||
|
||||
dispatch(
|
||||
setMergedCanvas({
|
||||
|
@ -34,6 +34,8 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
}),
|
||||
image_category: 'general',
|
||||
is_intermediate: false,
|
||||
board_id: state.gallery.autoAddBoardId,
|
||||
crop_visible: true,
|
||||
postUploadAction: {
|
||||
type: 'TOAST',
|
||||
toastOptions: { title: 'Canvas Saved to Gallery' },
|
||||
|
@ -156,14 +156,13 @@ export const addImageDroppedListener = () => {
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO &&
|
||||
overData.context.boardId
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { imageDTO } = activeData.payload;
|
||||
const { boardId } = overData.context;
|
||||
|
||||
// if the board is "No Board", this is a remove action
|
||||
if (boardId === 'no_board') {
|
||||
// image was droppe on the "NoBoardBoard"
|
||||
if (!boardId) {
|
||||
dispatch(
|
||||
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||
imageDTO,
|
||||
@ -172,12 +171,7 @@ export const addImageDroppedListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle adding image to batch
|
||||
if (boardId === 'batch') {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Otherwise, add the image to the board
|
||||
// image was dropped on a user board
|
||||
dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
imageDTO,
|
||||
|
@ -5,30 +5,30 @@ import { startAppListening } from '..';
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
export const addImageUpdatedFulfilledListener = () => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
moduleLog.debug(
|
||||
{
|
||||
data: {
|
||||
oldImage: action.meta.arg.originalArgs,
|
||||
updatedImage: action.payload,
|
||||
},
|
||||
},
|
||||
'Image updated'
|
||||
);
|
||||
},
|
||||
});
|
||||
// startAppListening({
|
||||
// matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||
// effect: (action, { dispatch, getState }) => {
|
||||
// moduleLog.debug(
|
||||
// {
|
||||
// data: {
|
||||
// oldImage: action.meta.arg.originalArgs,
|
||||
// updatedImage: action.payload,
|
||||
// },
|
||||
// },
|
||||
// 'Image updated'
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
};
|
||||
|
||||
export const addImageUpdatedRejectedListener = () => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||
effect: (action, { dispatch }) => {
|
||||
moduleLog.debug(
|
||||
{ data: action.meta.arg.originalArgs },
|
||||
'Image update failed'
|
||||
);
|
||||
},
|
||||
});
|
||||
// startAppListening({
|
||||
// matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||
// effect: (action, { dispatch }) => {
|
||||
// moduleLog.debug(
|
||||
// { data: action.meta.arg.originalArgs },
|
||||
// 'Image update failed'
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
};
|
||||
|
@ -8,10 +8,7 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { startAppListening } from '..';
|
||||
import {
|
||||
SYSTEM_BOARDS,
|
||||
imagesApi,
|
||||
} from '../../../../../services/api/endpoints/images';
|
||||
import { imagesApi } from '../../../../../services/api/endpoints/images';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -26,7 +23,7 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const imageDTO = action.payload;
|
||||
const state = getState();
|
||||
const { selectedBoardId } = state.gallery;
|
||||
const { selectedBoardId, autoAddBoardId } = state.gallery;
|
||||
|
||||
moduleLog.debug({ arg: '<Blob>', imageDTO }, 'Image uploaded');
|
||||
|
||||
@ -44,13 +41,13 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
// default action - just upload and alert user
|
||||
if (postUploadAction?.type === 'TOAST') {
|
||||
const { toastOptions } = postUploadAction;
|
||||
if (SYSTEM_BOARDS.includes(selectedBoardId)) {
|
||||
if (!autoAddBoardId) {
|
||||
dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
|
||||
} else {
|
||||
// Add this image to the board
|
||||
dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
board_id: selectedBoardId,
|
||||
board_id: autoAddBoardId,
|
||||
imageDTO,
|
||||
})
|
||||
);
|
||||
@ -59,10 +56,10 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
const { data } = boardsApi.endpoints.listAllBoards.select()(state);
|
||||
|
||||
// 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
|
||||
? `Added to board ${board.board_name}`
|
||||
: `Added to board ${selectedBoardId}`;
|
||||
: `Added to board ${autoAddBoardId}`;
|
||||
|
||||
dispatch(
|
||||
addToast({
|
||||
|
@ -3,6 +3,7 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
IMAGE_CATEGORIES,
|
||||
boardIdSelected,
|
||||
galleryViewChanged,
|
||||
imageSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||
@ -55,37 +56,16 @@ export const addInvocationCompleteEventListener = () => {
|
||||
}
|
||||
|
||||
if (!imageDTO.is_intermediate) {
|
||||
// update the cache for 'All Images'
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
categories: IMAGE_CATEGORIES,
|
||||
},
|
||||
(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;
|
||||
}
|
||||
)
|
||||
);
|
||||
/**
|
||||
* Cache updates for when an image result is received
|
||||
* - *add* to getImageDTO
|
||||
* - IF `autoAddBoardId` is set:
|
||||
* - THEN add it to the board_id/images
|
||||
* - ELSE (`autoAddBoardId` is not set):
|
||||
* - THEN add it to the no_board/images
|
||||
*/
|
||||
|
||||
const { autoAddBoardId } = gallery;
|
||||
|
||||
// add image to the board if auto-add is enabled
|
||||
if (autoAddBoardId) {
|
||||
dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
@ -93,8 +73,31 @@ export const addInvocationCompleteEventListener = () => {
|
||||
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;
|
||||
|
||||
// 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 (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
|
||||
dispatch(boardIdSelected(autoAddBoardId));
|
||||
dispatch(galleryViewChanged('images'));
|
||||
} else if (!autoAddBoardId) {
|
||||
dispatch(boardIdSelected('images'));
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
}
|
||||
|
@ -12,25 +12,35 @@ export const addStagingAreaImageSavedListener = () => {
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const { imageDTO } = action.payload;
|
||||
|
||||
dispatch(
|
||||
imagesApi.endpoints.updateImage.initiate({
|
||||
imageDTO,
|
||||
changes: { is_intermediate: false },
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
.then((image) => {
|
||||
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Saving Failed',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
try {
|
||||
const newImageDTO = await dispatch(
|
||||
imagesApi.endpoints.changeImageIsIntermediate.initiate({
|
||||
imageDTO,
|
||||
is_intermediate: false,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// we may need to add it to the autoadd board
|
||||
const { autoAddBoardId } = getState().gallery;
|
||||
|
||||
if (autoAddBoardId) {
|
||||
await dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
imageDTO: newImageDTO,
|
||||
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',
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -73,7 +73,7 @@ export const addUserInvokedCanvasListener = () => {
|
||||
// For img2img and inpaint/outpaint, we need to upload the init images
|
||||
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
|
||||
// upload the image, saving the request id
|
||||
const { requestId: initImageUploadedRequestId } = dispatch(
|
||||
canvasInitImage = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([baseBlob], 'canvasInitImage.png', {
|
||||
type: 'image/png',
|
||||
@ -81,23 +81,13 @@ export const addUserInvokedCanvasListener = () => {
|
||||
image_category: 'general',
|
||||
is_intermediate: true,
|
||||
})
|
||||
);
|
||||
|
||||
// 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;
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
// For inpaint/outpaint, we also need to upload the mask layer
|
||||
if (['inpaint', 'outpaint'].includes(generationMode)) {
|
||||
// upload the image, saving the request id
|
||||
const { requestId: maskImageUploadedRequestId } = dispatch(
|
||||
canvasMaskImage = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([maskBlob], 'canvasMaskImage.png', {
|
||||
type: 'image/png',
|
||||
@ -105,17 +95,7 @@ export const addUserInvokedCanvasListener = () => {
|
||||
image_category: 'mask',
|
||||
is_intermediate: true,
|
||||
})
|
||||
);
|
||||
|
||||
// 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;
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
const graph = buildCanvasGraph(
|
||||
@ -141,14 +121,14 @@ export const addUserInvokedCanvasListener = () => {
|
||||
sessionCreated.fulfilled.match(action) &&
|
||||
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
|
||||
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
|
||||
dispatch(
|
||||
imagesApi.endpoints.updateImage.initiate({
|
||||
imagesApi.endpoints.changeImageSessionId.initiate({
|
||||
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
|
||||
if (['inpaint'].includes(generationMode) && canvasMaskImage) {
|
||||
dispatch(
|
||||
imagesApi.endpoints.updateImage.initiate({
|
||||
imagesApi.endpoints.changeImageSessionId.initiate({
|
||||
imageDTO: canvasMaskImage,
|
||||
changes: { session_id: sessionId },
|
||||
session_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -167,7 +147,7 @@ export const addUserInvokedCanvasListener = () => {
|
||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||
dispatch(
|
||||
stagingAreaInitialized({
|
||||
sessionId,
|
||||
sessionId: session_id,
|
||||
boundingBox: {
|
||||
...state.canvas.boundingBoxCoordinates,
|
||||
...state.canvas.boundingBoxDimensions,
|
||||
@ -177,7 +157,7 @@ export const addUserInvokedCanvasListener = () => {
|
||||
}
|
||||
|
||||
// Flag the session with the canvas session ID
|
||||
dispatch(canvasSessionIdChanged(sessionId));
|
||||
dispatch(canvasSessionIdChanged(session_id));
|
||||
|
||||
// We are ready to invoke the session!
|
||||
dispatch(sessionReadyToInvoke());
|
||||
|
@ -92,7 +92,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
||||
sx={{
|
||||
p: 4,
|
||||
borderBottomRadius: 'base',
|
||||
bg: mode('base.100', 'base.800')(colorMode),
|
||||
bg: 'base.100',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -18,12 +18,20 @@ import {
|
||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
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 { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import IAIDraggable from './IAIDraggable';
|
||||
import IAIDroppable from './IAIDroppable';
|
||||
import SelectionOverlay from './SelectionOverlay';
|
||||
|
||||
type IAIDndImageProps = {
|
||||
imageDTO: ImageDTO | undefined;
|
||||
@ -49,6 +57,7 @@ type IAIDndImageProps = {
|
||||
thumbnail?: boolean;
|
||||
noContentFallback?: ReactElement;
|
||||
useThumbailFallback?: boolean;
|
||||
withHoverOverlay?: boolean;
|
||||
};
|
||||
|
||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
@ -75,9 +84,17 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
resetIcon = <FaUndo />,
|
||||
noContentFallback = <IAINoContentFallback icon={FaImage} />,
|
||||
useThumbailFallback,
|
||||
withHoverOverlay = false,
|
||||
} = props;
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
const handleMouseOut = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||
postUploadAction,
|
||||
@ -105,6 +122,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
{(ref) => (
|
||||
<Flex
|
||||
ref={ref}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
sx={{
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
@ -147,14 +166,14 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: {
|
||||
shadow: isSelected ? 'selected.dark' : undefined,
|
||||
},
|
||||
...imageSx,
|
||||
}}
|
||||
/>
|
||||
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
|
||||
<SelectionOverlay
|
||||
isSelected={isSelected}
|
||||
isHovered={withHoverOverlay ? isHovered : false}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{!imageDTO && !isUploadDisabled && (
|
||||
|
@ -19,10 +19,11 @@ import { useUploadImageMutation } from 'services/api/endpoints/images';
|
||||
import { PostUploadAction } from 'services/api/types';
|
||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
|
||||
const selector = createSelector(
|
||||
[activeTabNameSelector],
|
||||
(activeTabName) => {
|
||||
[stateSelector, activeTabNameSelector],
|
||||
({ gallery }, activeTabName) => {
|
||||
let postUploadAction: PostUploadAction = { type: 'TOAST' };
|
||||
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
@ -33,7 +34,10 @@ const selector = createSelector(
|
||||
postUploadAction = { type: 'SET_INITIAL_IMAGE' };
|
||||
}
|
||||
|
||||
const { autoAddBoardId } = gallery;
|
||||
|
||||
return {
|
||||
autoAddBoardId,
|
||||
postUploadAction,
|
||||
};
|
||||
},
|
||||
@ -46,7 +50,7 @@ type ImageUploaderProps = {
|
||||
|
||||
const ImageUploader = (props: ImageUploaderProps) => {
|
||||
const { children } = props;
|
||||
const { postUploadAction } = useAppSelector(selector);
|
||||
const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
|
||||
const isBusy = useAppSelector(selectIsBusy);
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
@ -74,9 +78,10 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
image_category: 'user',
|
||||
is_intermediate: false,
|
||||
postUploadAction,
|
||||
board_id: autoAddBoardId,
|
||||
});
|
||||
},
|
||||
[postUploadAction, uploadImage]
|
||||
[autoAddBoardId, postUploadAction, uploadImage]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
|
@ -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;
|
@ -1,3 +1,4 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
||||
@ -31,6 +32,9 @@ export const useImageUploadButton = ({
|
||||
postUploadAction,
|
||||
isDisabled,
|
||||
}: UseImageUploadButtonArgs) => {
|
||||
const autoAddBoardId = useAppSelector(
|
||||
(state) => state.gallery.autoAddBoardId
|
||||
);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const onDropAccepted = useCallback(
|
||||
(files: File[]) => {
|
||||
@ -45,9 +49,10 @@ export const useImageUploadButton = ({
|
||||
image_category: 'user',
|
||||
is_intermediate: false,
|
||||
postUploadAction: postUploadAction ?? { type: 'TOAST' },
|
||||
board_id: autoAddBoardId,
|
||||
});
|
||||
},
|
||||
[postUploadAction, uploadImage]
|
||||
[autoAddBoardId, postUploadAction, uploadImage]
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -98,12 +98,16 @@ const ParamEmbeddingPopover = (props: Props) => {
|
||||
sx={{ p: 0, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }}
|
||||
>
|
||||
{data.length === 0 ? (
|
||||
<Flex sx={{ justifyContent: 'center', p: 2 }}>
|
||||
<Text
|
||||
sx={{ fontSize: 'sm', color: 'base.500', _dark: 'base.700' }}
|
||||
>
|
||||
No Embeddings Loaded
|
||||
</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
p: 2,
|
||||
fontSize: 'sm',
|
||||
color: 'base.500',
|
||||
_dark: { color: 'base.700' },
|
||||
}}
|
||||
>
|
||||
<Text>No Embeddings Loaded</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<IAIMantineSearchableSelect
|
||||
|
@ -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;
|
@ -52,7 +52,7 @@ const BoardAutoAddSelect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
|
||||
dispatch(autoAddBoardIdChanged(v === 'none' ? undefined : v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
@ -1,17 +1,23 @@
|
||||
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaFolder } from 'react-icons/fa';
|
||||
import {
|
||||
autoAddBoardIdChanged,
|
||||
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 { menuListMotionProps } from 'theme/components/menu';
|
||||
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 = {
|
||||
board?: BoardDTO;
|
||||
board_id: string;
|
||||
board_id?: string;
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||
};
|
||||
@ -19,9 +25,32 @@ type Props = {
|
||||
const BoardContextMenu = memo(
|
||||
({ board, board_id, setBoardToDelete, children }: Props) => {
|
||||
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(() => {
|
||||
dispatch(boardIdSelected(board?.board_id ?? board_id));
|
||||
}, [board?.board_id, board_id, dispatch]);
|
||||
dispatch(boardIdSelected(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
|
||||
const handleSetAutoAdd = useCallback(() => {
|
||||
dispatch(autoAddBoardIdChanged(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
|
||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
@ -33,17 +62,24 @@ const BoardContextMenu = memo(
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={skipEvent}
|
||||
>
|
||||
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
|
||||
Select Board
|
||||
</MenuItem>
|
||||
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
|
||||
{board && (
|
||||
<GalleryBoardContextMenuItems
|
||||
board={board}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
)}
|
||||
<MenuGroup title={boardName}>
|
||||
<MenuItem
|
||||
icon={<FaPlus />}
|
||||
isDisabled={isAutoAdd}
|
||||
onClick={handleSetAutoAdd}
|
||||
>
|
||||
Auto-add to this Board
|
||||
</MenuItem>
|
||||
{!board && <NoBoardContextMenuItems />}
|
||||
{board && (
|
||||
<GalleryBoardContextMenuItems
|
||||
board={board}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
)}
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
)}
|
||||
>
|
||||
|
@ -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;
|
@ -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;
|
@ -16,6 +16,7 @@ import AddBoardButton from './AddBoardButton';
|
||||
import BoardsSearch from './BoardsSearch';
|
||||
import GalleryBoard from './GalleryBoard';
|
||||
import SystemBoardButton from './SystemBoardButton';
|
||||
import NoBoardBoard from './NoBoardBoard';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
@ -42,10 +43,6 @@ const BoardsList = (props: Props) => {
|
||||
)
|
||||
: boards;
|
||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const handleClickSearchIcon = useCallback(() => {
|
||||
setIsSearching((v) => !v);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -61,54 +58,7 @@ const BoardsList = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{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 />}
|
||||
/>
|
||||
<BoardsSearch />
|
||||
<AddBoardButton />
|
||||
</Flex>
|
||||
<OverlayScrollbarsComponent
|
||||
@ -126,10 +76,13 @@ const BoardsList = (props: Props) => {
|
||||
<Grid
|
||||
className="list-container"
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(96px, 1fr));`,
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(108px, 1fr));`,
|
||||
maxH: 346,
|
||||
}}
|
||||
>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<NoBoardBoard isSelected={selectedBoardId === undefined} />
|
||||
</GridItem>
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board) => (
|
||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||
|
@ -28,12 +28,7 @@ const selector = createSelector(
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type Props = {
|
||||
setIsSearching: (isSearching: boolean) => void;
|
||||
};
|
||||
|
||||
const BoardsSearch = (props: Props) => {
|
||||
const { setIsSearching } = props;
|
||||
const BoardsSearch = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { searchText } = useAppSelector(selector);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -47,8 +42,7 @@ const BoardsSearch = (props: Props) => {
|
||||
|
||||
const clearBoardSearch = useCallback(() => {
|
||||
dispatch(setBoardSearchText(''));
|
||||
setIsSearching(false);
|
||||
}, [dispatch, setIsSearching]);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleKeydown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
|
@ -19,16 +19,14 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
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 { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
import AutoAddIcon from '../AutoAddIcon';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'accent.200',
|
||||
color: 'blackAlpha.900',
|
||||
};
|
||||
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||
|
||||
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'base.500',
|
||||
@ -59,11 +57,19 @@ const GalleryBoard = memo(
|
||||
);
|
||||
|
||||
const { isSelectedForAutoAdd } = useAppSelector(selector);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
const handleMouseOut = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||
board.cover_image_name ?? skipToken
|
||||
);
|
||||
|
||||
const { totalImages, totalAssets } = useBoardTotal(board.board_id);
|
||||
|
||||
const { board_name, board_id } = board;
|
||||
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||
|
||||
@ -84,26 +90,30 @@ const GalleryBoard = memo(
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(newBoardName: string) => {
|
||||
if (!newBoardName) {
|
||||
// empty strings are not allowed
|
||||
async (newBoardName: string) => {
|
||||
// empty strings are not allowed
|
||||
if (!newBoardName.trim()) {
|
||||
setLocalBoardName(board_name);
|
||||
return;
|
||||
}
|
||||
|
||||
// don't updated the board name if it hasn't changed
|
||||
if (newBoardName === board_name) {
|
||||
// don't updated the board name if it hasn't changed
|
||||
return;
|
||||
}
|
||||
updateBoard({ board_id, changes: { board_name: newBoardName } })
|
||||
.unwrap()
|
||||
.then((response) => {
|
||||
// update local state
|
||||
setLocalBoardName(response.board_name);
|
||||
})
|
||||
.catch(() => {
|
||||
// revert on error
|
||||
setLocalBoardName(board_name);
|
||||
});
|
||||
|
||||
try {
|
||||
const { board_name } = await updateBoard({
|
||||
board_id,
|
||||
changes: { board_name: newBoardName },
|
||||
}).unwrap();
|
||||
|
||||
// update local state
|
||||
setLocalBoardName(board_name);
|
||||
} catch {
|
||||
// revert on error
|
||||
setLocalBoardName(board_name);
|
||||
}
|
||||
},
|
||||
[board_id, board_name, updateBoard]
|
||||
);
|
||||
@ -117,6 +127,8 @@ const GalleryBoard = memo(
|
||||
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
|
||||
>
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
@ -143,57 +155,49 @@ const GalleryBoard = memo(
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
cursor: 'pointer',
|
||||
bg: 'base.200',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
bg: 'base.200',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
sx={{
|
||||
objectFit: 'cover',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
borderBottomRadius: 'lg',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
boxSize={12}
|
||||
as={FaUser}
|
||||
sx={{
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
borderBottomRadius: 'lg',
|
||||
mt: -6,
|
||||
opacity: 0.7,
|
||||
color: 'base.500',
|
||||
_dark: {
|
||||
color: 'base.500',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
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
|
||||
</Flex>
|
||||
)}
|
||||
{/* <Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
@ -201,33 +205,14 @@ const GalleryBoard = memo(
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
variant="solid"
|
||||
sx={
|
||||
isSelectedForAutoAdd
|
||||
? AUTO_ADD_BADGE_STYLES
|
||||
: BASE_BADGE_STYLES
|
||||
}
|
||||
>
|
||||
{board.image_count}
|
||||
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||
{totalImages}/{totalAssets}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Box
|
||||
className="selection-box"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
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> */}
|
||||
{isSelectedForAutoAdd && <AutoAddIcon />}
|
||||
<SelectionOverlay
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
|
@ -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 {
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
boardIdSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
ListImagesArgs,
|
||||
useListImagesQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import GenericBoard from './GenericBoard';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import InvokeAILogoImage from 'assets/images/logo.png';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
import AutoAddIcon from '../AutoAddIcon';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
const baseQueryArg: ListImagesArgs = {
|
||||
board_id: 'none',
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'base.500',
|
||||
color: 'whiteAlpha.900',
|
||||
};
|
||||
interface Props {
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
const dispatch = useDispatch();
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
({ gallery }) => {
|
||||
const { autoAddBoardId } = gallery;
|
||||
return { autoAddBoardId };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(boardIdSelected('no_board'));
|
||||
};
|
||||
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
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, {
|
||||
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: 'no_board' },
|
||||
};
|
||||
const droppableData: MoveBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: 'no_board',
|
||||
actionType: 'MOVE_BOARD',
|
||||
context: { boardId: undefined },
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
board_id="no_board"
|
||||
droppableData={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaFolderOpen}
|
||||
label="No Board"
|
||||
badgeCount={total}
|
||||
/>
|
||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
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;
|
||||
|
@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
@ -42,7 +42,7 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
||||
|
||||
const handleToggleAutoAdd = useCallback(() => {
|
||||
dispatch(
|
||||
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id)
|
||||
autoAddBoardIdChanged(isSelectedForAutoAdd ? undefined : board.board_id)
|
||||
);
|
||||
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
|
||||
|
||||
@ -59,16 +59,15 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
||||
</MenuItem> */}
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />}
|
||||
onClickCapture={handleToggleAutoAdd}
|
||||
>
|
||||
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'}
|
||||
</MenuItem>
|
||||
{/* {!isSelectedForAutoAdd && (
|
||||
<MenuItem icon={<FaPlus />} onClick={handleToggleAutoAdd}>
|
||||
Auto-add to this Board
|
||||
</MenuItem>
|
||||
)} */}
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDelete}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete Board
|
||||
</MenuItem>
|
||||
|
@ -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);
|
@ -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);
|
@ -1,12 +1,11 @@
|
||||
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 { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
@ -27,52 +26,64 @@ const GalleryBoardName = (props: Props) => {
|
||||
const { isOpen, onToggle } = props;
|
||||
const { selectedBoardId } = useAppSelector(selector);
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
const numOfBoardImages = useBoardTotal(selectedBoardId);
|
||||
// const { totalImages, totalAssets } = useBoardTotal(selectedBoardId);
|
||||
|
||||
const formattedBoardName = useMemo(() => {
|
||||
if (!boardName) return '';
|
||||
if (boardName && !numOfBoardImages) return boardName;
|
||||
if (boardName.length > 20) {
|
||||
return `${boardName.substring(0, 20)}... (${numOfBoardImages})`;
|
||||
return `${boardName.substring(0, 20)}...`;
|
||||
}
|
||||
return `${boardName} (${numOfBoardImages})`;
|
||||
}, [boardName, numOfBoardImages]);
|
||||
return boardName;
|
||||
// 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 (
|
||||
<Flex
|
||||
as={Button}
|
||||
onClick={onToggle}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
// variant="ghost"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
gap: 2,
|
||||
w: 'full',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
px: 2,
|
||||
_hover: {
|
||||
bg: 'base.100',
|
||||
_dark: { bg: 'base.800' },
|
||||
},
|
||||
// bg: 'base.100',
|
||||
// _dark: { bg: 'base.800' },
|
||||
// _hover: {
|
||||
// bg: 'base.200',
|
||||
// _dark: { bg: 'base.700' },
|
||||
// },
|
||||
}}
|
||||
>
|
||||
<Spacer />
|
||||
<Box position="relative">
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{formattedBoardName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
w: '100%',
|
||||
textAlign: 'center',
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{formattedBoardName}
|
||||
</Text>
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
|
@ -35,6 +35,8 @@ import {
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
@ -70,7 +72,16 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
|
||||
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 } =
|
||||
useCopyImageToClipboard();
|
||||
|
@ -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 { 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 { memo, useRef } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
import GalleryPinButton from './GalleryPinButton';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
||||
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(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const { selectedBoardId } = state.gallery;
|
||||
const { selectedBoardId, galleryView } = state.gallery;
|
||||
|
||||
return {
|
||||
selectedBoardId,
|
||||
galleryView,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
@ -26,10 +41,19 @@ const selector = createSelector(
|
||||
const ImageGalleryContent = () => {
|
||||
const resizeObserverRef = 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 } =
|
||||
useDisclosure();
|
||||
|
||||
const handleClickImages = useCallback(() => {
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickAssets = useCallback(() => {
|
||||
dispatch(galleryViewChanged('assets'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<VStack
|
||||
sx={{
|
||||
@ -48,11 +72,11 @@ const ImageGalleryContent = () => {
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<GallerySettingsPopover />
|
||||
<GalleryBoardName
|
||||
isOpen={isBoardListOpen}
|
||||
onToggle={onToggleBoardList}
|
||||
/>
|
||||
<GallerySettingsPopover />
|
||||
<GalleryPinButton />
|
||||
</Flex>
|
||||
<Box>
|
||||
@ -60,6 +84,55 @@ const ImageGalleryContent = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
<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' ? (
|
||||
<BatchImageGrid />
|
||||
) : (
|
||||
|
@ -106,6 +106,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
isDropDisabled={true}
|
||||
isUploadDisabled={true}
|
||||
thumbnail={true}
|
||||
withHoverOverlay
|
||||
// resetIcon={<FaTrash />}
|
||||
// resetTooltip="Delete image"
|
||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
||||
|
@ -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 IAIButton from 'common/components/IAIButton';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { IMAGE_LIMIT } from 'features/gallery//store/gallerySlice';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
UseOverlayScrollbarsParams,
|
||||
useOverlayScrollbars,
|
||||
@ -15,10 +16,10 @@ import {
|
||||
useLazyListImagesQuery,
|
||||
useListImagesQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
import GalleryImage from './GalleryImage';
|
||||
import ImageGridItemContainer from './ImageGridItemContainer';
|
||||
import ImageGridListContainer from './ImageGridListContainer';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
|
||||
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
|
||||
defer: true,
|
||||
@ -40,7 +41,10 @@ const GalleryImageGrid = () => {
|
||||
const [initialize, osInstance] = useOverlayScrollbars(
|
||||
overlayScrollbarsConfig
|
||||
);
|
||||
|
||||
const selectedBoardId = useAppSelector(
|
||||
(state) => state.gallery.selectedBoardId
|
||||
);
|
||||
const { currentViewTotal } = useBoardTotal(selectedBoardId);
|
||||
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
||||
|
||||
const { currentData, isFetching, isSuccess, isError } =
|
||||
@ -49,19 +53,23 @@ const GalleryImageGrid = () => {
|
||||
const [listImages] = useLazyListImagesQuery();
|
||||
|
||||
const areMoreAvailable = useMemo(() => {
|
||||
if (!currentData) {
|
||||
if (!currentData || !currentViewTotal) {
|
||||
return false;
|
||||
}
|
||||
return currentData.ids.length < currentData.total;
|
||||
}, [currentData]);
|
||||
return currentData.ids.length < currentViewTotal;
|
||||
}, [currentData, currentViewTotal]);
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
if (!areMoreAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
listImages({
|
||||
...queryArgs,
|
||||
offset: currentData?.ids.length ?? 0,
|
||||
limit: IMAGE_LIMIT,
|
||||
});
|
||||
}, [listImages, queryArgs, currentData?.ids.length]);
|
||||
}, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize the gallery's custom scrollbar
|
||||
@ -79,20 +87,34 @@ const GalleryImageGrid = () => {
|
||||
|
||||
if (!currentData) {
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full' }}>
|
||||
<Spinner size="2xl" opacity={0.5} />
|
||||
</Box>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IAINoContentFallback label="Loading..." icon={FaImage} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSuccess && currentData?.ids.length === 0) {
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full' }}>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@ -121,9 +143,7 @@ const GalleryImageGrid = () => {
|
||||
loadingText="Loading"
|
||||
flexShrink={0}
|
||||
>
|
||||
{areMoreAvailable
|
||||
? t('gallery.loadMore')
|
||||
: t('gallery.allImagesLoaded')}
|
||||
{`Load More (${currentData.ids.length} of ${currentViewTotal})`}
|
||||
</IAIButton>
|
||||
</>
|
||||
);
|
||||
|
@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
IMAGE_LIMIT,
|
||||
imageSelected,
|
||||
selectImagesById,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { useCallback } from 'react';
|
||||
@ -53,8 +52,8 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
|
||||
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
||||
|
||||
const nextImageId = images[nextImageIndex].image_name;
|
||||
const prevImageId = images[prevImageIndex].image_name;
|
||||
const nextImageId = images[nextImageIndex]?.image_name;
|
||||
const prevImageId = images[prevImageIndex]?.image_name;
|
||||
|
||||
const nextImage = selectors.selectById(data, nextImageId);
|
||||
const prevImage = selectors.selectById(data, prevImageId);
|
||||
@ -65,7 +64,7 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
isOnLastImage:
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
areMoreImagesAvailable: data?.total ?? 0 > imagesLength,
|
||||
areMoreImagesAvailable: (data?.total ?? 0) > imagesLength,
|
||||
isFetching: status === 'pending',
|
||||
nextImage,
|
||||
prevImage,
|
||||
|
@ -2,11 +2,11 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { ListImagesArgs } from 'services/api/endpoints/images';
|
||||
import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
|
||||
import {
|
||||
getBoardIdQueryParamForBoard,
|
||||
getCategoriesQueryParamForBoard,
|
||||
} from './util';
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
} from './gallerySlice';
|
||||
|
||||
export const gallerySelector = (state: RootState) => state.gallery;
|
||||
|
||||
@ -19,14 +19,13 @@ export const selectLastSelectedImage = createSelector(
|
||||
export const selectListImagesBaseQueryArgs = createSelector(
|
||||
[(state: RootState) => state],
|
||||
(state) => {
|
||||
const { selectedBoardId } = state.gallery;
|
||||
|
||||
const categories = getCategoriesQueryParamForBoard(selectedBoardId);
|
||||
const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
|
||||
const { selectedBoardId, galleryView } = state.gallery;
|
||||
const categories =
|
||||
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||
|
||||
const listImagesBaseQueryArgs: ListImagesArgs = {
|
||||
board_id: selectedBoardId ?? 'none',
|
||||
categories,
|
||||
board_id,
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { ImageCategory } from 'services/api/types';
|
||||
@ -14,20 +14,17 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
||||
export const INITIAL_IMAGE_LIMIT = 100;
|
||||
export const IMAGE_LIMIT = 20;
|
||||
|
||||
// export type GalleryView = 'images' | 'assets';
|
||||
export type BoardId =
|
||||
| 'images'
|
||||
| 'assets'
|
||||
| 'no_board'
|
||||
| 'batch'
|
||||
| (string & Record<never, never>);
|
||||
export type GalleryView = 'images' | 'assets';
|
||||
// export type BoardId = 'no_board' | (string & Record<never, never>);
|
||||
export type BoardId = string | undefined;
|
||||
|
||||
type GalleryState = {
|
||||
selection: string[];
|
||||
shouldAutoSwitch: boolean;
|
||||
autoAddBoardId: string | null;
|
||||
autoAddBoardId: string | undefined;
|
||||
galleryImageMinimumWidth: number;
|
||||
selectedBoardId: BoardId;
|
||||
galleryView: GalleryView;
|
||||
batchImageNames: string[];
|
||||
isBatchEnabled: boolean;
|
||||
};
|
||||
@ -35,9 +32,10 @@ type GalleryState = {
|
||||
export const initialGalleryState: GalleryState = {
|
||||
selection: [],
|
||||
shouldAutoSwitch: true,
|
||||
autoAddBoardId: null,
|
||||
autoAddBoardId: undefined,
|
||||
galleryImageMinimumWidth: 96,
|
||||
selectedBoardId: 'images',
|
||||
selectedBoardId: undefined,
|
||||
galleryView: 'images',
|
||||
batchImageNames: [],
|
||||
isBatchEnabled: false,
|
||||
};
|
||||
@ -46,14 +44,8 @@ export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState: initialGalleryState,
|
||||
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>) => {
|
||||
// MULTI SELECT LOGIC
|
||||
// const rangeEndImageName = action.payload;
|
||||
// const lastSelectedImage = state.selection[state.selection.length - 1];
|
||||
// const filteredImages = selectFilteredImagesLocal(state);
|
||||
@ -74,6 +66,7 @@ export const gallerySlice = createSlice({
|
||||
// }
|
||||
},
|
||||
imageSelectionToggled: (state, action: PayloadAction<string>) => {
|
||||
// MULTI SELECT LOGIC
|
||||
// if (
|
||||
// state.selection.includes(action.payload) &&
|
||||
// state.selection.length > 1
|
||||
@ -96,6 +89,7 @@ export const gallerySlice = createSlice({
|
||||
},
|
||||
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||
state.selectedBoardId = action.payload;
|
||||
state.galleryView = 'images';
|
||||
},
|
||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isBatchEnabled = action.payload;
|
||||
@ -125,23 +119,27 @@ export const gallerySlice = createSlice({
|
||||
state.batchImageNames = [];
|
||||
state.selection = [];
|
||||
},
|
||||
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||
autoAddBoardIdChanged: (
|
||||
state,
|
||||
action: PayloadAction<string | undefined>
|
||||
) => {
|
||||
state.autoAddBoardId = action.payload;
|
||||
},
|
||||
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
|
||||
state.galleryView = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addMatcher(
|
||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||
(state, action) => {
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
if (deletedBoardId === state.selectedBoardId) {
|
||||
state.selectedBoardId = 'images';
|
||||
}
|
||||
if (deletedBoardId === state.autoAddBoardId) {
|
||||
state.autoAddBoardId = null;
|
||||
}
|
||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
if (deletedBoardId === state.selectedBoardId) {
|
||||
state.selectedBoardId = undefined;
|
||||
state.galleryView = 'images';
|
||||
}
|
||||
);
|
||||
if (deletedBoardId === state.autoAddBoardId) {
|
||||
state.autoAddBoardId = undefined;
|
||||
}
|
||||
});
|
||||
builder.addMatcher(
|
||||
boardsApi.endpoints.listAllBoards.matchFulfilled,
|
||||
(state, action) => {
|
||||
@ -151,7 +149,7 @@ export const gallerySlice = createSlice({
|
||||
}
|
||||
|
||||
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
||||
state.autoAddBoardId = null;
|
||||
state.autoAddBoardId = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -170,6 +168,12 @@ export const {
|
||||
imagesAddedToBatch,
|
||||
imagesRemovedFromBatch,
|
||||
autoAddBoardIdChanged,
|
||||
galleryViewChanged,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
||||
const isAnyBoardDeleted = isAnyOf(
|
||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||
boardsApi.endpoints.deleteBoardAndImages.matchFulfilled
|
||||
);
|
||||
|
@ -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 { ImageCategory, ImageDTO } from 'services/api/types';
|
||||
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
|
||||
|
||||
export const getCategoriesQueryParamForBoard = (
|
||||
board_id: BoardId
|
||||
@ -20,16 +19,11 @@ export const getCategoriesQueryParamForBoard = (
|
||||
|
||||
export const getBoardIdQueryParamForBoard = (
|
||||
board_id: BoardId
|
||||
): string | undefined => {
|
||||
if (board_id === 'no_board') {
|
||||
): string | null => {
|
||||
if (board_id === undefined) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
// system boards besides 'no_board'
|
||||
if (SYSTEM_BOARDS.includes(board_id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// user boards
|
||||
return board_id;
|
||||
};
|
||||
@ -52,3 +46,10 @@ export const getBoardIdFromBoardAndCategoriesQueryParam = (
|
||||
|
||||
return board_id ?? 'UNKNOWN_BOARD';
|
||||
};
|
||||
|
||||
export const getCategories = (imageDTO: ImageDTO) => {
|
||||
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||
return IMAGE_CATEGORIES;
|
||||
}
|
||||
return ASSETS_CATEGORIES;
|
||||
};
|
||||
|
@ -78,7 +78,6 @@ const ParametersDrawer = () => {
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
paddingTop={1.5}
|
||||
paddingBottom={4}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
|
@ -164,7 +164,7 @@ const ResizableDrawer = ({
|
||||
sx={{
|
||||
borderColor: mode('base.200', 'base.800')(colorMode),
|
||||
p: 4,
|
||||
bg: mode('base.100', 'base.900')(colorMode),
|
||||
bg: mode('base.50', 'base.900')(colorMode),
|
||||
height: 'full',
|
||||
shadow: isOpen ? 'dark-lg' : undefined,
|
||||
...containerStyles,
|
||||
|
@ -1,52 +1,36 @@
|
||||
import { ImageDTO, OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
|
||||
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'];
|
||||
import { api } from '..';
|
||||
|
||||
export const boardImagesApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
/**
|
||||
* Board Images Queries
|
||||
*/
|
||||
|
||||
listBoardImages: build.query<
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
ListBoardImagesArg
|
||||
>({
|
||||
query: ({ board_id, offset, limit }) => ({
|
||||
url: `board_images/${board_id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => {
|
||||
// any list of boardimages
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
// and individual tags for each boardimage
|
||||
tags.push(
|
||||
...result.items.map(({ board_id, image_name }) => ({
|
||||
type: 'BoardImage' as const,
|
||||
id: `${board_id}_${image_name}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return tags;
|
||||
},
|
||||
}),
|
||||
// listBoardImages: build.query<
|
||||
// OffsetPaginatedResults_ImageDTO_,
|
||||
// ListBoardImagesArg
|
||||
// >({
|
||||
// query: ({ board_id, offset, limit }) => ({
|
||||
// url: `board_images/${board_id}`,
|
||||
// method: 'GET',
|
||||
// }),
|
||||
// providesTags: (result, error, arg) => {
|
||||
// // any list of boardimages
|
||||
// const tags: ApiFullTagDescription[] = [
|
||||
// { type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
|
||||
// ];
|
||||
// if (result) {
|
||||
// // and individual tags for each boardimage
|
||||
// tags.push(
|
||||
// ...result.items.map(({ board_id, image_name }) => ({
|
||||
// type: 'BoardImage' as const,
|
||||
// id: `${board_id}_${image_name}`,
|
||||
// }))
|
||||
// );
|
||||
// }
|
||||
// return tags;
|
||||
// },
|
||||
// }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useListBoardImagesQuery } = boardImagesApi;
|
||||
// export const { useListBoardImagesQuery } = boardImagesApi;
|
||||
|
@ -109,10 +109,25 @@ export const boardsApi = api.injectEndpoints({
|
||||
|
||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg },
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
// 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 }) {
|
||||
/**
|
||||
@ -167,24 +182,14 @@ export const boardsApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(draft);
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.updateMany(draft, updates);
|
||||
const newCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(newState);
|
||||
draft.total = Math.max(
|
||||
draft.total - (oldCount - newCount),
|
||||
0
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// after deleting a board, select the 'All Images' board
|
||||
dispatch(boardIdSelected('images'));
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
@ -197,9 +202,24 @@ export const boardsApi = api.injectEndpoints({
|
||||
method: 'DELETE',
|
||||
params: { include_images: true },
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg },
|
||||
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
{
|
||||
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 }) {
|
||||
/**
|
||||
@ -231,27 +251,17 @@ export const boardsApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(draft);
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeMany(
|
||||
draft,
|
||||
deleted_images
|
||||
);
|
||||
const newCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(newState);
|
||||
draft.total = Math.max(
|
||||
draft.total - (oldCount - newCount),
|
||||
0
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// after deleting a board, select the 'All Images' board
|
||||
dispatch(boardIdSelected('images'));
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
|
@ -6,18 +6,17 @@ import {
|
||||
BoardId,
|
||||
IMAGE_CATEGORIES,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { omit } from 'lodash-es';
|
||||
import { getCategories } from 'features/gallery/store/util';
|
||||
import queryString from 'query-string';
|
||||
import { ApiFullTagDescription, api } from '..';
|
||||
import { components, paths } from '../schema';
|
||||
import {
|
||||
ImageCategory,
|
||||
ImageChanges,
|
||||
ImageDTO,
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
PostUploadAction,
|
||||
} from '../types';
|
||||
import { getCacheAction } from './util';
|
||||
import { getIsImageInDateRange } from './util';
|
||||
|
||||
export type ListImagesArgs = NonNullable<
|
||||
paths['/api/v1/images/']['get']['parameters']['query']
|
||||
@ -51,8 +50,6 @@ export const imagesSelectors = imagesAdapter.getSelectors();
|
||||
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
|
||||
`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
|
||||
|
||||
export const SYSTEM_BOARDS = ['images', 'assets', 'no_board', 'batch'];
|
||||
|
||||
export const imagesApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
/**
|
||||
@ -155,6 +152,42 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
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>({
|
||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||
invalidatesTags: ['IntermediatesCount'],
|
||||
@ -164,56 +197,42 @@ export const imagesApi = api.injectEndpoints({
|
||||
url: `images/${image_name}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Image', id: arg.image_name },
|
||||
invalidatesTags: (result, error, { board_id }) => [
|
||||
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
|
||||
],
|
||||
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
|
||||
/**
|
||||
* Cache changes for `deleteImage`:
|
||||
* - *remove* from "All Images" / "All Assets"
|
||||
* - IF it has a board:
|
||||
* - THEN *remove* from it's own board
|
||||
* - ELSE *remove* from "No Board"
|
||||
* - NOT POSSIBLE: *remove* from getImageDTO
|
||||
* - $cache = [board_id|no_board]/[images|assets]
|
||||
* - *remove* from $cache
|
||||
*/
|
||||
|
||||
const { image_name, board_id, image_category } = imageDTO;
|
||||
const { image_name, board_id } = imageDTO;
|
||||
|
||||
// Figure out the `listImages` caches that we need to update
|
||||
// That means constructing the possible query args that are serialized into the cache key...
|
||||
|
||||
const removeFromCacheKeys: ListImagesArgs[] = [];
|
||||
// Store patches so we can undo if the query fails
|
||||
const patches: PatchCollection[] = [];
|
||||
|
||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
||||
? IMAGE_CATEGORIES
|
||||
: ASSETS_CATEGORIES;
|
||||
// $cache = [board_id|no_board]/[images|assets]
|
||||
const categories = getCategories(imageDTO);
|
||||
|
||||
// remove from "All Images"
|
||||
removeFromCacheKeys.push({ categories });
|
||||
|
||||
if (board_id) {
|
||||
// remove from it's own board
|
||||
removeFromCacheKeys.push({ board_id });
|
||||
} else {
|
||||
// remove from "No Board"
|
||||
removeFromCacheKeys.push({ board_id: 'none' });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
)
|
||||
// *remove* from $cache
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{ board_id: board_id ?? 'none', categories },
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(draft, image_name);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
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;
|
||||
// For now, we will not allow image categories to change
|
||||
changes: Omit<ImageChanges, 'image_category'>;
|
||||
}
|
||||
{ imageDTO: ImageDTO; is_intermediate: boolean }
|
||||
>({
|
||||
query: ({ imageDTO, changes }) => ({
|
||||
query: ({ imageDTO, is_intermediate }) => ({
|
||||
url: `images/${imageDTO.image_name}`,
|
||||
method: 'PATCH',
|
||||
body: changes,
|
||||
body: { is_intermediate },
|
||||
}),
|
||||
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(
|
||||
{ imageDTO: oldImageDTO, changes: _changes },
|
||||
{ imageDTO, is_intermediate },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
// let's be extra-sure we do not accidentally change categories
|
||||
const changes = omit(_changes, 'image_category');
|
||||
|
||||
/**
|
||||
* Cache changes for "updateImage":
|
||||
* - *update* "getImageDTO" cache
|
||||
* - for "All Images" || "All Assets":
|
||||
* - IF it is not already in the cache
|
||||
* - THEN *add* it to "All Images" / "All Assets" and update the total
|
||||
* - ELSE *update* it
|
||||
* - IF the image has a board:
|
||||
* - THEN *update* it's own board
|
||||
* - ELSE *update* the "No Board" board
|
||||
* Cache changes for `changeImageIsIntermediate`:
|
||||
* - *update* getImageDTO
|
||||
* - $cache = [board_id|no_board]/[images|assets]
|
||||
* - IF it is being changed to an intermediate:
|
||||
* - remove from $cache
|
||||
* - ELSE (it is being changed to a non-intermediate):
|
||||
* - IF it eligible for insertion into existing $cache:
|
||||
* - *upsert* to $cache
|
||||
*/
|
||||
|
||||
// Store patches so we can undo if the query fails
|
||||
const patches: PatchCollection[] = [];
|
||||
const { image_name, board_id, image_category, is_intermediate } =
|
||||
oldImageDTO;
|
||||
|
||||
const isChangingFromIntermediate = changes.is_intermediate === false;
|
||||
// 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
|
||||
// *update* getImageDTO
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, changes);
|
||||
Object.assign(draft, { is_intermediate });
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Update the "All Image" or "All Assets" board
|
||||
const queryArgsToUpdate: ListImagesArgs[] = [{ categories }];
|
||||
// $cache = [board_id|no_board]/[images|assets]
|
||||
const categories = getCategories(imageDTO);
|
||||
|
||||
// IF the image has a board:
|
||||
if (board_id) {
|
||||
// THEN update it's own board
|
||||
queryArgsToUpdate.push({ board_id });
|
||||
if (is_intermediate) {
|
||||
// IF it is being changed to an intermediate:
|
||||
// remove from $cache
|
||||
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 update the "No Board" board
|
||||
queryArgsToUpdate.push({ board_id: 'none' });
|
||||
}
|
||||
// ELSE (it is being changed to a non-intermediate):
|
||||
console.log(imageDTO);
|
||||
const queryArgs = {
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories,
|
||||
};
|
||||
|
||||
queryArgsToUpdate.forEach((queryArg) => {
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArg)(
|
||||
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||
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(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArg,
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
// One of the common changes is to make a canvas intermediate a non-intermediate,
|
||||
// i.e. save a canvas image to the gallery.
|
||||
// If that was the change, need to add the image to the cache instead of updating
|
||||
// the existing cache entry.
|
||||
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,
|
||||
});
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
await queryFulfilled;
|
||||
@ -354,9 +420,18 @@ export const imagesApi = api.injectEndpoints({
|
||||
is_intermediate: boolean;
|
||||
postUploadAction?: PostUploadAction;
|
||||
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();
|
||||
formData.append('file', file);
|
||||
return {
|
||||
@ -367,14 +442,32 @@ export const imagesApi = api.injectEndpoints({
|
||||
image_category,
|
||||
is_intermediate,
|
||||
session_id,
|
||||
board_id,
|
||||
crop_visible,
|
||||
},
|
||||
};
|
||||
},
|
||||
async onQueryStarted(
|
||||
{ file, image_category, is_intermediate, postUploadAction },
|
||||
{
|
||||
file,
|
||||
image_category,
|
||||
is_intermediate,
|
||||
postUploadAction,
|
||||
session_id,
|
||||
board_id,
|
||||
},
|
||||
{ dispatch, queryFulfilled }
|
||||
) {
|
||||
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;
|
||||
|
||||
if (imageDTO.is_intermediate) {
|
||||
@ -382,21 +475,42 @@ export const imagesApi = api.injectEndpoints({
|
||||
return;
|
||||
}
|
||||
|
||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
||||
? IMAGE_CATEGORIES
|
||||
: ASSETS_CATEGORIES;
|
||||
// *add* to `getImageDTO`
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData(
|
||||
'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(
|
||||
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total = draft.total + 1;
|
||||
})
|
||||
imagesApi.util.invalidateTags([
|
||||
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||
])
|
||||
);
|
||||
} catch {
|
||||
// no-op
|
||||
// query failed, no action needed
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -412,102 +526,102 @@ export const imagesApi = api.injectEndpoints({
|
||||
body: { board_id, image_name },
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'BoardImage' },
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
invalidatesTags: (result, error, { board_id, imageDTO }) => [
|
||||
{ type: 'Board', id: 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(
|
||||
{ board_id, imageDTO: oldImageDTO },
|
||||
{ board_id, imageDTO },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
/**
|
||||
* Cache changes for `addImageToBoard`:
|
||||
* - *update* the `getImageDTO` cache
|
||||
* - *remove* from "No Board"
|
||||
* - IF the image has an old `board_id`:
|
||||
* - THEN *remove* from it's old `board_id`
|
||||
* - IF the image's `created_at` is within the range of the board's cached images
|
||||
* - OR the board cache has length of 0 or 1
|
||||
* - THEN *add* it to new `board_id`
|
||||
* - *update* getImageDTO
|
||||
* - IF it is intermediate:
|
||||
* - BAIL OUT ON FURTHER CHANGES
|
||||
* - IF it has an old board_id:
|
||||
* - THEN *remove* from old board_id/[images|assets]
|
||||
* - ELSE *remove* from no_board/[images|assets]
|
||||
* - $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 categories = getCategories(imageDTO);
|
||||
|
||||
// Updated imageDTO with new board_id
|
||||
const newImageDTO = { ...oldImageDTO, board_id };
|
||||
|
||||
// Update getImageDTO cache
|
||||
// *update* getImageDTO
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, newImageDTO);
|
||||
Object.assign(draft, { board_id });
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Do the "Remove from" cache updates
|
||||
removeFromQueryArgs.forEach((queryArgs) => {
|
||||
if (!imageDTO.is_intermediate) {
|
||||
// *remove* from [no_board|board_id]/[images|assets]
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArgs,
|
||||
{
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories,
|
||||
},
|
||||
(draft) => {
|
||||
// sanity check
|
||||
if (draft.ids.includes(image_name)) {
|
||||
imagesAdapter.removeOne(draft, image_name);
|
||||
draft.total = Math.max(draft.total - 1, 0);
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(
|
||||
draft,
|
||||
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
|
||||
if (!SYSTEM_BOARDS.includes(board_id)) {
|
||||
const queryArgs = { board_id };
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||
// $cache = board_id/[images|assets]
|
||||
const queryArgs = { board_id: board_id ?? 'none', categories };
|
||||
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||
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)) {
|
||||
// Do the "Add to" cache updates
|
||||
const isCacheFullyPopulated =
|
||||
currentCache.data &&
|
||||
currentCache.data.ids.length >= currentCache.data.total;
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
imageDTO
|
||||
);
|
||||
|
||||
if (isCacheFullyPopulated || isInDateRange) {
|
||||
// THEN *add* to $cache
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
if (cacheAction === 'add') {
|
||||
imagesAdapter.addOne(draft, newImageDTO);
|
||||
draft.total += 1;
|
||||
} else {
|
||||
imagesAdapter.updateOne(draft, {
|
||||
id: image_name,
|
||||
changes: { board_id },
|
||||
});
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -531,87 +645,97 @@ export const imagesApi = api.injectEndpoints({
|
||||
body: { board_id, image_name },
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'BoardImage' },
|
||||
{ type: 'Board', id: arg.imageDTO.board_id },
|
||||
invalidatesTags: (result, error, { imageDTO }) => [
|
||||
{ type: 'Board', id: 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(
|
||||
{ imageDTO },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
/**
|
||||
* Cache changes for `removeImageFromBoard`:
|
||||
* - *update* `getImageDTO`
|
||||
* - IF the image's `created_at` is within the range of the board's cached images
|
||||
* - THEN *add* to "No Board"
|
||||
* - *remove* from `old_board_id`
|
||||
* Cache changes for removeImageFromBoard:
|
||||
* - *update* getImageDTO
|
||||
* - *remove* from board_id/[images|assets]
|
||||
* - $cache = no_board/[images|assets]
|
||||
* - 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[] = [];
|
||||
|
||||
// Updated imageDTO with new board_id
|
||||
const newImageDTO = { ...imageDTO, board_id: undefined };
|
||||
|
||||
// Update getImageDTO cache
|
||||
// *update* getImageDTO
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, newImageDTO);
|
||||
Object.assign(draft, { board_id: undefined });
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Remove from old board
|
||||
if (old_board_id) {
|
||||
const oldBoardQueryArgs = { board_id: old_board_id };
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
oldBoardQueryArgs,
|
||||
(draft) => {
|
||||
// sanity check
|
||||
if (draft.ids.includes(image_name)) {
|
||||
imagesAdapter.removeOne(draft, image_name);
|
||||
draft.total = Math.max(draft.total - 1, 0);
|
||||
}
|
||||
}
|
||||
)
|
||||
// *remove* from board_id/[images|assets]
|
||||
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;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Add to "No Board"
|
||||
const noBoardQueryArgs = { board_id: 'none' };
|
||||
const { data } = imagesApi.endpoints.listImages.select(
|
||||
noBoardQueryArgs
|
||||
)(getState());
|
||||
// $cache = no_board/[images|assets]
|
||||
const queryArgs = { board_id: 'none', categories };
|
||||
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||
getState()
|
||||
);
|
||||
|
||||
// Check if we need to make any cache changes
|
||||
const cacheAction = getCacheAction(data, imageDTO);
|
||||
// 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 =
|
||||
currentCache.data &&
|
||||
currentCache.data.ids.length >= currentCache.data.total;
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
imageDTO
|
||||
);
|
||||
|
||||
if (isCacheFullyPopulated || isInDateRange) {
|
||||
// THEN *upsert* to $cache
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
noBoardQueryArgs,
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
if (cacheAction === 'add') {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total += 1;
|
||||
} else {
|
||||
imagesAdapter.updateOne(draft, {
|
||||
id: image_name,
|
||||
changes: { board_id: undefined },
|
||||
});
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -635,7 +759,8 @@ export const {
|
||||
useGetImageDTOQuery,
|
||||
useGetImageMetadataQuery,
|
||||
useDeleteImageMutation,
|
||||
useUpdateImageMutation,
|
||||
useGetBoardImagesTotalQuery,
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useUploadImageMutation,
|
||||
useAddImageToBoardMutation,
|
||||
useRemoveImageFromBoardMutation,
|
||||
|
@ -25,27 +25,27 @@ export const getIsImageInDateRange = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the action we should take when an image may need to be added or updated in a cache.
|
||||
*/
|
||||
export const getCacheAction = (
|
||||
data: ImageCache | undefined,
|
||||
imageDTO: ImageDTO
|
||||
): 'add' | 'update' | 'none' => {
|
||||
const isInDateRange = getIsImageInDateRange(data, imageDTO);
|
||||
const isCacheFullyPopulated = data && data.total === data.ids.length;
|
||||
const shouldUpdateCache =
|
||||
Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
|
||||
// /**
|
||||
// * Determines the action we should take when an image may need to be added or updated in a cache.
|
||||
// */
|
||||
// export const getCacheAction = (
|
||||
// data: ImageCache | undefined,
|
||||
// imageDTO: ImageDTO
|
||||
// ): 'add' | 'update' | 'none' => {
|
||||
// const isInDateRange = getIsImageInDateRange(data, imageDTO);
|
||||
// const isCacheFullyPopulated = data && data.total === data.ids.length;
|
||||
// const shouldUpdateCache =
|
||||
// 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) {
|
||||
return 'update';
|
||||
}
|
||||
// if (shouldUpdateCache && isImageInCache) {
|
||||
// return 'update';
|
||||
// }
|
||||
|
||||
if (shouldUpdateCache && !isImageInCache) {
|
||||
return 'add';
|
||||
}
|
||||
// if (shouldUpdateCache && !isImageInCache) {
|
||||
// return 'add';
|
||||
// }
|
||||
|
||||
return 'none';
|
||||
};
|
||||
// return 'none';
|
||||
// };
|
||||
|
@ -4,19 +4,8 @@ import { useListAllBoardsQuery } from '../endpoints/boards';
|
||||
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
let boardName = '';
|
||||
if (board_id === 'images') {
|
||||
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';
|
||||
}
|
||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||
const boardName = selectedBoard?.board_name || 'Uncategorized';
|
||||
|
||||
return { boardName };
|
||||
},
|
||||
|
@ -1,53 +1,21 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
BoardId,
|
||||
IMAGE_CATEGORIES,
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
import { useMemo } from 'react';
|
||||
import { ListImagesArgs, useListImagesQuery } from '../endpoints/images';
|
||||
import {
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useGetBoardImagesTotalQuery,
|
||||
} from '../endpoints/images';
|
||||
|
||||
const baseQueryArg: ListImagesArgs = {
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
};
|
||||
|
||||
const imagesQueryArg: ListImagesArgs = {
|
||||
categories: IMAGE_CATEGORIES,
|
||||
...baseQueryArg,
|
||||
};
|
||||
|
||||
const assetsQueryArg: ListImagesArgs = {
|
||||
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;
|
||||
export const useBoardTotal = (board_id: BoardId) => {
|
||||
const galleryView = useAppSelector((state) => state.gallery.galleryView);
|
||||
|
||||
const { data: totalImages } = useGetBoardImagesTotalQuery(board_id);
|
||||
const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
|
||||
|
||||
const currentViewTotal = useMemo(
|
||||
() => (galleryView === 'images' ? totalImages : totalAssets),
|
||||
[galleryView, totalAssets, totalImages]
|
||||
);
|
||||
|
||||
return { totalImages, totalAssets, currentViewTotal };
|
||||
};
|
||||
|
@ -10,6 +10,8 @@ import { $authToken, $baseUrl } from 'services/api/client';
|
||||
|
||||
export const tagTypes = [
|
||||
'Board',
|
||||
'BoardImagesTotal',
|
||||
'BoardAssetsTotal',
|
||||
'Image',
|
||||
'ImageNameList',
|
||||
'ImageList',
|
||||
|
@ -1305,7 +1305,7 @@ export type components = {
|
||||
* @description The nodes in this graph
|
||||
*/
|
||||
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
|
||||
@ -1348,7 +1348,7 @@ export type components = {
|
||||
* @description The results of node executions
|
||||
*/
|
||||
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
|
||||
@ -5355,6 +5355,12 @@ export type components = {
|
||||
*/
|
||||
image?: components["schemas"]["ImageField"];
|
||||
};
|
||||
/**
|
||||
* StableDiffusion2ModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusionXLModelFormat
|
||||
* @description An enumeration.
|
||||
@ -5367,12 +5373,6 @@ export type components = {
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusion2ModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@ -5483,7 +5483,7 @@ export type operations = {
|
||||
};
|
||||
requestBody: {
|
||||
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: {
|
||||
@ -5520,7 +5520,7 @@ export type operations = {
|
||||
};
|
||||
requestBody: {
|
||||
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: {
|
||||
@ -6046,8 +6046,12 @@ export type operations = {
|
||||
image_category: components["schemas"]["ImageCategory"];
|
||||
/** @description Whether this is an intermediate image */
|
||||
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 */
|
||||
session_id?: string;
|
||||
/** @description Whether to crop the image */
|
||||
crop_visible?: boolean;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
|
@ -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({
|
||||
variants: {
|
||||
invokeAI,
|
||||
invokeAIOutline,
|
||||
},
|
||||
defaultProps: {
|
||||
variant: 'invokeAI',
|
||||
|
@ -78,12 +78,12 @@ export const theme: ThemeOverride = {
|
||||
hoverSelected: {
|
||||
light:
|
||||
'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: {
|
||||
light:
|
||||
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-200)',
|
||||
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-600)',
|
||||
'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 3px var(--invokeai-colors-accent-400)',
|
||||
},
|
||||
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`,
|
||||
},
|
||||
|
@ -13,7 +13,7 @@
|
||||
- [ ] No, because:
|
||||
|
||||
|
||||
## Have you updated relevant documentation?
|
||||
## Have you updated all relevant documentation?
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user