mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into nodepromptsize
This commit is contained in:
commit
0689e36390
2
.github/workflows/mkdocs-material.yml
vendored
2
.github/workflows/mkdocs-material.yml
vendored
@ -2,7 +2,7 @@ name: mkdocs-material
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'refs/heads/v2.3'
|
- 'refs/heads/main'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
59
README.md
59
README.md
@ -36,15 +36,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
_**Note: This is an alpha release. Bugs are expected and not all
|
|
||||||
features are fully implemented. Please use the GitHub [Issues
|
|
||||||
pages](https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen)
|
|
||||||
to report unexpected problems. Also note that InvokeAI root directory
|
|
||||||
which contains models, outputs and configuration files, has changed
|
|
||||||
between the 2.x and 3.x release. If you wish to use your v2.3 root
|
|
||||||
directory with v3.0, please follow the directions in [Migrating a 2.3
|
|
||||||
root directory to 3.0](#migrating-to-3).**_
|
|
||||||
|
|
||||||
InvokeAI is a leading creative engine built to empower professionals
|
InvokeAI is a leading creative engine built to empower professionals
|
||||||
and enthusiasts alike. Generate and create stunning visual media using
|
and enthusiasts alike. Generate and create stunning visual media using
|
||||||
the latest AI-driven technologies. InvokeAI offers an industry leading
|
the latest AI-driven technologies. InvokeAI offers an industry leading
|
||||||
@ -264,19 +255,24 @@ old models directory (which contains the models selected at install
|
|||||||
time) will be renamed `models.orig` and can be deleted once you have
|
time) will be renamed `models.orig` and can be deleted once you have
|
||||||
confirmed that the migration was successful.
|
confirmed that the migration was successful.
|
||||||
|
|
||||||
|
If you wish, you can pass the 2.3 root directory to both `--from` and
|
||||||
|
`--to` in order to update in place. Warning: this directory will no
|
||||||
|
longer be usable with InvokeAI 2.3.
|
||||||
|
|
||||||
#### Migrating in place
|
#### Migrating in place
|
||||||
|
|
||||||
For the adventurous, you may do an in-place upgrade from 2.3 to 3.0
|
For the adventurous, you may do an in-place upgrade from 2.3 to 3.0
|
||||||
without touching the command line. The recipe is as follows>
|
without touching the command line. ***This recipe does not work on
|
||||||
|
Windows platforms due to a bug in the Windows version of the 2.3
|
||||||
|
upgrade script.** See the next section for a Windows recipe.
|
||||||
|
|
||||||
|
##### For Mac and Linux Users:
|
||||||
|
|
||||||
1. Launch the InvokeAI launcher script in your current v2.3 root directory.
|
1. Launch the InvokeAI launcher script in your current v2.3 root directory.
|
||||||
|
|
||||||
2. Select option [9] "Update InvokeAI" to bring up the updater dialog.
|
2. Select option [9] "Update InvokeAI" to bring up the updater dialog.
|
||||||
|
|
||||||
3a. During the alpha release phase, select option [3] and manually
|
3. Select option [1] to upgrade to the latest release.
|
||||||
enter the tag name `v3.0.0+a2`.
|
|
||||||
|
|
||||||
3b. Once 3.0 is released, select option [1] to upgrade to the latest release.
|
|
||||||
|
|
||||||
4. Once the upgrade is finished you will be returned to the launcher
|
4. Once the upgrade is finished you will be returned to the launcher
|
||||||
menu. Select option [7] "Re-run the configure script to fix a broken
|
menu. Select option [7] "Re-run the configure script to fix a broken
|
||||||
@ -295,14 +291,33 @@ worked, you can safely remove these files. Alternatively you can
|
|||||||
restore a working v2.3 directory by removing the new files and
|
restore a working v2.3 directory by removing the new files and
|
||||||
restoring the ".orig" files' original names.
|
restoring the ".orig" files' original names.
|
||||||
|
|
||||||
|
##### For Windows Users:
|
||||||
|
|
||||||
|
Windows Users can upgrade with the
|
||||||
|
|
||||||
|
1. Enter the 2.3 root directory you wish to upgrade
|
||||||
|
2. Launch `invoke.sh` or `invoke.bat`
|
||||||
|
3. Select the "Developer's console" option [8]
|
||||||
|
4. Type the following commands
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install "invokeai @ https://github.com/invoke-ai/InvokeAI/archive/refs/tags/v3.0.0" --use-pep517 --upgrade
|
||||||
|
invokeai-configure --root .
|
||||||
|
```
|
||||||
|
(Replace `v3.0.0` with the current release number if this document is out of date).
|
||||||
|
|
||||||
|
The first command will install and upgrade new software to run
|
||||||
|
InvokeAI. The second will prepare the 2.3 directory for use with 3.0.
|
||||||
|
You may now launch the WebUI in the usual way, by selecting option [1]
|
||||||
|
from the launcher script
|
||||||
|
|
||||||
#### Migration Caveats
|
#### Migration Caveats
|
||||||
|
|
||||||
The migration script will migrate your invokeai settings and models,
|
The migration script will migrate your invokeai settings and models,
|
||||||
including textual inversion models, LoRAs and merges that you may have
|
including textual inversion models, LoRAs and merges that you may have
|
||||||
installed previously. However it does **not** migrate the generated
|
installed previously. However it does **not** migrate the generated
|
||||||
images stored in your 2.3-format outputs directory. The released
|
images stored in your 2.3-format outputs directory. You will need to
|
||||||
version of 3.0 is expected to have an interface for importing an
|
manually import selected images into the 3.0 gallery via drag-and-drop.
|
||||||
entire directory of image files as a batch.
|
|
||||||
|
|
||||||
## Hardware Requirements
|
## Hardware Requirements
|
||||||
|
|
||||||
@ -314,9 +329,12 @@ AMD card (using the ROCm driver).
|
|||||||
|
|
||||||
You will need one of the following:
|
You will need one of the following:
|
||||||
|
|
||||||
- An NVIDIA-based graphics card with 4 GB or more VRAM memory.
|
- An NVIDIA-based graphics card with 4 GB or more VRAM memory. 6-8 GB
|
||||||
|
of VRAM is highly recommended for rendering using the Stable
|
||||||
|
Diffusion XL models
|
||||||
- An Apple computer with an M1 chip.
|
- An Apple computer with an M1 chip.
|
||||||
- An AMD-based graphics card with 4GB or more VRAM memory. (Linux only)
|
- An AMD-based graphics card with 4GB or more VRAM memory (Linux
|
||||||
|
only), 6-8 GB for XL rendering.
|
||||||
|
|
||||||
We do not recommend the GTX 1650 or 1660 series video cards. They are
|
We do not recommend the GTX 1650 or 1660 series video cards. They are
|
||||||
unable to run in half-precision mode and do not have sufficient VRAM
|
unable to run in half-precision mode and do not have sufficient VRAM
|
||||||
@ -349,13 +367,12 @@ Invoke AI provides an organized gallery system for easily storing, accessing, an
|
|||||||
### Other features
|
### Other features
|
||||||
|
|
||||||
- *Support for both ckpt and diffusers models*
|
- *Support for both ckpt and diffusers models*
|
||||||
- *SD 2.0, 2.1 support*
|
- *SD 2.0, 2.1, XL support*
|
||||||
- *Upscaling Tools*
|
- *Upscaling Tools*
|
||||||
- *Embedding Manager & Support*
|
- *Embedding Manager & Support*
|
||||||
- *Model Manager & Support*
|
- *Model Manager & Support*
|
||||||
- *Node-Based Architecture*
|
- *Node-Based Architecture*
|
||||||
- *Node-Based Plug-&-Play UI (Beta)*
|
- *Node-Based Plug-&-Play UI (Beta)*
|
||||||
- *SDXL Support* (Coming soon)
|
|
||||||
|
|
||||||
### Latest Changes
|
### Latest Changes
|
||||||
|
|
||||||
|
1
docs/assets/sdxl-graphs/sdxl-base-example1.json
Normal file
1
docs/assets/sdxl-graphs/sdxl-base-example1.json
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/sdxl-graphs/sdxl-base-refine-example1.json
Normal file
1
docs/assets/sdxl-graphs/sdxl-base-refine-example1.json
Normal file
File diff suppressed because one or more lines are too long
@ -24,7 +24,7 @@ title: Home
|
|||||||
|
|
||||||
[![CI checks on main badge]][ci checks on main link]
|
[![CI checks on main badge]][ci checks on main link]
|
||||||
[![CI checks on dev badge]][ci checks on dev link]
|
[![CI checks on dev badge]][ci checks on dev link]
|
||||||
[![latest commit to dev badge]][latest commit to dev link]
|
<!-- [![latest commit to dev badge]][latest commit to dev link] -->
|
||||||
|
|
||||||
[![github open issues badge]][github open issues link]
|
[![github open issues badge]][github open issues link]
|
||||||
[![github open prs badge]][github open prs link]
|
[![github open prs badge]][github open prs link]
|
||||||
@ -54,10 +54,10 @@ title: Home
|
|||||||
[github stars badge]:
|
[github stars badge]:
|
||||||
https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github
|
https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github
|
||||||
[github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers
|
[github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers
|
||||||
[latest commit to dev badge]:
|
<!-- [latest commit to dev badge]:
|
||||||
https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900
|
https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900
|
||||||
[latest commit to dev link]:
|
[latest commit to dev link]:
|
||||||
https://github.com/invoke-ai/InvokeAI/commits/development
|
https://github.com/invoke-ai/InvokeAI/commits/main -->
|
||||||
[latest release badge]:
|
[latest release badge]:
|
||||||
https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github
|
https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github
|
||||||
[latest release link]: https://github.com/invoke-ai/InvokeAI/releases
|
[latest release link]: https://github.com/invoke-ai/InvokeAI/releases
|
||||||
@ -82,6 +82,25 @@ Q&A</a>]
|
|||||||
|
|
||||||
This fork is rapidly evolving. Please use the [Issues tab](https://github.com/invoke-ai/InvokeAI/issues) to report bugs and make feature requests. Be sure to use the provided templates. They will help aid diagnose issues faster.
|
This fork is rapidly evolving. Please use the [Issues tab](https://github.com/invoke-ai/InvokeAI/issues) to report bugs and make feature requests. Be sure to use the provided templates. They will help aid diagnose issues faster.
|
||||||
|
|
||||||
|
## :octicons-package-dependencies-24: Installation
|
||||||
|
|
||||||
|
This fork is supported across Linux, Windows and Macintosh. Linux users can use
|
||||||
|
either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
|
||||||
|
driver).
|
||||||
|
|
||||||
|
### [Installation Getting Started Guide](installation)
|
||||||
|
#### **[Automated Installer](installation/010_INSTALL_AUTOMATED.md)**
|
||||||
|
✅ This is the recommended installation method for first-time users.
|
||||||
|
#### [Manual Installation](installation/020_INSTALL_MANUAL.md)
|
||||||
|
This method is recommended for experienced users and developers
|
||||||
|
#### [Docker Installation](installation/040_INSTALL_DOCKER.md)
|
||||||
|
This method is recommended for those familiar with running Docker containers
|
||||||
|
### Other Installation Guides
|
||||||
|
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
|
||||||
|
- [XFormers](installation/070_INSTALL_XFORMERS.md)
|
||||||
|
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
|
||||||
|
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
|
||||||
|
|
||||||
## :fontawesome-solid-computer: Hardware Requirements
|
## :fontawesome-solid-computer: Hardware Requirements
|
||||||
|
|
||||||
### :octicons-cpu-24: System
|
### :octicons-cpu-24: System
|
||||||
@ -107,24 +126,6 @@ images in full-precision mode:
|
|||||||
- At least 18 GB of free disk space for the machine learning model, Python, and
|
- At least 18 GB of free disk space for the machine learning model, Python, and
|
||||||
all its dependencies.
|
all its dependencies.
|
||||||
|
|
||||||
## :octicons-package-dependencies-24: Installation
|
|
||||||
|
|
||||||
This fork is supported across Linux, Windows and Macintosh. Linux users can use
|
|
||||||
either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
|
|
||||||
driver).
|
|
||||||
|
|
||||||
### [Installation Getting Started Guide](installation)
|
|
||||||
#### [Automated Installer](installation/010_INSTALL_AUTOMATED.md)
|
|
||||||
This method is recommended for 1st time users
|
|
||||||
#### [Manual Installation](installation/020_INSTALL_MANUAL.md)
|
|
||||||
This method is recommended for experienced users and developers
|
|
||||||
#### [Docker Installation](installation/040_INSTALL_DOCKER.md)
|
|
||||||
This method is recommended for those familiar with running Docker containers
|
|
||||||
### Other Installation Guides
|
|
||||||
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
|
|
||||||
- [XFormers](installation/070_INSTALL_XFORMERS.md)
|
|
||||||
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
|
|
||||||
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
|
|
||||||
|
|
||||||
## :octicons-gift-24: InvokeAI Features
|
## :octicons-gift-24: InvokeAI Features
|
||||||
|
|
||||||
|
@ -124,9 +124,9 @@ experimental versions later.
|
|||||||
[latest release](https://github.com/invoke-ai/InvokeAI/releases/latest),
|
[latest release](https://github.com/invoke-ai/InvokeAI/releases/latest),
|
||||||
and look for a file named:
|
and look for a file named:
|
||||||
|
|
||||||
- InvokeAI-installer-v2.X.X.zip
|
- InvokeAI-installer-v3.X.X.zip
|
||||||
|
|
||||||
where "2.X.X" is the latest released version. The file is located
|
where "3.X.X" is the latest released version. The file is located
|
||||||
at the very bottom of the release page, under **Assets**.
|
at the very bottom of the release page, under **Assets**.
|
||||||
|
|
||||||
4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new
|
4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new
|
||||||
|
@ -15,7 +15,7 @@ See the [troubleshooting
|
|||||||
section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated
|
section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated
|
||||||
install guide for frequently-encountered installation issues.
|
install guide for frequently-encountered installation issues.
|
||||||
|
|
||||||
## Main Application
|
## Installation options
|
||||||
|
|
||||||
1. [Automated Installer](010_INSTALL_AUTOMATED.md)
|
1. [Automated Installer](010_INSTALL_AUTOMATED.md)
|
||||||
|
|
||||||
@ -24,6 +24,9 @@ install guide for frequently-encountered installation issues.
|
|||||||
"developer console" which will help us debug problems with you and
|
"developer console" which will help us debug problems with you and
|
||||||
give you to access experimental features.
|
give you to access experimental features.
|
||||||
|
|
||||||
|
|
||||||
|
✅ This is the recommended option for first time users.
|
||||||
|
|
||||||
2. [Manual Installation](020_INSTALL_MANUAL.md)
|
2. [Manual Installation](020_INSTALL_MANUAL.md)
|
||||||
|
|
||||||
In this method you will manually run the commands needed to install
|
In this method you will manually run the commands needed to install
|
||||||
|
@ -1,15 +1,34 @@
|
|||||||
# Community Nodes
|
# Community Nodes
|
||||||
|
|
||||||
These are nodes that have been developed by the community for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md).
|
These are nodes that have been developed by the community, for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md).
|
||||||
|
|
||||||
If you'd like to submit a node for the community, please refer to the [node creation overview](overview.md).
|
If you'd like to submit a node for the community, please refer to the [node creation overview](./overview.md#contributing-nodes).
|
||||||
|
|
||||||
To download a node, simply download the `.py` node file from the link and add it to the `invokeai/app/invocations/` folder in your Invoke AI install location. Along with the node, an example node graph should be provided to help you get started with the node.
|
To download a node, simply download the `.py` node file from the link and add it to the `invokeai/app/invocations/` folder in your Invoke AI install location. Along with the node, an example node graph should be provided to help you get started with the node.
|
||||||
|
|
||||||
To use a community node graph, download the the `.json` node graph file and load it into Invoke AI via the **Load Nodes** button on the Node Editor.
|
To use a community node graph, download the the `.json` node graph file and load it into Invoke AI via the **Load Nodes** button on the Node Editor.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
The nodes linked below have been developed and contributed by members of the Invoke AI community. While we strive to ensure the quality and safety of these contributions, we do not guarantee the reliability or security of the nodes. If you have issues or concerns with any of the nodes below, please raise it on GitHub or in the Discord.
|
||||||
|
|
||||||
## List of Nodes
|
## List of Nodes
|
||||||
|
|
||||||
|
### Face Mask
|
||||||
|
|
||||||
|
**Description:** This node autodetects a face in the image using MediaPipe and masks it by making it transparent. Via outpainting you can swap faces with other faces, or invert the mask and swap things around the face with other things. Additionally, you can supply X and Y offset values to scale/change the shape of the mask for finer control. The node also outputs an all-white mask in the same dimensions as the input image. This is needed by the inpaint node (and unified canvas) for outpainting.
|
||||||
|
|
||||||
|
**Node Link:** https://github.com/ymgenesis/InvokeAI/blob/facemaskmediapipe/invokeai/app/invocations/facemask.py
|
||||||
|
|
||||||
|
**Example Node Graph:** https://www.mediafire.com/file/gohn5sb1bfp8use/21-July_2023-FaceMask.json/file
|
||||||
|
|
||||||
|
**Output Examples**
|
||||||
|
|
||||||
|
![2e3168cb-af6a-475d-bfac-c7b7fd67b4c2](https://github.com/ymgenesis/InvokeAI/assets/25252829/a5ad7d44-2ada-4b3c-a56e-a21f8244a1ac)
|
||||||
|
![2_annotated](https://github.com/ymgenesis/InvokeAI/assets/25252829/53416c8a-a23b-4d76-bb6d-3cfd776e0096)
|
||||||
|
![2fe2150c-fd08-4e26-8c36-f0610bf441bb](https://github.com/ymgenesis/InvokeAI/assets/25252829/b0f7ecfe-f093-4147-a904-b9f131b41dc9)
|
||||||
|
![831b6b98-4f0f-4360-93c8-69a9c1338cbe](https://github.com/ymgenesis/InvokeAI/assets/25252829/fc7b0622-e361-4155-8a76-082894d084f0)
|
||||||
|
|
||||||
--------------------------------
|
--------------------------------
|
||||||
### Super Cool Node Template
|
### Super Cool Node Template
|
||||||
|
|
||||||
@ -23,6 +42,11 @@ To use a community node graph, download the the `.json` node graph file and load
|
|||||||
|
|
||||||
![Invoke AI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png)
|
![Invoke AI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png)
|
||||||
|
|
||||||
|
### Ideal Size
|
||||||
|
|
||||||
|
**Description:** This node calculates an ideal image size for a first pass of a multi-pass upscaling. The aim is to avoid duplication that results from choosing a size larger than the model is capable of.
|
||||||
|
|
||||||
|
**Node Link:** https://github.com/JPPhoto/ideal-size-node
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).
|
If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# Nodes
|
# Nodes
|
||||||
|
|
||||||
## What are Nodes?
|
## What are Nodes?
|
||||||
An Node is simply a single operation that takes in some inputs and gives
|
An Node is simply a single operation that takes in some inputs and gives
|
||||||
out some outputs. We can then chain multiple nodes together to create more
|
out some outputs. We can then chain multiple nodes together to create more
|
||||||
@ -10,7 +11,7 @@ You can read more about nodes and the node editor [here](../features/NODES.md).
|
|||||||
|
|
||||||
|
|
||||||
## Downloading Nodes
|
## Downloading Nodes
|
||||||
To download a new node, visit our list of [Community Nodes](communityNodes.md). These are codes that have been created by the community, for the community.
|
To download a new node, visit our list of [Community Nodes](communityNodes.md). These are nodes that have been created by the community, for the community.
|
||||||
|
|
||||||
|
|
||||||
## Contributing Nodes
|
## Contributing Nodes
|
||||||
@ -18,10 +19,10 @@ To download a new node, visit our list of [Community Nodes](communityNodes.md).
|
|||||||
To learn about creating a new node, please visit our [Node creation documenation](../contributing/INVOCATIONS.md).
|
To learn about creating a new node, please visit our [Node creation documenation](../contributing/INVOCATIONS.md).
|
||||||
|
|
||||||
Once you’ve created a node and confirmed that it behaves as expected locally, follow these steps:
|
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
|
* Make sure the node is contained in a new Python (.py) file
|
||||||
- Submit a pull request with a link to your node in GitHub against the `nodes` branch to add the node to the [Community Nodes](Community Nodes) list
|
* Submit a pull request with a link to your node in GitHub against the `nodes` branch to add the node to the [Community Nodes](Community Nodes) list
|
||||||
- Make sure you are following the template below and have provided all relevant details about the node and what it does.
|
* Make sure you are following the template below and have provided all relevant details about the node and what it does.
|
||||||
- A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you might be asked for permission to include it in the core project.
|
* A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you might be asked for permission to include it in the core project.
|
||||||
|
|
||||||
### Community Node Template
|
### Community Node Template
|
||||||
|
|
||||||
|
@ -17,67 +17,267 @@ We thank them for all of their time and hard work.
|
|||||||
|
|
||||||
* @lstein (Lincoln Stein) - Co-maintainer
|
* @lstein (Lincoln Stein) - Co-maintainer
|
||||||
* @blessedcoolant - Co-maintainer
|
* @blessedcoolant - Co-maintainer
|
||||||
* @hipsterusername (Kent Keirsey) - Product Manager
|
* @hipsterusername (Kent Keirsey) - Co-maintainer, CEO, Positive Vibes
|
||||||
* @psychedelicious - Web Team Leader
|
* @psychedelicious (Spencer Mabrito) - Web Team Leader
|
||||||
* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard
|
* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard
|
||||||
* @damian0815 - Attention Systems and Gameplay Engineer
|
* @damian0815 - Attention Systems and Compel Maintainer
|
||||||
* @mauwii (Matthias Wild) - Continuous integration and product maintenance engineer
|
|
||||||
* @Netsvetaev (Artur Netsvetaev) - UI/UX Developer
|
|
||||||
* @tildebyte - General gadfly and resident (self-appointed) know-it-all
|
|
||||||
* @keturn - Lead for Diffusers port
|
|
||||||
* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
|
* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
|
||||||
* @jpphoto (Jonathan Pollack) - Inference and rendering engine optimization
|
* @genomancer (Gregg Helt) - Controlnet support
|
||||||
* @genomancer (Gregg Helt) - Model training and merging
|
* @StAlKeR7779 (Sergey Borisov) - Torch stack, ONNX, model management, optimization
|
||||||
|
* @cheerio (Mary Rogers) - Lead Engineer & Web App Development
|
||||||
|
* @brandon (Brandon Rising) - Platform, Infrastructure, Backend Systems
|
||||||
|
* @ryanjdick (Ryan Dick) - Machine Learning & Training
|
||||||
|
* @millu (Millun Atluri) - Community Manager, Documentation, Node-wrangler
|
||||||
|
* @chainchompa (Jennifer Player) - Web Development & Chain-Chomping
|
||||||
|
* @keturn (Kevin Turner) - Diffusers
|
||||||
|
* @gogurt enjoyer - Discord moderator and end user support
|
||||||
|
* @whosawhatsis - Discord moderator and end user support
|
||||||
|
* @dwinrger - Discord moderator and end user support
|
||||||
|
* @526christian - Discord moderator and end user support
|
||||||
|
|
||||||
## **Contributions by**
|
## **Full List of Contributors by Commit Name**
|
||||||
|
|
||||||
- [Sean McLellan](https://github.com/Oceanswave)
|
- AbdBarho
|
||||||
- [Kevin Gibbons](https://github.com/bakkot)
|
- ablattmann
|
||||||
- [Tesseract Cat](https://github.com/TesseractCat)
|
- AdamOStark
|
||||||
- [blessedcoolant](https://github.com/blessedcoolant)
|
- Adam Rice
|
||||||
- [David Ford](https://github.com/david-ford)
|
- Airton Silva
|
||||||
- [yunsaki](https://github.com/yunsaki)
|
- Alexander Eichhorn
|
||||||
- [James Reynolds](https://github.com/magnusviri)
|
- Alexandre D. Roberge
|
||||||
- [David Wager](https://github.com/maddavid123)
|
- Andreas Rozek
|
||||||
- [Jason Toffaletti](https://github.com/toffaletti)
|
- Andre LaBranche
|
||||||
- [tildebyte](https://github.com/tildebyte)
|
- Andy Bearman
|
||||||
- [Cragin Godley](https://github.com/cgodley)
|
- Andy Luhrs
|
||||||
- [BlueAmulet](https://github.com/BlueAmulet)
|
- Andy Pilate
|
||||||
- [Benjamin Warner](https://github.com/warner-benjamin)
|
- Any-Winter-4079
|
||||||
- [Cora Johnson-Roberson](https://github.com/corajr)
|
- apolinario
|
||||||
- [veprogames](https://github.com/veprogames)
|
- ArDiouscuros
|
||||||
- [JigenD](https://github.com/JigenD)
|
- Armando C. Santisbon
|
||||||
- [Niek van der Maas](https://github.com/Niek)
|
- Arthur Holstvoogd
|
||||||
- [Henry van Megen](https://github.com/hvanmegen)
|
- artmen1516
|
||||||
- [Håvard Gulldahl](https://github.com/havardgulldahl)
|
- Artur
|
||||||
- [greentext2](https://github.com/greentext2)
|
- Arturo Mendivil
|
||||||
- [Simon Vans-Colina](https://github.com/simonvc)
|
- Ben Alkov
|
||||||
- [Gabriel Rotbart](https://github.com/gabrielrotbart)
|
- Benjamin Warner
|
||||||
- [Eric Khun](https://github.com/erickhun)
|
- Bernard Maltais
|
||||||
- [Brent Ozar](https://github.com/BrentOzar)
|
- blessedcoolant
|
||||||
- [nderscore](https://github.com/nderscore)
|
- blhook
|
||||||
- [Mikhail Tishin](https://github.com/tishin)
|
- BlueAmulet
|
||||||
- [Tom Elovi Spruce](https://github.com/ilovecomputers)
|
- Bouncyknighter
|
||||||
- [spezialspezial](https://github.com/spezialspezial)
|
- Brandon Rising
|
||||||
- [Yosuke Shinya](https://github.com/shinya7y)
|
- Brent Ozar
|
||||||
- [Andy Pilate](https://github.com/Cubox)
|
- Brian Racer
|
||||||
- [Muhammad Usama](https://github.com/SMUsamaShah)
|
- bsilvereagle
|
||||||
- [Arturo Mendivil](https://github.com/artmen1516)
|
- c67e708d
|
||||||
- [Paul Sajna](https://github.com/sajattack)
|
- CapableWeb
|
||||||
- [Samuel Husso](https://github.com/shusso)
|
- Carson Katri
|
||||||
- [nicolai256](https://github.com/nicolai256)
|
- Chloe
|
||||||
- [Mihai](https://github.com/mh-dm)
|
- Chris Dawson
|
||||||
- [Any Winter](https://github.com/any-winter-4079)
|
- Chris Hayes
|
||||||
- [Doggettx](https://github.com/doggettx)
|
- Chris Jones
|
||||||
- [Matthias Wild](https://github.com/mauwii)
|
- chromaticist
|
||||||
- [Kyle Schouviller](https://github.com/kyle0654)
|
- Claus F. Strasburger
|
||||||
- [rabidcopy](https://github.com/rabidcopy)
|
- cmdr2
|
||||||
- [Dominic Letz](https://github.com/dominicletz)
|
- cody
|
||||||
- [Dmitry T.](https://github.com/ArDiouscuros)
|
- Conor Reid
|
||||||
- [Kent Keirsey](https://github.com/hipsterusername)
|
- Cora Johnson-Roberson
|
||||||
- [psychedelicious](https://github.com/psychedelicious)
|
- coreco
|
||||||
- [damian0815](https://github.com/damian0815)
|
- cosmii02
|
||||||
- [Eugene Brodsky](https://github.com/ebr)
|
- cpacker
|
||||||
|
- Cragin Godley
|
||||||
|
- creachec
|
||||||
|
- Damian Stewart
|
||||||
|
- Daniel Manzke
|
||||||
|
- Danny Beer
|
||||||
|
- Dan Sully
|
||||||
|
- David Burnett
|
||||||
|
- David Ford
|
||||||
|
- David Regla
|
||||||
|
- David Wager
|
||||||
|
- Daya Adianto
|
||||||
|
- db3000
|
||||||
|
- Denis Olshin
|
||||||
|
- Dennis
|
||||||
|
- Dominic Letz
|
||||||
|
- DrGunnarMallon
|
||||||
|
- Edward Johan
|
||||||
|
- elliotsayes
|
||||||
|
- Elrik
|
||||||
|
- ElrikUnderlake
|
||||||
|
- Eric Khun
|
||||||
|
- Eric Wolf
|
||||||
|
- Eugene Brodsky
|
||||||
|
- ExperimentalCyborg
|
||||||
|
- Fabian Bahl
|
||||||
|
- Fabio 'MrWHO' Torchetti
|
||||||
|
- fattire
|
||||||
|
- Felipe Nogueira
|
||||||
|
- Félix Sanz
|
||||||
|
- figgefigge
|
||||||
|
- Gabriel Mackievicz Telles
|
||||||
|
- gabrielrotbart
|
||||||
|
- gallegonovato
|
||||||
|
- Gérald LONLAS
|
||||||
|
- GitHub Actions Bot
|
||||||
|
- gogurtenjoyer
|
||||||
|
- greentext2
|
||||||
|
- Gregg Helt
|
||||||
|
- H4rk
|
||||||
|
- Håvard Gulldahl
|
||||||
|
- henry
|
||||||
|
- Henry van Megen
|
||||||
|
- hipsterusername
|
||||||
|
- hj
|
||||||
|
- Hosted Weblate
|
||||||
|
- Iman Karim
|
||||||
|
- ismail ihsan bülbül
|
||||||
|
- Ivan Efimov
|
||||||
|
- jakehl
|
||||||
|
- Jakub Kolčář
|
||||||
|
- JamDon2
|
||||||
|
- James Reynolds
|
||||||
|
- Jan Skurovec
|
||||||
|
- Jari Vetoniemi
|
||||||
|
- Jason Toffaletti
|
||||||
|
- Jaulustus
|
||||||
|
- Jeff Mahoney
|
||||||
|
- jeremy
|
||||||
|
- Jeremy Clark
|
||||||
|
- JigenD
|
||||||
|
- Jim Hays
|
||||||
|
- Johan Roxendal
|
||||||
|
- Johnathon Selstad
|
||||||
|
- Jonathan
|
||||||
|
- Joseph Dries III
|
||||||
|
- JPPhoto
|
||||||
|
- jspraul
|
||||||
|
- Justin Wong
|
||||||
|
- Juuso V
|
||||||
|
- Kaspar Emanuel
|
||||||
|
- Katsuyuki-Karasawa
|
||||||
|
- Kent Keirsey
|
||||||
|
- Kevin Coakley
|
||||||
|
- Kevin Gibbons
|
||||||
|
- Kevin Schaul
|
||||||
|
- Kevin Turner
|
||||||
|
- krummrey
|
||||||
|
- Kyle Lacy
|
||||||
|
- Kyle Schouviller
|
||||||
|
- Lawrence Norton
|
||||||
|
- LemonDouble
|
||||||
|
- Leo Pasanen
|
||||||
|
- Lincoln Stein
|
||||||
|
- LoganPederson
|
||||||
|
- Lynne Whitehorn
|
||||||
|
- majick
|
||||||
|
- Marco Labarile
|
||||||
|
- Martin Kristiansen
|
||||||
|
- Mary Hipp Rogers
|
||||||
|
- mastercaster9000
|
||||||
|
- Matthias Wild
|
||||||
|
- michaelk71
|
||||||
|
- mickr777
|
||||||
|
- Mihai
|
||||||
|
- Mihail Dumitrescu
|
||||||
|
- Mikhail Tishin
|
||||||
|
- Millun Atluri
|
||||||
|
- Minjune Song
|
||||||
|
- mitien
|
||||||
|
- mofuzz
|
||||||
|
- Muhammad Usama
|
||||||
|
- Name
|
||||||
|
- _nderscore
|
||||||
|
- Netzer R
|
||||||
|
- Nicholas Koh
|
||||||
|
- Nicholas Körfer
|
||||||
|
- nicolai256
|
||||||
|
- Niek van der Maas
|
||||||
|
- noodlebox
|
||||||
|
- Nuno Coração
|
||||||
|
- ofirkris
|
||||||
|
- Olivier Louvignes
|
||||||
|
- owenvincent
|
||||||
|
- Patrick Esser
|
||||||
|
- Patrick Tien
|
||||||
|
- Patrick von Platen
|
||||||
|
- Paul Sajna
|
||||||
|
- pejotr
|
||||||
|
- Peter Baylies
|
||||||
|
- Peter Lin
|
||||||
|
- plucked
|
||||||
|
- prixt
|
||||||
|
- psychedelicious
|
||||||
|
- Rainer Bernhardt
|
||||||
|
- Riccardo Giovanetti
|
||||||
|
- Rich Jones
|
||||||
|
- rmagur1203
|
||||||
|
- Rob Baines
|
||||||
|
- Robert Bolender
|
||||||
|
- Robin Rombach
|
||||||
|
- Rohan Barar
|
||||||
|
- rpagliuca
|
||||||
|
- rromb
|
||||||
|
- Rupesh Sreeraman
|
||||||
|
- Ryan Cao
|
||||||
|
- Saifeddine
|
||||||
|
- Saifeddine ALOUI
|
||||||
|
- SammCheese
|
||||||
|
- Sammy
|
||||||
|
- sammyf
|
||||||
|
- Samuel Husso
|
||||||
|
- Scott Lahteine
|
||||||
|
- Sean McLellan
|
||||||
|
- Sebastian Aigner
|
||||||
|
- Sergey Borisov
|
||||||
|
- Sergey Krashevich
|
||||||
|
- Shapor Naghibzadeh
|
||||||
|
- Shawn Zhong
|
||||||
|
- Simon Vans-Colina
|
||||||
|
- skunkworxdark
|
||||||
|
- slashtechno
|
||||||
|
- spezialspezial
|
||||||
|
- ssantos
|
||||||
|
- StAlKeR7779
|
||||||
|
- Stephan Koglin-Fischer
|
||||||
|
- SteveCaruso
|
||||||
|
- Steve Martinelli
|
||||||
|
- Steven Frank
|
||||||
|
- System X - Files
|
||||||
|
- Taylor Kems
|
||||||
|
- techicode
|
||||||
|
- techybrain-dev
|
||||||
|
- tesseractcat
|
||||||
|
- thealanle
|
||||||
|
- Thomas
|
||||||
|
- tildebyte
|
||||||
|
- Tim Cabbage
|
||||||
|
- Tom
|
||||||
|
- Tom Elovi Spruce
|
||||||
|
- Tom Gouville
|
||||||
|
- tomosuto
|
||||||
|
- Travco
|
||||||
|
- Travis Palmer
|
||||||
|
- tyler
|
||||||
|
- unknown
|
||||||
|
- user1
|
||||||
|
- Vedant Madane
|
||||||
|
- veprogames
|
||||||
|
- wa.code
|
||||||
|
- wfng92
|
||||||
|
- whosawhatsis
|
||||||
|
- Will
|
||||||
|
- William Becher
|
||||||
|
- William Chong
|
||||||
|
- xra
|
||||||
|
- Yeung Yiu Hung
|
||||||
|
- ymgenesis
|
||||||
|
- Yorzaren
|
||||||
|
- Yosuke Shinya
|
||||||
|
- yun saki
|
||||||
|
- Zadagu
|
||||||
|
- zeptofine
|
||||||
|
- 冯不游
|
||||||
|
- 唐澤 克幸
|
||||||
|
|
||||||
## **Original CompVis Authors**
|
## **Original CompVis Authors**
|
||||||
|
|
||||||
|
@ -58,7 +58,8 @@ class ApiDependencies:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger = logger):
|
def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger = logger):
|
||||||
logger.debug(f"InvokeAI version {__version__}")
|
logger.info(f"InvokeAI version {__version__}")
|
||||||
|
logger.info(f"Root directory = {str(config.root_path)}")
|
||||||
logger.debug(f"Internet connectivity is {config.internet_available}")
|
logger.debug(f"Internet connectivity is {config.internet_available}")
|
||||||
|
|
||||||
events = FastAPIEventService(event_handler_id)
|
events = FastAPIEventService(event_handler_id)
|
||||||
|
@ -40,9 +40,15 @@ async def upload_image(
|
|||||||
response: Response,
|
response: Response,
|
||||||
image_category: ImageCategory = Query(description="The category of the image"),
|
image_category: ImageCategory = Query(description="The category of the image"),
|
||||||
is_intermediate: bool = Query(description="Whether this is an intermediate image"),
|
is_intermediate: bool = Query(description="Whether this is an intermediate image"),
|
||||||
|
board_id: Optional[str] = Query(
|
||||||
|
default=None, description="The board to add this image to, if any"
|
||||||
|
),
|
||||||
session_id: Optional[str] = Query(
|
session_id: Optional[str] = Query(
|
||||||
default=None, description="The session ID associated with this upload, if any"
|
default=None, description="The session ID associated with this upload, if any"
|
||||||
),
|
),
|
||||||
|
crop_visible: Optional[bool] = Query(
|
||||||
|
default=False, description="Whether to crop the image"
|
||||||
|
),
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
"""Uploads an image"""
|
"""Uploads an image"""
|
||||||
if not file.content_type.startswith("image"):
|
if not file.content_type.startswith("image"):
|
||||||
@ -52,6 +58,9 @@ async def upload_image(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
pil_image = Image.open(io.BytesIO(contents))
|
pil_image = Image.open(io.BytesIO(contents))
|
||||||
|
if crop_visible:
|
||||||
|
bbox = pil_image.getbbox()
|
||||||
|
pil_image = pil_image.crop(bbox)
|
||||||
except:
|
except:
|
||||||
# Error opening the image
|
# Error opening the image
|
||||||
raise HTTPException(status_code=415, detail="Failed to read image")
|
raise HTTPException(status_code=415, detail="Failed to read image")
|
||||||
@ -62,6 +71,7 @@ async def upload_image(
|
|||||||
image_origin=ResourceOrigin.EXTERNAL,
|
image_origin=ResourceOrigin.EXTERNAL,
|
||||||
image_category=image_category,
|
image_category=image_category,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
|
board_id=board_id,
|
||||||
is_intermediate=is_intermediate,
|
is_intermediate=is_intermediate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -374,16 +374,16 @@ setting environment variables INVOKEAI_<setting>.
|
|||||||
max_cache_size : float = Field(default=6.0, gt=0, description="Maximum memory amount used by model cache for rapid switching", category='Memory/Performance')
|
max_cache_size : float = Field(default=6.0, gt=0, description="Maximum memory amount used by model cache for rapid switching", category='Memory/Performance')
|
||||||
max_vram_cache_size : float = Field(default=2.75, ge=0, description="Amount of VRAM reserved for model storage", category='Memory/Performance')
|
max_vram_cache_size : float = Field(default=2.75, ge=0, description="Amount of VRAM reserved for model storage", category='Memory/Performance')
|
||||||
gpu_mem_reserved : float = Field(default=2.75, ge=0, description="DEPRECATED: use max_vram_cache_size. Amount of VRAM reserved for model storage", category='DEPRECATED')
|
gpu_mem_reserved : float = Field(default=2.75, ge=0, description="DEPRECATED: use max_vram_cache_size. Amount of VRAM reserved for model storage", category='DEPRECATED')
|
||||||
precision : Literal[tuple(['auto','float16','float32','autocast'])] = Field(default='float16',description='Floating point precision', category='Memory/Performance')
|
precision : Literal[tuple(['auto','float16','float32','autocast'])] = Field(default='auto',description='Floating point precision', category='Memory/Performance')
|
||||||
sequential_guidance : bool = Field(default=False, description="Whether to calculate guidance in serial instead of in parallel, lowering memory requirements", category='Memory/Performance')
|
sequential_guidance : bool = Field(default=False, description="Whether to calculate guidance in serial instead of in parallel, lowering memory requirements", category='Memory/Performance')
|
||||||
xformers_enabled : bool = Field(default=True, description="Enable/disable memory-efficient attention", category='Memory/Performance')
|
xformers_enabled : bool = Field(default=True, description="Enable/disable memory-efficient attention", category='Memory/Performance')
|
||||||
tiled_decode : bool = Field(default=False, description="Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty)", category='Memory/Performance')
|
tiled_decode : bool = Field(default=False, description="Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty)", category='Memory/Performance')
|
||||||
|
|
||||||
root : Path = Field(default=_find_root(), description='InvokeAI runtime root directory', category='Paths')
|
root : Path = Field(default=_find_root(), description='InvokeAI runtime root directory', category='Paths')
|
||||||
autoimport_dir : Path = Field(default='autoimport/main', description='Path to a directory of models files to be imported on startup.', category='Paths')
|
autoimport_dir : Path = Field(default='autoimport', description='Path to a directory of models files to be imported on startup.', category='Paths')
|
||||||
lora_dir : Path = Field(default='autoimport/lora', description='Path to a directory of LoRA/LyCORIS models to be imported on startup.', category='Paths')
|
lora_dir : Path = Field(default=None, description='Path to a directory of LoRA/LyCORIS models to be imported on startup.', category='Paths')
|
||||||
embedding_dir : Path = Field(default='autoimport/embedding', description='Path to a directory of Textual Inversion embeddings to be imported on startup.', category='Paths')
|
embedding_dir : Path = Field(default=None, description='Path to a directory of Textual Inversion embeddings to be imported on startup.', category='Paths')
|
||||||
controlnet_dir : Path = Field(default='autoimport/controlnet', description='Path to a directory of ControlNet embeddings to be imported on startup.', category='Paths')
|
controlnet_dir : Path = Field(default=None, description='Path to a directory of ControlNet embeddings to be imported on startup.', category='Paths')
|
||||||
conf_path : Path = Field(default='configs/models.yaml', description='Path to models definition file', category='Paths')
|
conf_path : Path = Field(default='configs/models.yaml', description='Path to models definition file', category='Paths')
|
||||||
models_dir : Path = Field(default='models', description='Path to the models directory', category='Paths')
|
models_dir : Path = Field(default='models', description='Path to the models directory', category='Paths')
|
||||||
legacy_conf_dir : Path = Field(default='configs/stable-diffusion', description='Path to directory of legacy checkpoint config files', category='Paths')
|
legacy_conf_dir : Path = Field(default='configs/stable-diffusion', description='Path to directory of legacy checkpoint config files', category='Paths')
|
||||||
@ -397,7 +397,7 @@ setting environment variables INVOKEAI_<setting>.
|
|||||||
log_handlers : List[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>"', category="Logging")
|
log_handlers : List[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>"', category="Logging")
|
||||||
# note - would be better to read the log_format values from logging.py, but this creates circular dependencies issues
|
# note - would be better to read the log_format values from logging.py, but this creates circular dependencies issues
|
||||||
log_format : Literal[tuple(['plain','color','syslog','legacy'])] = Field(default="color", description='Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style', category="Logging")
|
log_format : Literal[tuple(['plain','color','syslog','legacy'])] = Field(default="color", description='Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style', category="Logging")
|
||||||
log_level : Literal[tuple(["debug","info","warning","error","critical"])] = Field(default="debug", description="Emit logging messages at this level or higher", category="Logging")
|
log_level : Literal[tuple(["debug","info","warning","error","critical"])] = Field(default="info", description="Emit logging messages at this level or higher", category="Logging")
|
||||||
|
|
||||||
version : bool = Field(default=False, description="Show InvokeAI version and exit", category="Other")
|
version : bool = Field(default=False, description="Show InvokeAI version and exit", category="Other")
|
||||||
#fmt: on
|
#fmt: on
|
||||||
|
@ -52,6 +52,7 @@ class ImageServiceABC(ABC):
|
|||||||
image_category: ImageCategory,
|
image_category: ImageCategory,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
|
board_id: Optional[str] = None,
|
||||||
is_intermediate: bool = False,
|
is_intermediate: bool = False,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
@ -174,6 +175,7 @@ class ImageService(ImageServiceABC):
|
|||||||
image_category: ImageCategory,
|
image_category: ImageCategory,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
|
board_id: Optional[str] = None,
|
||||||
is_intermediate: bool = False,
|
is_intermediate: bool = False,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
@ -215,6 +217,11 @@ class ImageService(ImageServiceABC):
|
|||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if board_id is not None:
|
||||||
|
self._services.board_image_records.add_image_to_board(
|
||||||
|
board_id=board_id, image_name=image_name
|
||||||
|
)
|
||||||
|
|
||||||
self._services.image_files.save(
|
self._services.image_files.save(
|
||||||
image_name=image_name, image=image, metadata=metadata, graph=graph
|
image_name=image_name, image=image, metadata=metadata, graph=graph
|
||||||
)
|
)
|
||||||
|
@ -299,10 +299,11 @@ class ModelManagerService(ModelManagerServiceBase):
|
|||||||
else:
|
else:
|
||||||
config_file = config.root_dir / "configs/models.yaml"
|
config_file = config.root_dir / "configs/models.yaml"
|
||||||
|
|
||||||
logger.debug(f'config file={config_file}')
|
logger.debug(f'Config file={config_file}')
|
||||||
|
|
||||||
device = torch.device(choose_torch_device())
|
device = torch.device(choose_torch_device())
|
||||||
logger.debug(f'GPU device = {device}')
|
device_name = torch.cuda.get_device_name() if device==torch.device('cuda') else ''
|
||||||
|
logger.info(f'GPU device = {device} {device_name}')
|
||||||
|
|
||||||
precision = config.precision
|
precision = config.precision
|
||||||
if precision == "auto":
|
if precision == "auto":
|
||||||
|
@ -23,6 +23,7 @@ from urllib import request
|
|||||||
|
|
||||||
import npyscreen
|
import npyscreen
|
||||||
import transformers
|
import transformers
|
||||||
|
import omegaconf
|
||||||
from diffusers import AutoencoderKL
|
from diffusers import AutoencoderKL
|
||||||
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
|
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
|
||||||
from huggingface_hub import HfFolder
|
from huggingface_hub import HfFolder
|
||||||
@ -44,6 +45,7 @@ from invokeai.backend.util.logging import InvokeAILogger
|
|||||||
from invokeai.frontend.install.model_install import addModelsForm, process_and_execute
|
from invokeai.frontend.install.model_install import addModelsForm, process_and_execute
|
||||||
from invokeai.frontend.install.widgets import (
|
from invokeai.frontend.install.widgets import (
|
||||||
CenteredButtonPress,
|
CenteredButtonPress,
|
||||||
|
FileBox,
|
||||||
IntTitleSlider,
|
IntTitleSlider,
|
||||||
set_min_terminal_size,
|
set_min_terminal_size,
|
||||||
CyclingForm,
|
CyclingForm,
|
||||||
@ -409,21 +411,21 @@ Use cursor arrows to make a checkbox selection, and space to toggle.
|
|||||||
self.nextrely += 1
|
self.nextrely += 1
|
||||||
self.add_widget_intelligent(
|
self.add_widget_intelligent(
|
||||||
npyscreen.FixedText,
|
npyscreen.FixedText,
|
||||||
value="Directories containing textual inversion, controlnet and LoRA models (<tab> autocompletes, ctrl-N advances):",
|
value="Folder to recursively scan for new checkpoints, ControlNets, LoRAs and TI models (<tab> autocompletes, ctrl-N advances):",
|
||||||
editable=False,
|
editable=False,
|
||||||
color="CONTROL",
|
color="CONTROL",
|
||||||
)
|
)
|
||||||
self.autoimport_dirs = {}
|
self.autoimport_dirs = {}
|
||||||
for description, config_name, path in autoimport_paths(old_opts):
|
self.autoimport_dirs['autoimport_dir'] = self.add_widget_intelligent(
|
||||||
self.autoimport_dirs[config_name] = self.add_widget_intelligent(
|
FileBox,
|
||||||
npyscreen.TitleFilename,
|
name=f'Autoimport Folder',
|
||||||
name=description+':',
|
value=str(config.root_path / config.autoimport_dir),
|
||||||
value=str(path),
|
|
||||||
select_dir=True,
|
select_dir=True,
|
||||||
must_exist=False,
|
must_exist=False,
|
||||||
use_two_lines=False,
|
use_two_lines=False,
|
||||||
labelColor="GOOD",
|
labelColor="GOOD",
|
||||||
begin_entry_at=32,
|
begin_entry_at=32,
|
||||||
|
max_height = 3,
|
||||||
scroll_exit=True
|
scroll_exit=True
|
||||||
)
|
)
|
||||||
self.nextrely += 1
|
self.nextrely += 1
|
||||||
@ -567,7 +569,14 @@ def default_startup_options(init_file: Path) -> Namespace:
|
|||||||
return opts
|
return opts
|
||||||
|
|
||||||
def default_user_selections(program_opts: Namespace) -> InstallSelections:
|
def default_user_selections(program_opts: Namespace) -> InstallSelections:
|
||||||
installer = ModelInstall(config)
|
|
||||||
|
try:
|
||||||
|
installer = ModelInstall(config)
|
||||||
|
except omegaconf.errors.ConfigKeyError:
|
||||||
|
logger.warning('Your models.yaml file is corrupt or out of date. Reinitializing')
|
||||||
|
initialize_rootdir(config.root_path, True)
|
||||||
|
installer = ModelInstall(config)
|
||||||
|
|
||||||
models = installer.all_models()
|
models = installer.all_models()
|
||||||
return InstallSelections(
|
return InstallSelections(
|
||||||
install_models=[models[installer.default_model()].path or models[installer.default_model()].repo_id]
|
install_models=[models[installer.default_model()].path or models[installer.default_model()].repo_id]
|
||||||
@ -575,19 +584,8 @@ def default_user_selections(program_opts: Namespace) -> InstallSelections:
|
|||||||
else [models[x].path or models[x].repo_id for x in installer.recommended_models()]
|
else [models[x].path or models[x].repo_id for x in installer.recommended_models()]
|
||||||
if program_opts.yes_to_all
|
if program_opts.yes_to_all
|
||||||
else list(),
|
else list(),
|
||||||
# scan_directory=None,
|
|
||||||
# autoscan_on_startup=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# -------------------------------------
|
|
||||||
def autoimport_paths(config: InvokeAIAppConfig):
|
|
||||||
return [
|
|
||||||
('Checkpoints & diffusers models', 'autoimport_dir', config.root_path / config.autoimport_dir),
|
|
||||||
('LoRA/LyCORIS models', 'lora_dir', config.root_path / config.lora_dir),
|
|
||||||
('Controlnet models', 'controlnet_dir', config.root_path / config.controlnet_dir),
|
|
||||||
('Textual Inversion Embeddings', 'embedding_dir', config.root_path / config.embedding_dir),
|
|
||||||
]
|
|
||||||
|
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
def initialize_rootdir(root: Path, yes_to_all: bool = False):
|
def initialize_rootdir(root: Path, yes_to_all: bool = False):
|
||||||
logger.info("** INITIALIZING INVOKEAI RUNTIME DIRECTORY **")
|
logger.info("** INITIALIZING INVOKEAI RUNTIME DIRECTORY **")
|
||||||
@ -663,7 +661,7 @@ def write_opts(opts: Namespace, init_file: Path):
|
|||||||
with open(init_file,'w', encoding='utf-8') as file:
|
with open(init_file,'w', encoding='utf-8') as file:
|
||||||
file.write(new_config.to_yaml())
|
file.write(new_config.to_yaml())
|
||||||
|
|
||||||
if opts.hf_token:
|
if hasattr(opts,'hf_token') and opts.hf_token:
|
||||||
HfLogin(opts.hf_token)
|
HfLogin(opts.hf_token)
|
||||||
|
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
|
@ -3,6 +3,6 @@ Initialization file for invokeai.backend.model_management
|
|||||||
"""
|
"""
|
||||||
from .model_manager import ModelManager, ModelInfo, AddModelResult, SchedulerPredictionType
|
from .model_manager import ModelManager, ModelInfo, AddModelResult, SchedulerPredictionType
|
||||||
from .model_cache import ModelCache
|
from .model_cache import ModelCache
|
||||||
from .models import BaseModelType, ModelType, SubModelType, ModelVariantType, ModelNotFoundException
|
from .models import BaseModelType, ModelType, SubModelType, ModelVariantType, ModelNotFoundException, DuplicateModelException
|
||||||
from .model_merge import ModelMerger, MergeInterpolationMethod
|
from .model_merge import ModelMerger, MergeInterpolationMethod
|
||||||
|
|
||||||
|
@ -251,7 +251,9 @@ from .model_search import ModelSearch
|
|||||||
from .models import (
|
from .models import (
|
||||||
BaseModelType, ModelType, SubModelType,
|
BaseModelType, ModelType, SubModelType,
|
||||||
ModelError, SchedulerPredictionType, MODEL_CLASSES,
|
ModelError, SchedulerPredictionType, MODEL_CLASSES,
|
||||||
ModelConfigBase, ModelNotFoundException, InvalidModelException,
|
ModelConfigBase,
|
||||||
|
ModelNotFoundException, InvalidModelException,
|
||||||
|
DuplicateModelException,
|
||||||
)
|
)
|
||||||
|
|
||||||
# We are only starting to number the config file with release 3.
|
# We are only starting to number the config file with release 3.
|
||||||
@ -858,7 +860,7 @@ class ModelManager(object):
|
|||||||
loaded_files = set()
|
loaded_files = set()
|
||||||
new_models_found = False
|
new_models_found = False
|
||||||
|
|
||||||
self.logger.info(f'scanning {self.app_config.models_path} for new models')
|
self.logger.info(f'Scanning {self.app_config.models_path} for new models')
|
||||||
with Chdir(self.app_config.root_path):
|
with Chdir(self.app_config.root_path):
|
||||||
for model_key, model_config in list(self.models.items()):
|
for model_key, model_config in list(self.models.items()):
|
||||||
model_name, cur_base_model, cur_model_type = self.parse_key(model_key)
|
model_name, cur_base_model, cur_model_type = self.parse_key(model_key)
|
||||||
@ -891,15 +893,18 @@ class ModelManager(object):
|
|||||||
model_name = model_path.name if model_path.is_dir() else model_path.stem
|
model_name = model_path.name if model_path.is_dir() else model_path.stem
|
||||||
model_key = self.create_key(model_name, cur_base_model, cur_model_type)
|
model_key = self.create_key(model_name, cur_base_model, cur_model_type)
|
||||||
|
|
||||||
if model_key in self.models:
|
|
||||||
raise Exception(f"Model with key {model_key} added twice")
|
|
||||||
|
|
||||||
if model_path.is_relative_to(self.app_config.root_path):
|
|
||||||
model_path = model_path.relative_to(self.app_config.root_path)
|
|
||||||
try:
|
try:
|
||||||
|
if model_key in self.models:
|
||||||
|
raise DuplicateModelException(f"Model with key {model_key} added twice")
|
||||||
|
|
||||||
|
if model_path.is_relative_to(self.app_config.root_path):
|
||||||
|
model_path = model_path.relative_to(self.app_config.root_path)
|
||||||
|
|
||||||
model_config: ModelConfigBase = model_class.probe_config(str(model_path))
|
model_config: ModelConfigBase = model_class.probe_config(str(model_path))
|
||||||
self.models[model_key] = model_config
|
self.models[model_key] = model_config
|
||||||
new_models_found = True
|
new_models_found = True
|
||||||
|
except DuplicateModelException as e:
|
||||||
|
self.logger.warning(e)
|
||||||
except InvalidModelException:
|
except InvalidModelException:
|
||||||
self.logger.warning(f"Not a valid model: {model_path}")
|
self.logger.warning(f"Not a valid model: {model_path}")
|
||||||
except NotImplementedError as e:
|
except NotImplementedError as e:
|
||||||
@ -956,7 +961,7 @@ class ModelManager(object):
|
|||||||
config.lora_dir,
|
config.lora_dir,
|
||||||
config.embedding_dir,
|
config.embedding_dir,
|
||||||
config.controlnet_dir,
|
config.controlnet_dir,
|
||||||
]
|
] if x
|
||||||
}
|
}
|
||||||
scanner = ScanAndImport(directories, self.logger, ignore=known_paths, installer=installer)
|
scanner = ScanAndImport(directories, self.logger, ignore=known_paths, installer=installer)
|
||||||
scanner.search()
|
scanner.search()
|
||||||
|
@ -2,7 +2,11 @@ import inspect
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Literal, get_origin
|
from typing import Literal, get_origin
|
||||||
from .base import BaseModelType, ModelType, SubModelType, ModelBase, ModelConfigBase, ModelVariantType, SchedulerPredictionType, ModelError, SilenceWarnings, ModelNotFoundException, InvalidModelException
|
from .base import (
|
||||||
|
BaseModelType, ModelType, SubModelType, ModelBase, ModelConfigBase,
|
||||||
|
ModelVariantType, SchedulerPredictionType, ModelError, SilenceWarnings,
|
||||||
|
ModelNotFoundException, InvalidModelException, DuplicateModelException
|
||||||
|
)
|
||||||
from .stable_diffusion import StableDiffusion1Model, StableDiffusion2Model
|
from .stable_diffusion import StableDiffusion1Model, StableDiffusion2Model
|
||||||
from .sdxl import StableDiffusionXLModel
|
from .sdxl import StableDiffusionXLModel
|
||||||
from .vae import VaeModel
|
from .vae import VaeModel
|
||||||
|
@ -15,6 +15,9 @@ from contextlib import suppress
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import List, Dict, Optional, Type, Literal, TypeVar, Generic, Callable, Any, Union
|
from typing import List, Dict, Optional, Type, Literal, TypeVar, Generic, Callable, Any, Union
|
||||||
|
|
||||||
|
class DuplicateModelException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class InvalidModelException(Exception):
|
class InvalidModelException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import math
|
||||||
import torch
|
import torch
|
||||||
|
import diffusers
|
||||||
|
|
||||||
|
|
||||||
if torch.backends.mps.is_available():
|
if torch.backends.mps.is_available():
|
||||||
@ -61,3 +63,150 @@ def new_torch_interpolate(input, size=None, scale_factor=None, mode='nearest', a
|
|||||||
return _torch_interpolate(input, size, scale_factor, mode, align_corners, recompute_scale_factor, antialias)
|
return _torch_interpolate(input, size, scale_factor, mode, align_corners, recompute_scale_factor, antialias)
|
||||||
|
|
||||||
torch.nn.functional.interpolate = new_torch_interpolate
|
torch.nn.functional.interpolate = new_torch_interpolate
|
||||||
|
|
||||||
|
# TODO: refactor it
|
||||||
|
_SlicedAttnProcessor = diffusers.models.attention_processor.SlicedAttnProcessor
|
||||||
|
class ChunkedSlicedAttnProcessor:
|
||||||
|
r"""
|
||||||
|
Processor for implementing sliced attention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slice_size (`int`, *optional*):
|
||||||
|
The number of steps to compute attention. Uses as many slices as `attention_head_dim // slice_size`, and
|
||||||
|
`attention_head_dim` must be a multiple of the `slice_size`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, slice_size):
|
||||||
|
assert isinstance(slice_size, int)
|
||||||
|
slice_size = 1 # TODO: maybe implement chunking in batches too when enough memory
|
||||||
|
self.slice_size = slice_size
|
||||||
|
self._sliced_attn_processor = _SlicedAttnProcessor(slice_size)
|
||||||
|
|
||||||
|
def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None):
|
||||||
|
if self.slice_size != 1 or attn.upcast_attention:
|
||||||
|
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
|
||||||
|
169
invokeai/frontend/web/dist/assets/App-06ea4e5e.js
vendored
Normal file
169
invokeai/frontend/web/dist/assets/App-06ea4e5e.js
vendored
Normal file
File diff suppressed because one or more lines are too long
169
invokeai/frontend/web/dist/assets/App-9c3ea767.js
vendored
169
invokeai/frontend/web/dist/assets/App-9c3ea767.js
vendored
File diff suppressed because one or more lines are too long
1
invokeai/frontend/web/dist/assets/MantineProvider-b5842fc1.js
vendored
Normal file
1
invokeai/frontend/web/dist/assets/MantineProvider-b5842fc1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
125
invokeai/frontend/web/dist/assets/index-adc79457.js
vendored
125
invokeai/frontend/web/dist/assets/index-adc79457.js
vendored
File diff suppressed because one or more lines are too long
125
invokeai/frontend/web/dist/assets/index-e2437518.js
vendored
Normal file
125
invokeai/frontend/web/dist/assets/index-e2437518.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
invokeai/frontend/web/dist/index.html
vendored
2
invokeai/frontend/web/dist/index.html
vendored
@ -12,7 +12,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="./assets/index-adc79457.js"></script>
|
<script type="module" crossorigin src="./assets/index-e2437518.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body dir="ltr">
|
<body dir="ltr">
|
||||||
|
14
invokeai/frontend/web/dist/locales/en.json
vendored
14
invokeai/frontend/web/dist/locales/en.json
vendored
@ -455,7 +455,12 @@
|
|||||||
"addDifference": "Add Difference",
|
"addDifference": "Add Difference",
|
||||||
"pickModelType": "Pick Model Type",
|
"pickModelType": "Pick Model Type",
|
||||||
"selectModel": "Select Model",
|
"selectModel": "Select Model",
|
||||||
"importModels": "Import Models"
|
"importModels": "Import Models",
|
||||||
|
"settings": "Settings",
|
||||||
|
"syncModels": "Sync Models",
|
||||||
|
"syncModelsDesc": "If your models are out of sync with the backend, you can refresh them up using this option. This is generally handy in cases where you manually update your models.yaml file or add models to the InvokeAI root folder after the application has booted.",
|
||||||
|
"modelsSynced": "Models Synced",
|
||||||
|
"modelSyncFailed": "Model Sync Failed"
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@ -547,7 +552,8 @@
|
|||||||
"saveSteps": "Save images every n steps",
|
"saveSteps": "Save images every n steps",
|
||||||
"confirmOnDelete": "Confirm On Delete",
|
"confirmOnDelete": "Confirm On Delete",
|
||||||
"displayHelpIcons": "Display Help Icons",
|
"displayHelpIcons": "Display Help Icons",
|
||||||
"useCanvasBeta": "Use Canvas Beta Layout",
|
"alternateCanvasLayout": "Alternate Canvas Layout",
|
||||||
|
"enableNodesEditor": "Enable Nodes Editor",
|
||||||
"enableImageDebugging": "Enable Image Debugging",
|
"enableImageDebugging": "Enable Image Debugging",
|
||||||
"useSlidersForAll": "Use Sliders For All Options",
|
"useSlidersForAll": "Use Sliders For All Options",
|
||||||
"showProgressInViewer": "Show Progress Images in Viewer",
|
"showProgressInViewer": "Show Progress Images in Viewer",
|
||||||
@ -564,7 +570,9 @@
|
|||||||
"ui": "User Interface",
|
"ui": "User Interface",
|
||||||
"favoriteSchedulers": "Favorite Schedulers",
|
"favoriteSchedulers": "Favorite Schedulers",
|
||||||
"favoriteSchedulersPlaceholder": "No schedulers favorited",
|
"favoriteSchedulersPlaceholder": "No schedulers favorited",
|
||||||
"showAdvancedOptions": "Show Advanced Options"
|
"showAdvancedOptions": "Show Advanced Options",
|
||||||
|
"experimental": "Experimental",
|
||||||
|
"beta": "Beta"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"serverError": "Server Error",
|
"serverError": "Server Error",
|
||||||
|
@ -175,9 +175,7 @@ export const isValidDrop = (
|
|||||||
const destinationBoard = overData.context.boardId;
|
const destinationBoard = overData.context.boardId;
|
||||||
|
|
||||||
const isSameBoard = currentBoard === destinationBoard;
|
const isSameBoard = currentBoard === destinationBoard;
|
||||||
const isDestinationValid = !currentBoard
|
const isDestinationValid = !currentBoard ? destinationBoard : true;
|
||||||
? destinationBoard !== 'no_board'
|
|
||||||
: true;
|
|
||||||
|
|
||||||
return !isSameBoard && isDestinationValid;
|
return !isSameBoard && isDestinationValid;
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,10 @@ export const addFirstListImagesListener = () => {
|
|||||||
action,
|
action,
|
||||||
{ getState, dispatch, unsubscribe, cancelActiveListeners }
|
{ getState, dispatch, unsubscribe, cancelActiveListeners }
|
||||||
) => {
|
) => {
|
||||||
// Only run this listener on the first listImages request for `images` categories
|
// Only run this listener on the first listImages request for no-board images
|
||||||
if (
|
if (
|
||||||
action.meta.arg.queryCacheKey !==
|
action.meta.arg.queryCacheKey !==
|
||||||
getListImagesUrl({ categories: IMAGE_CATEGORIES })
|
getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import {
|
import {
|
||||||
|
ASSETS_CATEGORIES,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
|
galleryViewChanged,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
|
||||||
getBoardIdQueryParamForBoard,
|
|
||||||
getCategoriesQueryParamForBoard,
|
|
||||||
} from 'features/gallery/store/util';
|
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
import { isAnyOf } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
|
|
||||||
export const addBoardIdSelectedListener = () => {
|
export const addBoardIdSelectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: boardIdSelected,
|
matcher: isAnyOf(boardIdSelected, galleryViewChanged),
|
||||||
effect: async (
|
effect: async (
|
||||||
action,
|
action,
|
||||||
{ getState, dispatch, condition, cancelActiveListeners }
|
{ getState, dispatch, condition, cancelActiveListeners }
|
||||||
@ -22,12 +22,21 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
|
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
|
||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
|
|
||||||
const _board_id = action.payload;
|
const state = getState();
|
||||||
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
|
|
||||||
|
|
||||||
const categories = getCategoriesQueryParamForBoard(_board_id);
|
const board_id = boardIdSelected.match(action)
|
||||||
const board_id = getBoardIdQueryParamForBoard(_board_id);
|
? action.payload
|
||||||
const queryArgs = { board_id, categories };
|
: state.gallery.selectedBoardId;
|
||||||
|
|
||||||
|
const galleryView = galleryViewChanged.match(action)
|
||||||
|
? action.payload
|
||||||
|
: state.gallery.galleryView;
|
||||||
|
|
||||||
|
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
|
||||||
|
const categories =
|
||||||
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||||
|
|
||||||
|
const queryArgs = { board_id: board_id ?? 'none', categories };
|
||||||
|
|
||||||
// wait until the board has some images - maybe it already has some from a previous fetch
|
// wait until the board has some images - maybe it already has some from a previous fetch
|
||||||
// must use getState() to ensure we do not have stale state
|
// must use getState() to ensure we do not have stale state
|
||||||
@ -35,7 +44,7 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
() =>
|
() =>
|
||||||
imagesApi.endpoints.listImages.select(queryArgs)(getState())
|
imagesApi.endpoints.listImages.select(queryArgs)(getState())
|
||||||
.isSuccess,
|
.isSuccess,
|
||||||
1000
|
5000
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
@ -45,7 +45,7 @@ export const addCanvasMergedListener = () => {
|
|||||||
relativeTo: canvasBaseLayer.getParent(),
|
relativeTo: canvasBaseLayer.getParent(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageUploadedRequest = dispatch(
|
const imageDTO = await dispatch(
|
||||||
imagesApi.endpoints.uploadImage.initiate({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([blob], 'mergedCanvas.png', {
|
file: new File([blob], 'mergedCanvas.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
@ -57,17 +57,10 @@ export const addCanvasMergedListener = () => {
|
|||||||
toastOptions: { title: 'Canvas Merged' },
|
toastOptions: { title: 'Canvas Merged' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
).unwrap();
|
||||||
|
|
||||||
const [{ payload }] = await take(
|
|
||||||
(uploadedImageAction) =>
|
|
||||||
imagesApi.endpoints.uploadImage.matchFulfilled(uploadedImageAction) &&
|
|
||||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
|
// TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
|
||||||
const { image_name } =
|
const { image_name } = imageDTO;
|
||||||
payload as typeof imagesApi.endpoints.uploadImage.Types.ResultType;
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setMergedCanvas({
|
setMergedCanvas({
|
||||||
|
@ -34,6 +34,8 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
}),
|
}),
|
||||||
image_category: 'general',
|
image_category: 'general',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
|
board_id: state.gallery.autoAddBoardId,
|
||||||
|
crop_visible: true,
|
||||||
postUploadAction: {
|
postUploadAction: {
|
||||||
type: 'TOAST',
|
type: 'TOAST',
|
||||||
toastOptions: { title: 'Canvas Saved to Gallery' },
|
toastOptions: { title: 'Canvas Saved to Gallery' },
|
||||||
|
@ -156,14 +156,13 @@ export const addImageDroppedListener = () => {
|
|||||||
if (
|
if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO &&
|
activeData.payload.imageDTO
|
||||||
overData.context.boardId
|
|
||||||
) {
|
) {
|
||||||
const { imageDTO } = activeData.payload;
|
const { imageDTO } = activeData.payload;
|
||||||
const { boardId } = overData.context;
|
const { boardId } = overData.context;
|
||||||
|
|
||||||
// if the board is "No Board", this is a remove action
|
// image was droppe on the "NoBoardBoard"
|
||||||
if (boardId === 'no_board') {
|
if (!boardId) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.removeImageFromBoard.initiate({
|
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||||
imageDTO,
|
imageDTO,
|
||||||
@ -172,12 +171,7 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle adding image to batch
|
// image was dropped on a user board
|
||||||
if (boardId === 'batch') {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, add the image to the board
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
imageDTO,
|
imageDTO,
|
||||||
|
@ -5,30 +5,30 @@ import { startAppListening } from '..';
|
|||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
export const addImageUpdatedFulfilledListener = () => {
|
export const addImageUpdatedFulfilledListener = () => {
|
||||||
startAppListening({
|
// startAppListening({
|
||||||
matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
// matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||||
effect: (action, { dispatch, getState }) => {
|
// effect: (action, { dispatch, getState }) => {
|
||||||
moduleLog.debug(
|
// moduleLog.debug(
|
||||||
{
|
// {
|
||||||
data: {
|
// data: {
|
||||||
oldImage: action.meta.arg.originalArgs,
|
// oldImage: action.meta.arg.originalArgs,
|
||||||
updatedImage: action.payload,
|
// updatedImage: action.payload,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
'Image updated'
|
// 'Image updated'
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addImageUpdatedRejectedListener = () => {
|
export const addImageUpdatedRejectedListener = () => {
|
||||||
startAppListening({
|
// startAppListening({
|
||||||
matcher: imagesApi.endpoints.updateImage.matchRejected,
|
// matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||||
effect: (action, { dispatch }) => {
|
// effect: (action, { dispatch }) => {
|
||||||
moduleLog.debug(
|
// moduleLog.debug(
|
||||||
{ data: action.meta.arg.originalArgs },
|
// { data: action.meta.arg.originalArgs },
|
||||||
'Image update failed'
|
// 'Image update failed'
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
@ -8,10 +8,7 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
|||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import {
|
import { imagesApi } from '../../../../../services/api/endpoints/images';
|
||||||
SYSTEM_BOARDS,
|
|
||||||
imagesApi,
|
|
||||||
} from '../../../../../services/api/endpoints/images';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -26,7 +23,7 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
const imageDTO = action.payload;
|
const imageDTO = action.payload;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { selectedBoardId } = state.gallery;
|
const { selectedBoardId, autoAddBoardId } = state.gallery;
|
||||||
|
|
||||||
moduleLog.debug({ arg: '<Blob>', imageDTO }, 'Image uploaded');
|
moduleLog.debug({ arg: '<Blob>', imageDTO }, 'Image uploaded');
|
||||||
|
|
||||||
@ -44,13 +41,13 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
// default action - just upload and alert user
|
// default action - just upload and alert user
|
||||||
if (postUploadAction?.type === 'TOAST') {
|
if (postUploadAction?.type === 'TOAST') {
|
||||||
const { toastOptions } = postUploadAction;
|
const { toastOptions } = postUploadAction;
|
||||||
if (SYSTEM_BOARDS.includes(selectedBoardId)) {
|
if (!autoAddBoardId) {
|
||||||
dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
|
dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
|
||||||
} else {
|
} else {
|
||||||
// Add this image to the board
|
// Add this image to the board
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
board_id: selectedBoardId,
|
board_id: autoAddBoardId,
|
||||||
imageDTO,
|
imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -59,10 +56,10 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
const { data } = boardsApi.endpoints.listAllBoards.select()(state);
|
const { data } = boardsApi.endpoints.listAllBoards.select()(state);
|
||||||
|
|
||||||
// Fall back to just the board id if we can't find the board for some reason
|
// Fall back to just the board id if we can't find the board for some reason
|
||||||
const board = data?.find((b) => b.board_id === selectedBoardId);
|
const board = data?.find((b) => b.board_id === autoAddBoardId);
|
||||||
const description = board
|
const description = board
|
||||||
? `Added to board ${board.board_name}`
|
? `Added to board ${board.board_name}`
|
||||||
: `Added to board ${selectedBoardId}`;
|
: `Added to board ${autoAddBoardId}`;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast({
|
addToast({
|
||||||
|
@ -3,6 +3,7 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
|||||||
import {
|
import {
|
||||||
IMAGE_CATEGORIES,
|
IMAGE_CATEGORIES,
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
|
galleryViewChanged,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||||
@ -55,37 +56,16 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!imageDTO.is_intermediate) {
|
if (!imageDTO.is_intermediate) {
|
||||||
// update the cache for 'All Images'
|
/**
|
||||||
dispatch(
|
* Cache updates for when an image result is received
|
||||||
imagesApi.util.updateQueryData(
|
* - *add* to getImageDTO
|
||||||
'listImages',
|
* - IF `autoAddBoardId` is set:
|
||||||
{
|
* - THEN add it to the board_id/images
|
||||||
categories: IMAGE_CATEGORIES,
|
* - ELSE (`autoAddBoardId` is not set):
|
||||||
},
|
* - THEN add it to the no_board/images
|
||||||
(draft) => {
|
*/
|
||||||
imagesAdapter.addOne(draft, imageDTO);
|
|
||||||
draft.total = draft.total + 1;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// update the cache for 'No Board'
|
|
||||||
dispatch(
|
|
||||||
imagesApi.util.updateQueryData(
|
|
||||||
'listImages',
|
|
||||||
{
|
|
||||||
board_id: 'none',
|
|
||||||
},
|
|
||||||
(draft) => {
|
|
||||||
imagesAdapter.addOne(draft, imageDTO);
|
|
||||||
draft.total = draft.total + 1;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { autoAddBoardId } = gallery;
|
const { autoAddBoardId } = gallery;
|
||||||
|
|
||||||
// add image to the board if auto-add is enabled
|
|
||||||
if (autoAddBoardId) {
|
if (autoAddBoardId) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
@ -93,8 +73,31 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
imageDTO,
|
imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{
|
||||||
|
board_id: 'none',
|
||||||
|
categories: IMAGE_CATEGORIES,
|
||||||
|
},
|
||||||
|
(draft) => {
|
||||||
|
const oldTotal = draft.total;
|
||||||
|
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.invalidateTags([
|
||||||
|
{ type: 'BoardImagesTotal', id: autoAddBoardId ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: autoAddBoardId ?? 'none' },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
const { selectedBoardId, shouldAutoSwitch } = gallery;
|
const { selectedBoardId, shouldAutoSwitch } = gallery;
|
||||||
|
|
||||||
// If auto-switch is enabled, select the new image
|
// If auto-switch is enabled, select the new image
|
||||||
@ -102,8 +105,9 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
// if auto-add is enabled, switch the board as the image comes in
|
// if auto-add is enabled, switch the board as the image comes in
|
||||||
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
|
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
|
||||||
dispatch(boardIdSelected(autoAddBoardId));
|
dispatch(boardIdSelected(autoAddBoardId));
|
||||||
|
dispatch(galleryViewChanged('images'));
|
||||||
} else if (!autoAddBoardId) {
|
} else if (!autoAddBoardId) {
|
||||||
dispatch(boardIdSelected('images'));
|
dispatch(galleryViewChanged('images'));
|
||||||
}
|
}
|
||||||
dispatch(imageSelected(imageDTO.image_name));
|
dispatch(imageSelected(imageDTO.image_name));
|
||||||
}
|
}
|
||||||
|
@ -12,25 +12,35 @@ export const addStagingAreaImageSavedListener = () => {
|
|||||||
effect: async (action, { dispatch, getState, take }) => {
|
effect: async (action, { dispatch, getState, take }) => {
|
||||||
const { imageDTO } = action.payload;
|
const { imageDTO } = action.payload;
|
||||||
|
|
||||||
dispatch(
|
try {
|
||||||
imagesApi.endpoints.updateImage.initiate({
|
const newImageDTO = await dispatch(
|
||||||
imageDTO,
|
imagesApi.endpoints.changeImageIsIntermediate.initiate({
|
||||||
changes: { is_intermediate: false },
|
imageDTO,
|
||||||
})
|
is_intermediate: false,
|
||||||
)
|
})
|
||||||
.unwrap()
|
).unwrap();
|
||||||
.then((image) => {
|
|
||||||
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
// we may need to add it to the autoadd board
|
||||||
})
|
const { autoAddBoardId } = getState().gallery;
|
||||||
.catch((error) => {
|
|
||||||
dispatch(
|
if (autoAddBoardId) {
|
||||||
addToast({
|
await dispatch(
|
||||||
title: 'Image Saving Failed',
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
description: error.message,
|
imageDTO: newImageDTO,
|
||||||
status: 'error',
|
board_id: autoAddBoardId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Image Saving Failed',
|
||||||
|
description: (error as Error)?.message,
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -73,7 +73,7 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
// For img2img and inpaint/outpaint, we need to upload the init images
|
// For img2img and inpaint/outpaint, we need to upload the init images
|
||||||
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
|
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
|
||||||
// upload the image, saving the request id
|
// upload the image, saving the request id
|
||||||
const { requestId: initImageUploadedRequestId } = dispatch(
|
canvasInitImage = await dispatch(
|
||||||
imagesApi.endpoints.uploadImage.initiate({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([baseBlob], 'canvasInitImage.png', {
|
file: new File([baseBlob], 'canvasInitImage.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
@ -81,23 +81,13 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
image_category: 'general',
|
image_category: 'general',
|
||||||
is_intermediate: true,
|
is_intermediate: true,
|
||||||
})
|
})
|
||||||
);
|
).unwrap();
|
||||||
|
|
||||||
// Wait for the image to be uploaded, matching by request id
|
|
||||||
const [{ payload }] = await take(
|
|
||||||
// TODO: figure out how to narrow this action's type
|
|
||||||
(action) =>
|
|
||||||
imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
|
|
||||||
action.meta.requestId === initImageUploadedRequestId
|
|
||||||
);
|
|
||||||
|
|
||||||
canvasInitImage = payload as ImageDTO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For inpaint/outpaint, we also need to upload the mask layer
|
// For inpaint/outpaint, we also need to upload the mask layer
|
||||||
if (['inpaint', 'outpaint'].includes(generationMode)) {
|
if (['inpaint', 'outpaint'].includes(generationMode)) {
|
||||||
// upload the image, saving the request id
|
// upload the image, saving the request id
|
||||||
const { requestId: maskImageUploadedRequestId } = dispatch(
|
canvasMaskImage = await dispatch(
|
||||||
imagesApi.endpoints.uploadImage.initiate({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([maskBlob], 'canvasMaskImage.png', {
|
file: new File([maskBlob], 'canvasMaskImage.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
@ -105,17 +95,7 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
image_category: 'mask',
|
image_category: 'mask',
|
||||||
is_intermediate: true,
|
is_intermediate: true,
|
||||||
})
|
})
|
||||||
);
|
).unwrap();
|
||||||
|
|
||||||
// Wait for the image to be uploaded, matching by request id
|
|
||||||
const [{ payload }] = await take(
|
|
||||||
// TODO: figure out how to narrow this action's type
|
|
||||||
(action) =>
|
|
||||||
imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
|
|
||||||
action.meta.requestId === maskImageUploadedRequestId
|
|
||||||
);
|
|
||||||
|
|
||||||
canvasMaskImage = payload as ImageDTO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph = buildCanvasGraph(
|
const graph = buildCanvasGraph(
|
||||||
@ -141,14 +121,14 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
sessionCreated.fulfilled.match(action) &&
|
sessionCreated.fulfilled.match(action) &&
|
||||||
action.meta.requestId === sessionCreatedRequestId
|
action.meta.requestId === sessionCreatedRequestId
|
||||||
);
|
);
|
||||||
const sessionId = sessionCreatedAction.payload.id;
|
const session_id = sessionCreatedAction.payload.id;
|
||||||
|
|
||||||
// Associate the init image with the session, now that we have the session ID
|
// Associate the init image with the session, now that we have the session ID
|
||||||
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
|
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.updateImage.initiate({
|
imagesApi.endpoints.changeImageSessionId.initiate({
|
||||||
imageDTO: canvasInitImage,
|
imageDTO: canvasInitImage,
|
||||||
changes: { session_id: sessionId },
|
session_id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,9 +136,9 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
// Associate the mask image with the session, now that we have the session ID
|
// Associate the mask image with the session, now that we have the session ID
|
||||||
if (['inpaint'].includes(generationMode) && canvasMaskImage) {
|
if (['inpaint'].includes(generationMode) && canvasMaskImage) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.updateImage.initiate({
|
imagesApi.endpoints.changeImageSessionId.initiate({
|
||||||
imageDTO: canvasMaskImage,
|
imageDTO: canvasMaskImage,
|
||||||
changes: { session_id: sessionId },
|
session_id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -167,7 +147,7 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||||
dispatch(
|
dispatch(
|
||||||
stagingAreaInitialized({
|
stagingAreaInitialized({
|
||||||
sessionId,
|
sessionId: session_id,
|
||||||
boundingBox: {
|
boundingBox: {
|
||||||
...state.canvas.boundingBoxCoordinates,
|
...state.canvas.boundingBoxCoordinates,
|
||||||
...state.canvas.boundingBoxDimensions,
|
...state.canvas.boundingBoxDimensions,
|
||||||
@ -177,7 +157,7 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flag the session with the canvas session ID
|
// Flag the session with the canvas session ID
|
||||||
dispatch(canvasSessionIdChanged(sessionId));
|
dispatch(canvasSessionIdChanged(session_id));
|
||||||
|
|
||||||
// We are ready to invoke the session!
|
// We are ready to invoke the session!
|
||||||
dispatch(sessionReadyToInvoke());
|
dispatch(sessionReadyToInvoke());
|
||||||
|
@ -92,7 +92,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
p: 4,
|
p: 4,
|
||||||
borderBottomRadius: 'base',
|
borderBottomRadius: 'base',
|
||||||
bg: mode('base.100', 'base.800')(colorMode),
|
bg: 'base.100',
|
||||||
|
_dark: {
|
||||||
|
bg: 'base.800',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -18,12 +18,20 @@ import {
|
|||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
import {
|
||||||
|
MouseEvent,
|
||||||
|
ReactElement,
|
||||||
|
SyntheticEvent,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
||||||
import { ImageDTO, PostUploadAction } from 'services/api/types';
|
import { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import IAIDraggable from './IAIDraggable';
|
import IAIDraggable from './IAIDraggable';
|
||||||
import IAIDroppable from './IAIDroppable';
|
import IAIDroppable from './IAIDroppable';
|
||||||
|
import SelectionOverlay from './SelectionOverlay';
|
||||||
|
|
||||||
type IAIDndImageProps = {
|
type IAIDndImageProps = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
@ -49,6 +57,7 @@ type IAIDndImageProps = {
|
|||||||
thumbnail?: boolean;
|
thumbnail?: boolean;
|
||||||
noContentFallback?: ReactElement;
|
noContentFallback?: ReactElement;
|
||||||
useThumbailFallback?: boolean;
|
useThumbailFallback?: boolean;
|
||||||
|
withHoverOverlay?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||||
@ -75,9 +84,17 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
resetIcon = <FaUndo />,
|
resetIcon = <FaUndo />,
|
||||||
noContentFallback = <IAINoContentFallback icon={FaImage} />,
|
noContentFallback = <IAINoContentFallback icon={FaImage} />,
|
||||||
useThumbailFallback,
|
useThumbailFallback,
|
||||||
|
withHoverOverlay = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}, []);
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
@ -105,6 +122,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Flex
|
<Flex
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
sx={{
|
sx={{
|
||||||
width: 'full',
|
width: 'full',
|
||||||
height: 'full',
|
height: 'full',
|
||||||
@ -147,14 +166,14 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
maxW: 'full',
|
maxW: 'full',
|
||||||
maxH: 'full',
|
maxH: 'full',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
shadow: isSelected ? 'selected.light' : undefined,
|
|
||||||
_dark: {
|
|
||||||
shadow: isSelected ? 'selected.dark' : undefined,
|
|
||||||
},
|
|
||||||
...imageSx,
|
...imageSx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
|
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
|
||||||
|
<SelectionOverlay
|
||||||
|
isSelected={isSelected}
|
||||||
|
isHovered={withHoverOverlay ? isHovered : false}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{!imageDTO && !isUploadDisabled && (
|
{!imageDTO && !isUploadDisabled && (
|
||||||
|
@ -19,10 +19,11 @@ import { useUploadImageMutation } from 'services/api/endpoints/images';
|
|||||||
import { PostUploadAction } from 'services/api/types';
|
import { PostUploadAction } from 'services/api/types';
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
(activeTabName) => {
|
({ gallery }, activeTabName) => {
|
||||||
let postUploadAction: PostUploadAction = { type: 'TOAST' };
|
let postUploadAction: PostUploadAction = { type: 'TOAST' };
|
||||||
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
if (activeTabName === 'unifiedCanvas') {
|
||||||
@ -33,7 +34,10 @@ const selector = createSelector(
|
|||||||
postUploadAction = { type: 'SET_INITIAL_IMAGE' };
|
postUploadAction = { type: 'SET_INITIAL_IMAGE' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { autoAddBoardId } = gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
autoAddBoardId,
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -46,7 +50,7 @@ type ImageUploaderProps = {
|
|||||||
|
|
||||||
const ImageUploader = (props: ImageUploaderProps) => {
|
const ImageUploader = (props: ImageUploaderProps) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
const { postUploadAction } = useAppSelector(selector);
|
const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
|
||||||
const isBusy = useAppSelector(selectIsBusy);
|
const isBusy = useAppSelector(selectIsBusy);
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -74,9 +78,10 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
image_category: 'user',
|
image_category: 'user',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
|
board_id: autoAddBoardId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[postUploadAction, uploadImage]
|
[autoAddBoardId, postUploadAction, uploadImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
|
@ -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 { useCallback } from 'react';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
||||||
@ -31,6 +32,9 @@ export const useImageUploadButton = ({
|
|||||||
postUploadAction,
|
postUploadAction,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: UseImageUploadButtonArgs) => {
|
}: UseImageUploadButtonArgs) => {
|
||||||
|
const autoAddBoardId = useAppSelector(
|
||||||
|
(state) => state.gallery.autoAddBoardId
|
||||||
|
);
|
||||||
const [uploadImage] = useUploadImageMutation();
|
const [uploadImage] = useUploadImageMutation();
|
||||||
const onDropAccepted = useCallback(
|
const onDropAccepted = useCallback(
|
||||||
(files: File[]) => {
|
(files: File[]) => {
|
||||||
@ -45,9 +49,10 @@ export const useImageUploadButton = ({
|
|||||||
image_category: 'user',
|
image_category: 'user',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction: postUploadAction ?? { type: 'TOAST' },
|
postUploadAction: postUploadAction ?? { type: 'TOAST' },
|
||||||
|
board_id: autoAddBoardId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[postUploadAction, uploadImage]
|
[autoAddBoardId, postUploadAction, uploadImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -98,12 +98,16 @@ const ParamEmbeddingPopover = (props: Props) => {
|
|||||||
sx={{ p: 0, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }}
|
sx={{ p: 0, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }}
|
||||||
>
|
>
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<Flex sx={{ justifyContent: 'center', p: 2 }}>
|
<Flex
|
||||||
<Text
|
sx={{
|
||||||
sx={{ fontSize: 'sm', color: 'base.500', _dark: 'base.700' }}
|
justifyContent: 'center',
|
||||||
>
|
p: 2,
|
||||||
No Embeddings Loaded
|
fontSize: 'sm',
|
||||||
</Text>
|
color: 'base.500',
|
||||||
|
_dark: { color: 'base.700' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>No Embeddings Loaded</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<IAIMantineSearchableSelect
|
<IAIMantineSearchableSelect
|
||||||
|
@ -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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
|
dispatch(autoAddBoardIdChanged(v === 'none' ? undefined : v));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
|
import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
import {
|
||||||
import { memo, useCallback } from 'react';
|
autoAddBoardIdChanged,
|
||||||
import { FaFolder } from 'react-icons/fa';
|
boardIdSelected,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
|
import { FaFolder, FaPlus } from 'react-icons/fa';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||||
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems';
|
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
||||||
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
board?: BoardDTO;
|
board?: BoardDTO;
|
||||||
board_id: string;
|
board_id?: string;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||||
};
|
};
|
||||||
@ -19,9 +25,32 @@ type Props = {
|
|||||||
const BoardContextMenu = memo(
|
const BoardContextMenu = memo(
|
||||||
({ board, board_id, setBoardToDelete, children }: Props) => {
|
({ board, board_id, setBoardToDelete, children }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(stateSelector, ({ gallery }) => {
|
||||||
|
const isSelected = gallery.selectedBoardId === board_id;
|
||||||
|
const isAutoAdd = gallery.autoAddBoardId === board_id;
|
||||||
|
return { isSelected, isAutoAdd };
|
||||||
|
}),
|
||||||
|
[board_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isSelected, isAutoAdd } = useAppSelector(selector);
|
||||||
|
const boardName = useBoardName(board_id);
|
||||||
|
|
||||||
const handleSelectBoard = useCallback(() => {
|
const handleSelectBoard = useCallback(() => {
|
||||||
dispatch(boardIdSelected(board?.board_id ?? board_id));
|
dispatch(boardIdSelected(board_id));
|
||||||
}, [board?.board_id, board_id, dispatch]);
|
}, [board_id, dispatch]);
|
||||||
|
|
||||||
|
const handleSetAutoAdd = useCallback(() => {
|
||||||
|
dispatch(autoAddBoardIdChanged(board_id));
|
||||||
|
}, [board_id, dispatch]);
|
||||||
|
|
||||||
|
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
menuProps={{ size: 'sm', isLazy: true }}
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
@ -33,17 +62,24 @@ const BoardContextMenu = memo(
|
|||||||
<MenuList
|
<MenuList
|
||||||
sx={{ visibility: 'visible !important' }}
|
sx={{ visibility: 'visible !important' }}
|
||||||
motionProps={menuListMotionProps}
|
motionProps={menuListMotionProps}
|
||||||
|
onContextMenu={skipEvent}
|
||||||
>
|
>
|
||||||
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
|
<MenuGroup title={boardName}>
|
||||||
Select Board
|
<MenuItem
|
||||||
</MenuItem>
|
icon={<FaPlus />}
|
||||||
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
|
isDisabled={isAutoAdd}
|
||||||
{board && (
|
onClick={handleSetAutoAdd}
|
||||||
<GalleryBoardContextMenuItems
|
>
|
||||||
board={board}
|
Auto-add to this Board
|
||||||
setBoardToDelete={setBoardToDelete}
|
</MenuItem>
|
||||||
/>
|
{!board && <NoBoardContextMenuItems />}
|
||||||
)}
|
{board && (
|
||||||
|
<GalleryBoardContextMenuItems
|
||||||
|
board={board}
|
||||||
|
setBoardToDelete={setBoardToDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MenuGroup>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -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 BoardsSearch from './BoardsSearch';
|
||||||
import GalleryBoard from './GalleryBoard';
|
import GalleryBoard from './GalleryBoard';
|
||||||
import SystemBoardButton from './SystemBoardButton';
|
import SystemBoardButton from './SystemBoardButton';
|
||||||
|
import NoBoardBoard from './NoBoardBoard';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -42,10 +43,6 @@ const BoardsList = (props: Props) => {
|
|||||||
)
|
)
|
||||||
: boards;
|
: boards;
|
||||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
const handleClickSearchIcon = useCallback(() => {
|
|
||||||
setIsSearching((v) => !v);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -61,54 +58,7 @@ const BoardsList = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||||
<AnimatePresence mode="popLayout">
|
<BoardsSearch />
|
||||||
{isSearching ? (
|
|
||||||
<motion.div
|
|
||||||
key="boards-search"
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.1 },
|
|
||||||
}}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<BoardsSearch setIsSearching={setIsSearching} />
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
<motion.div
|
|
||||||
key="system-boards-select"
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.1 },
|
|
||||||
}}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<ButtonGroup sx={{ w: 'full', ps: 1.5 }} isAttached>
|
|
||||||
<SystemBoardButton board_id="images" />
|
|
||||||
<SystemBoardButton board_id="assets" />
|
|
||||||
<SystemBoardButton board_id="no_board" />
|
|
||||||
</ButtonGroup>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<IAIIconButton
|
|
||||||
aria-label="Search Boards"
|
|
||||||
size="sm"
|
|
||||||
isChecked={isSearching}
|
|
||||||
onClick={handleClickSearchIcon}
|
|
||||||
icon={<FaSearch />}
|
|
||||||
/>
|
|
||||||
<AddBoardButton />
|
<AddBoardButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
@ -126,10 +76,13 @@ const BoardsList = (props: Props) => {
|
|||||||
<Grid
|
<Grid
|
||||||
className="list-container"
|
className="list-container"
|
||||||
sx={{
|
sx={{
|
||||||
gridTemplateColumns: `repeat(auto-fill, minmax(96px, 1fr));`,
|
gridTemplateColumns: `repeat(auto-fill, minmax(108px, 1fr));`,
|
||||||
maxH: 346,
|
maxH: 346,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
|
<NoBoardBoard isSelected={selectedBoardId === undefined} />
|
||||||
|
</GridItem>
|
||||||
{filteredBoards &&
|
{filteredBoards &&
|
||||||
filteredBoards.map((board) => (
|
filteredBoards.map((board) => (
|
||||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||||
|
@ -28,12 +28,7 @@ const selector = createSelector(
|
|||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const BoardsSearch = () => {
|
||||||
setIsSearching: (isSearching: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BoardsSearch = (props: Props) => {
|
|
||||||
const { setIsSearching } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { searchText } = useAppSelector(selector);
|
const { searchText } = useAppSelector(selector);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -47,8 +42,7 @@ const BoardsSearch = (props: Props) => {
|
|||||||
|
|
||||||
const clearBoardSearch = useCallback(() => {
|
const clearBoardSearch = useCallback(() => {
|
||||||
dispatch(setBoardSearchText(''));
|
dispatch(setBoardSearchText(''));
|
||||||
setIsSearching(false);
|
}, [dispatch]);
|
||||||
}, [dispatch, setIsSearching]);
|
|
||||||
|
|
||||||
const handleKeydown = useCallback(
|
const handleKeydown = useCallback(
|
||||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
@ -19,16 +19,14 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { FaFolder } from 'react-icons/fa';
|
import { FaUser } from 'react-icons/fa';
|
||||||
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
|
import AutoAddIcon from '../AutoAddIcon';
|
||||||
import BoardContextMenu from '../BoardContextMenu';
|
import BoardContextMenu from '../BoardContextMenu';
|
||||||
|
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||||
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
|
||||||
bg: 'accent.200',
|
|
||||||
color: 'blackAlpha.900',
|
|
||||||
};
|
|
||||||
|
|
||||||
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||||
bg: 'base.500',
|
bg: 'base.500',
|
||||||
@ -59,11 +57,19 @@ const GalleryBoard = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { isSelectedForAutoAdd } = useAppSelector(selector);
|
const { isSelectedForAutoAdd } = useAppSelector(selector);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}, []);
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
}, []);
|
||||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||||
board.cover_image_name ?? skipToken
|
board.cover_image_name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { totalImages, totalAssets } = useBoardTotal(board.board_id);
|
||||||
|
|
||||||
const { board_name, board_id } = board;
|
const { board_name, board_id } = board;
|
||||||
const [localBoardName, setLocalBoardName] = useState(board_name);
|
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||||
|
|
||||||
@ -84,26 +90,30 @@ const GalleryBoard = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(newBoardName: string) => {
|
async (newBoardName: string) => {
|
||||||
if (!newBoardName) {
|
// empty strings are not allowed
|
||||||
// empty strings are not allowed
|
if (!newBoardName.trim()) {
|
||||||
setLocalBoardName(board_name);
|
setLocalBoardName(board_name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't updated the board name if it hasn't changed
|
||||||
if (newBoardName === board_name) {
|
if (newBoardName === board_name) {
|
||||||
// don't updated the board name if it hasn't changed
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateBoard({ board_id, changes: { board_name: newBoardName } })
|
|
||||||
.unwrap()
|
try {
|
||||||
.then((response) => {
|
const { board_name } = await updateBoard({
|
||||||
// update local state
|
board_id,
|
||||||
setLocalBoardName(response.board_name);
|
changes: { board_name: newBoardName },
|
||||||
})
|
}).unwrap();
|
||||||
.catch(() => {
|
|
||||||
// revert on error
|
// update local state
|
||||||
setLocalBoardName(board_name);
|
setLocalBoardName(board_name);
|
||||||
});
|
} catch {
|
||||||
|
// revert on error
|
||||||
|
setLocalBoardName(board_name);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[board_id, board_name, updateBoard]
|
[board_id, board_name, updateBoard]
|
||||||
);
|
);
|
||||||
@ -117,6 +127,8 @@ const GalleryBoard = memo(
|
|||||||
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
|
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -143,57 +155,49 @@ const GalleryBoard = memo(
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
bg: 'base.200',
|
||||||
|
_dark: {
|
||||||
|
bg: 'base.800',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
{coverImage?.thumbnail_url ? (
|
||||||
sx={{
|
<Image
|
||||||
w: 'full',
|
src={coverImage?.thumbnail_url}
|
||||||
h: 'full',
|
draggable={false}
|
||||||
justifyContent: 'center',
|
sx={{
|
||||||
alignItems: 'center',
|
objectFit: 'cover',
|
||||||
borderRadius: 'base',
|
w: 'full',
|
||||||
bg: 'base.200',
|
h: 'full',
|
||||||
_dark: {
|
maxH: 'full',
|
||||||
bg: 'base.800',
|
borderRadius: 'base',
|
||||||
},
|
borderBottomRadius: 'lg',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{coverImage?.thumbnail_url ? (
|
) : (
|
||||||
<Image
|
<Flex
|
||||||
src={coverImage?.thumbnail_url}
|
sx={{
|
||||||
draggable={false}
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
boxSize={12}
|
||||||
|
as={FaUser}
|
||||||
sx={{
|
sx={{
|
||||||
maxW: 'full',
|
mt: -6,
|
||||||
maxH: 'full',
|
opacity: 0.7,
|
||||||
borderRadius: 'base',
|
color: 'base.500',
|
||||||
borderBottomRadius: 'lg',
|
_dark: {
|
||||||
|
color: 'base.500',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
</Flex>
|
||||||
<Flex
|
)}
|
||||||
sx={{
|
{/* <Flex
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
boxSize={12}
|
|
||||||
as={FaFolder}
|
|
||||||
sx={{
|
|
||||||
mt: -3,
|
|
||||||
opacity: 0.7,
|
|
||||||
color: 'base.500',
|
|
||||||
_dark: {
|
|
||||||
color: 'base.500',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
insetInlineEnd: 0,
|
insetInlineEnd: 0,
|
||||||
@ -201,33 +205,14 @@ const GalleryBoard = memo(
|
|||||||
p: 1,
|
p: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||||
variant="solid"
|
{totalImages}/{totalAssets}
|
||||||
sx={
|
|
||||||
isSelectedForAutoAdd
|
|
||||||
? AUTO_ADD_BADGE_STYLES
|
|
||||||
: BASE_BADGE_STYLES
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{board.image_count}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex> */}
|
||||||
<Box
|
{isSelectedForAutoAdd && <AutoAddIcon />}
|
||||||
className="selection-box"
|
<SelectionOverlay
|
||||||
sx={{
|
isSelected={isSelected}
|
||||||
position: 'absolute',
|
isHovered={isHovered}
|
||||||
top: 0,
|
|
||||||
insetInlineEnd: 0,
|
|
||||||
bottom: 0,
|
|
||||||
insetInlineStart: 0,
|
|
||||||
borderRadius: 'base',
|
|
||||||
transitionProperty: 'common',
|
|
||||||
transitionDuration: 'common',
|
|
||||||
shadow: isSelected ? 'selected.light' : undefined,
|
|
||||||
_dark: {
|
|
||||||
shadow: isSelected ? 'selected.dark' : undefined,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -1,54 +1,179 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
import { Box, ChakraProps, Flex, Image, Text } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import {
|
import { stateSelector } from 'app/store/store';
|
||||||
INITIAL_IMAGE_LIMIT,
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
boardIdSelected,
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
} from 'features/gallery/store/gallerySlice';
|
import InvokeAILogoImage from 'assets/images/logo.png';
|
||||||
import { FaFolderOpen } from 'react-icons/fa';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import { useDispatch } from 'react-redux';
|
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||||
import {
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
ListImagesArgs,
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
useListImagesQuery,
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
} from 'services/api/endpoints/images';
|
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||||
import GenericBoard from './GenericBoard';
|
import AutoAddIcon from '../AutoAddIcon';
|
||||||
|
import BoardContextMenu from '../BoardContextMenu';
|
||||||
|
|
||||||
const baseQueryArg: ListImagesArgs = {
|
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||||
board_id: 'none',
|
bg: 'base.500',
|
||||||
offset: 0,
|
color: 'whiteAlpha.900',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
|
||||||
is_intermediate: false,
|
|
||||||
};
|
};
|
||||||
|
interface Props {
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
|
const selector = createSelector(
|
||||||
const dispatch = useDispatch();
|
stateSelector,
|
||||||
|
({ gallery }) => {
|
||||||
|
const { autoAddBoardId } = gallery;
|
||||||
|
return { autoAddBoardId };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
const handleClick = () => {
|
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||||
dispatch(boardIdSelected('no_board'));
|
const dispatch = useAppDispatch();
|
||||||
};
|
const { totalImages, totalAssets } = useBoardTotal(undefined);
|
||||||
|
const { autoAddBoardId } = useAppSelector(selector);
|
||||||
|
const boardName = useBoardName(undefined);
|
||||||
|
const handleSelectBoard = useCallback(() => {
|
||||||
|
dispatch(boardIdSelected(undefined));
|
||||||
|
}, [dispatch]);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}, []);
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { total } = useListImagesQuery(baseQueryArg, {
|
const droppableData: MoveBoardDropData = useMemo(
|
||||||
selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
|
() => ({
|
||||||
});
|
id: 'no_board',
|
||||||
|
actionType: 'MOVE_BOARD',
|
||||||
// TODO: Do we support making 'images' 'assets? if yes, we need to handle this
|
context: { boardId: undefined },
|
||||||
const droppableData: MoveBoardDropData = {
|
}),
|
||||||
id: 'all-images-board',
|
[]
|
||||||
actionType: 'MOVE_BOARD',
|
);
|
||||||
context: { boardId: 'no_board' },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericBoard
|
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
|
||||||
board_id="no_board"
|
<Flex
|
||||||
droppableData={droppableData}
|
onMouseOver={handleMouseOver}
|
||||||
dropLabel={<Text fontSize="md">Move</Text>}
|
onMouseOut={handleMouseOut}
|
||||||
onClick={handleClick}
|
sx={{
|
||||||
isSelected={isSelected}
|
position: 'relative',
|
||||||
icon={FaFolderOpen}
|
justifyContent: 'center',
|
||||||
label="No Board"
|
alignItems: 'center',
|
||||||
badgeCount={total}
|
aspectRatio: '1/1',
|
||||||
/>
|
borderRadius: 'base',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BoardContextMenu>
|
||||||
|
{(ref) => (
|
||||||
|
<Flex
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleSelectBoard}
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
position: 'relative',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
cursor: 'pointer',
|
||||||
|
bg: 'base.200',
|
||||||
|
_dark: {
|
||||||
|
bg: 'base.800',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <Icon
|
||||||
|
boxSize={12}
|
||||||
|
as={FaBucket}
|
||||||
|
sx={{
|
||||||
|
opacity: 0.7,
|
||||||
|
color: 'base.500',
|
||||||
|
_dark: {
|
||||||
|
color: 'base.500',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
<Image
|
||||||
|
src={InvokeAILogoImage}
|
||||||
|
alt="invoke-ai-logo"
|
||||||
|
sx={{
|
||||||
|
opacity: 0.4,
|
||||||
|
filter: 'grayscale(1)',
|
||||||
|
mt: -6,
|
||||||
|
w: 16,
|
||||||
|
h: 16,
|
||||||
|
minW: 16,
|
||||||
|
minH: 16,
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{/* <Flex
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
top: 0,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||||
|
{totalImages}/{totalAssets}
|
||||||
|
</Badge>
|
||||||
|
</Flex> */}
|
||||||
|
{!autoAddBoardId && <AutoAddIcon />}
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
p: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
w: 'full',
|
||||||
|
maxW: 'full',
|
||||||
|
borderBottomRadius: 'base',
|
||||||
|
bg: isSelected ? 'accent.400' : 'base.500',
|
||||||
|
color: isSelected ? 'base.50' : 'base.100',
|
||||||
|
_dark: {
|
||||||
|
bg: isSelected ? 'accent.500' : 'base.600',
|
||||||
|
color: isSelected ? 'base.50' : 'base.100',
|
||||||
|
},
|
||||||
|
lineHeight: 'short',
|
||||||
|
fontSize: 'xs',
|
||||||
|
fontWeight: isSelected ? 700 : 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{boardName}
|
||||||
|
</Flex>
|
||||||
|
<SelectionOverlay isSelected={isSelected} isHovered={isHovered} />
|
||||||
|
<IAIDroppable
|
||||||
|
data={droppableData}
|
||||||
|
dropLabel={<Text fontSize="md">Move</Text>}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</BoardContextMenu>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
NoBoardBoard.displayName = 'HoverableBoard';
|
||||||
|
|
||||||
export default NoBoardBoard;
|
export default NoBoardBoard;
|
||||||
|
@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { FaMinus, FaPlus, FaTrash } from 'react-icons/fa';
|
import { FaPlus, FaTrash } from 'react-icons/fa';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -42,7 +42,7 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
|||||||
|
|
||||||
const handleToggleAutoAdd = useCallback(() => {
|
const handleToggleAutoAdd = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id)
|
autoAddBoardIdChanged(isSelectedForAutoAdd ? undefined : board.board_id)
|
||||||
);
|
);
|
||||||
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
|
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
|
||||||
|
|
||||||
@ -59,16 +59,15 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
|||||||
</MenuItem> */}
|
</MenuItem> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
{/* {!isSelectedForAutoAdd && (
|
||||||
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />}
|
<MenuItem icon={<FaPlus />} onClick={handleToggleAutoAdd}>
|
||||||
onClickCapture={handleToggleAutoAdd}
|
Auto-add to this Board
|
||||||
>
|
</MenuItem>
|
||||||
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'}
|
)} */}
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
onClickCapture={handleDelete}
|
onClick={handleDelete}
|
||||||
>
|
>
|
||||||
Delete Board
|
Delete Board
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -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 { ChevronUpIcon } from '@chakra-ui/icons';
|
||||||
import { Box, Button, Flex, Spacer, Text } from '@chakra-ui/react';
|
import { Button, Flex, Text } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -27,52 +26,64 @@ const GalleryBoardName = (props: Props) => {
|
|||||||
const { isOpen, onToggle } = props;
|
const { isOpen, onToggle } = props;
|
||||||
const { selectedBoardId } = useAppSelector(selector);
|
const { selectedBoardId } = useAppSelector(selector);
|
||||||
const boardName = useBoardName(selectedBoardId);
|
const boardName = useBoardName(selectedBoardId);
|
||||||
const numOfBoardImages = useBoardTotal(selectedBoardId);
|
// const { totalImages, totalAssets } = useBoardTotal(selectedBoardId);
|
||||||
|
|
||||||
const formattedBoardName = useMemo(() => {
|
const formattedBoardName = useMemo(() => {
|
||||||
if (!boardName) return '';
|
|
||||||
if (boardName && !numOfBoardImages) return boardName;
|
|
||||||
if (boardName.length > 20) {
|
if (boardName.length > 20) {
|
||||||
return `${boardName.substring(0, 20)}... (${numOfBoardImages})`;
|
return `${boardName.substring(0, 20)}...`;
|
||||||
}
|
}
|
||||||
return `${boardName} (${numOfBoardImages})`;
|
return boardName;
|
||||||
}, [boardName, numOfBoardImages]);
|
// if (!boardName) {
|
||||||
|
// return '';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (boardName && (totalImages === undefined || totalAssets === undefined)) {
|
||||||
|
// return boardName;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const count = `${totalImages}/${totalAssets}`;
|
||||||
|
|
||||||
|
// if (boardName.length > 20) {
|
||||||
|
// return `${boardName.substring(0, 20)}... (${count})`;
|
||||||
|
// }
|
||||||
|
// return `${boardName} (${count})`;
|
||||||
|
}, [boardName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
as={Button}
|
as={Button}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
// variant="ghost"
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
w: 'full',
|
w: 'full',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
px: 2,
|
px: 2,
|
||||||
_hover: {
|
// bg: 'base.100',
|
||||||
bg: 'base.100',
|
// _dark: { bg: 'base.800' },
|
||||||
_dark: { bg: 'base.800' },
|
// _hover: {
|
||||||
},
|
// bg: 'base.200',
|
||||||
|
// _dark: { bg: 'base.700' },
|
||||||
|
// },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spacer />
|
<Text
|
||||||
<Box position="relative">
|
noOfLines={1}
|
||||||
<Text
|
sx={{
|
||||||
noOfLines={1}
|
fontWeight: 600,
|
||||||
sx={{
|
w: '100%',
|
||||||
fontWeight: 600,
|
textAlign: 'center',
|
||||||
color: 'base.800',
|
color: 'base.800',
|
||||||
_dark: {
|
_dark: {
|
||||||
color: 'base.200',
|
color: 'base.200',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formattedBoardName}
|
{formattedBoardName}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
|
||||||
<Spacer />
|
|
||||||
<ChevronUpIcon
|
<ChevronUpIcon
|
||||||
sx={{
|
sx={{
|
||||||
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||||
|
@ -35,6 +35,8 @@ import {
|
|||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
|
|
||||||
type SingleSelectionMenuItemsProps = {
|
type SingleSelectionMenuItemsProps = {
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
@ -70,7 +72,16 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
||||||
|
|
||||||
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
||||||
|
imageDTO.image_name,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
const { currentData } = useGetImageMetadataQuery(
|
||||||
|
debounceState.isPending()
|
||||||
|
? skipToken
|
||||||
|
: debouncedMetadataQueryArg ?? skipToken
|
||||||
|
);
|
||||||
|
|
||||||
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||||
useCopyImageToClipboard();
|
useCopyImageToClipboard();
|
||||||
|
@ -1,23 +1,38 @@
|
|||||||
import { Box, Flex, VStack, useDisclosure } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
Spacer,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
Tabs,
|
||||||
|
VStack,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
import GalleryBoardName from './GalleryBoardName';
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
import GalleryPinButton from './GalleryPinButton';
|
import GalleryPinButton from './GalleryPinButton';
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||||
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { FaImages, FaServer } from 'react-icons/fa';
|
||||||
|
import { galleryViewChanged } from '../store/gallerySlice';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { selectedBoardId } = state.gallery;
|
const { selectedBoardId, galleryView } = state.gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedBoardId,
|
selectedBoardId,
|
||||||
|
galleryView,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -26,10 +41,19 @@ const selector = createSelector(
|
|||||||
const ImageGalleryContent = () => {
|
const ImageGalleryContent = () => {
|
||||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||||
const galleryGridRef = useRef<HTMLDivElement>(null);
|
const galleryGridRef = useRef<HTMLDivElement>(null);
|
||||||
const { selectedBoardId } = useAppSelector(selector);
|
const { selectedBoardId, galleryView } = useAppSelector(selector);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
||||||
useDisclosure();
|
useDisclosure();
|
||||||
|
|
||||||
|
const handleClickImages = useCallback(() => {
|
||||||
|
dispatch(galleryViewChanged('images'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleClickAssets = useCallback(() => {
|
||||||
|
dispatch(galleryViewChanged('assets'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
sx={{
|
sx={{
|
||||||
@ -48,11 +72,11 @@ const ImageGalleryContent = () => {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GallerySettingsPopover />
|
|
||||||
<GalleryBoardName
|
<GalleryBoardName
|
||||||
isOpen={isBoardListOpen}
|
isOpen={isBoardListOpen}
|
||||||
onToggle={onToggleBoardList}
|
onToggle={onToggleBoardList}
|
||||||
/>
|
/>
|
||||||
|
<GallerySettingsPopover />
|
||||||
<GalleryPinButton />
|
<GalleryPinButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
@ -60,6 +84,55 @@ const ImageGalleryContent = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
index={galleryView === 'images' ? 0 : 1}
|
||||||
|
variant="unstyled"
|
||||||
|
size="sm"
|
||||||
|
sx={{ w: 'full' }}
|
||||||
|
>
|
||||||
|
<TabList>
|
||||||
|
<ButtonGroup
|
||||||
|
isAttached
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
as={IAIButton}
|
||||||
|
size="sm"
|
||||||
|
isChecked={galleryView === 'images'}
|
||||||
|
onClick={handleClickImages}
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
leftIcon={<FaImages />}
|
||||||
|
>
|
||||||
|
Images
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
as={IAIButton}
|
||||||
|
size="sm"
|
||||||
|
isChecked={galleryView === 'assets'}
|
||||||
|
onClick={handleClickAssets}
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
leftIcon={<FaServer />}
|
||||||
|
>
|
||||||
|
Assets
|
||||||
|
</Tab>
|
||||||
|
</ButtonGroup>
|
||||||
|
</TabList>
|
||||||
|
</Tabs>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{selectedBoardId === 'batch' ? (
|
{selectedBoardId === 'batch' ? (
|
||||||
<BatchImageGrid />
|
<BatchImageGrid />
|
||||||
) : (
|
) : (
|
||||||
|
@ -106,6 +106,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
isDropDisabled={true}
|
isDropDisabled={true}
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
thumbnail={true}
|
thumbnail={true}
|
||||||
|
withHoverOverlay
|
||||||
// resetIcon={<FaTrash />}
|
// resetIcon={<FaTrash />}
|
||||||
// resetTooltip="Delete image"
|
// resetTooltip="Delete image"
|
||||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
// withResetIcon // removed bc it's too easy to accidentally delete images
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Box, Spinner } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { IMAGE_LIMIT } from 'features/gallery//store/gallerySlice';
|
import { IMAGE_LIMIT } from 'features/gallery//store/gallerySlice';
|
||||||
|
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import {
|
import {
|
||||||
UseOverlayScrollbarsParams,
|
UseOverlayScrollbarsParams,
|
||||||
useOverlayScrollbars,
|
useOverlayScrollbars,
|
||||||
@ -15,10 +16,10 @@ import {
|
|||||||
useLazyListImagesQuery,
|
useLazyListImagesQuery,
|
||||||
useListImagesQuery,
|
useListImagesQuery,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
|
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||||
import GalleryImage from './GalleryImage';
|
import GalleryImage from './GalleryImage';
|
||||||
import ImageGridItemContainer from './ImageGridItemContainer';
|
import ImageGridItemContainer from './ImageGridItemContainer';
|
||||||
import ImageGridListContainer from './ImageGridListContainer';
|
import ImageGridListContainer from './ImageGridListContainer';
|
||||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
|
||||||
|
|
||||||
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
|
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
|
||||||
defer: true,
|
defer: true,
|
||||||
@ -40,7 +41,10 @@ const GalleryImageGrid = () => {
|
|||||||
const [initialize, osInstance] = useOverlayScrollbars(
|
const [initialize, osInstance] = useOverlayScrollbars(
|
||||||
overlayScrollbarsConfig
|
overlayScrollbarsConfig
|
||||||
);
|
);
|
||||||
|
const selectedBoardId = useAppSelector(
|
||||||
|
(state) => state.gallery.selectedBoardId
|
||||||
|
);
|
||||||
|
const { currentViewTotal } = useBoardTotal(selectedBoardId);
|
||||||
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
||||||
|
|
||||||
const { currentData, isFetching, isSuccess, isError } =
|
const { currentData, isFetching, isSuccess, isError } =
|
||||||
@ -49,19 +53,23 @@ const GalleryImageGrid = () => {
|
|||||||
const [listImages] = useLazyListImagesQuery();
|
const [listImages] = useLazyListImagesQuery();
|
||||||
|
|
||||||
const areMoreAvailable = useMemo(() => {
|
const areMoreAvailable = useMemo(() => {
|
||||||
if (!currentData) {
|
if (!currentData || !currentViewTotal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return currentData.ids.length < currentData.total;
|
return currentData.ids.length < currentViewTotal;
|
||||||
}, [currentData]);
|
}, [currentData, currentViewTotal]);
|
||||||
|
|
||||||
const handleLoadMoreImages = useCallback(() => {
|
const handleLoadMoreImages = useCallback(() => {
|
||||||
|
if (!areMoreAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
listImages({
|
listImages({
|
||||||
...queryArgs,
|
...queryArgs,
|
||||||
offset: currentData?.ids.length ?? 0,
|
offset: currentData?.ids.length ?? 0,
|
||||||
limit: IMAGE_LIMIT,
|
limit: IMAGE_LIMIT,
|
||||||
});
|
});
|
||||||
}, [listImages, queryArgs, currentData?.ids.length]);
|
}, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize the gallery's custom scrollbar
|
// Initialize the gallery's custom scrollbar
|
||||||
@ -79,20 +87,34 @@ const GalleryImageGrid = () => {
|
|||||||
|
|
||||||
if (!currentData) {
|
if (!currentData) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ w: 'full', h: 'full' }}>
|
<Flex
|
||||||
<Spinner size="2xl" opacity={0.5} />
|
sx={{
|
||||||
</Box>
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IAINoContentFallback label="Loading..." icon={FaImage} />
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess && currentData?.ids.length === 0) {
|
if (isSuccess && currentData?.ids.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ w: 'full', h: 'full' }}>
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<IAINoContentFallback
|
<IAINoContentFallback
|
||||||
label={t('gallery.noImagesInGallery')}
|
label={t('gallery.noImagesInGallery')}
|
||||||
icon={FaImage}
|
icon={FaImage}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,9 +143,7 @@ const GalleryImageGrid = () => {
|
|||||||
loadingText="Loading"
|
loadingText="Loading"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
{areMoreAvailable
|
{`Load More (${currentData.ids.length} of ${currentViewTotal})`}
|
||||||
? t('gallery.loadMore')
|
|
||||||
: t('gallery.allImagesLoaded')}
|
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import {
|
import {
|
||||||
IMAGE_LIMIT,
|
IMAGE_LIMIT,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectImagesById,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
import { clamp, isEqual } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@ -53,8 +52,8 @@ export const nextPrevImageButtonsSelector = createSelector(
|
|||||||
|
|
||||||
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
||||||
|
|
||||||
const nextImageId = images[nextImageIndex].image_name;
|
const nextImageId = images[nextImageIndex]?.image_name;
|
||||||
const prevImageId = images[prevImageIndex].image_name;
|
const prevImageId = images[prevImageIndex]?.image_name;
|
||||||
|
|
||||||
const nextImage = selectors.selectById(data, nextImageId);
|
const nextImage = selectors.selectById(data, nextImageId);
|
||||||
const prevImage = selectors.selectById(data, prevImageId);
|
const prevImage = selectors.selectById(data, prevImageId);
|
||||||
@ -65,7 +64,7 @@ export const nextPrevImageButtonsSelector = createSelector(
|
|||||||
isOnFirstImage: currentImageIndex === 0,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
isOnLastImage:
|
isOnLastImage:
|
||||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||||
areMoreImagesAvailable: data?.total ?? 0 > imagesLength,
|
areMoreImagesAvailable: (data?.total ?? 0) > imagesLength,
|
||||||
isFetching: status === 'pending',
|
isFetching: status === 'pending',
|
||||||
nextImage,
|
nextImage,
|
||||||
prevImage,
|
prevImage,
|
||||||
|
@ -2,11 +2,11 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { ListImagesArgs } from 'services/api/endpoints/images';
|
import { ListImagesArgs } from 'services/api/endpoints/images';
|
||||||
import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
|
|
||||||
import {
|
import {
|
||||||
getBoardIdQueryParamForBoard,
|
ASSETS_CATEGORIES,
|
||||||
getCategoriesQueryParamForBoard,
|
IMAGE_CATEGORIES,
|
||||||
} from './util';
|
INITIAL_IMAGE_LIMIT,
|
||||||
|
} from './gallerySlice';
|
||||||
|
|
||||||
export const gallerySelector = (state: RootState) => state.gallery;
|
export const gallerySelector = (state: RootState) => state.gallery;
|
||||||
|
|
||||||
@ -19,14 +19,13 @@ export const selectLastSelectedImage = createSelector(
|
|||||||
export const selectListImagesBaseQueryArgs = createSelector(
|
export const selectListImagesBaseQueryArgs = createSelector(
|
||||||
[(state: RootState) => state],
|
[(state: RootState) => state],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { selectedBoardId } = state.gallery;
|
const { selectedBoardId, galleryView } = state.gallery;
|
||||||
|
const categories =
|
||||||
const categories = getCategoriesQueryParamForBoard(selectedBoardId);
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||||
const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
|
|
||||||
|
|
||||||
const listImagesBaseQueryArgs: ListImagesArgs = {
|
const listImagesBaseQueryArgs: ListImagesArgs = {
|
||||||
|
board_id: selectedBoardId ?? 'none',
|
||||||
categories,
|
categories,
|
||||||
board_id,
|
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { ImageCategory } from 'services/api/types';
|
import { ImageCategory } from 'services/api/types';
|
||||||
@ -14,20 +14,17 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
|||||||
export const INITIAL_IMAGE_LIMIT = 100;
|
export const INITIAL_IMAGE_LIMIT = 100;
|
||||||
export const IMAGE_LIMIT = 20;
|
export const IMAGE_LIMIT = 20;
|
||||||
|
|
||||||
// export type GalleryView = 'images' | 'assets';
|
export type GalleryView = 'images' | 'assets';
|
||||||
export type BoardId =
|
// export type BoardId = 'no_board' | (string & Record<never, never>);
|
||||||
| 'images'
|
export type BoardId = string | undefined;
|
||||||
| 'assets'
|
|
||||||
| 'no_board'
|
|
||||||
| 'batch'
|
|
||||||
| (string & Record<never, never>);
|
|
||||||
|
|
||||||
type GalleryState = {
|
type GalleryState = {
|
||||||
selection: string[];
|
selection: string[];
|
||||||
shouldAutoSwitch: boolean;
|
shouldAutoSwitch: boolean;
|
||||||
autoAddBoardId: string | null;
|
autoAddBoardId: string | undefined;
|
||||||
galleryImageMinimumWidth: number;
|
galleryImageMinimumWidth: number;
|
||||||
selectedBoardId: BoardId;
|
selectedBoardId: BoardId;
|
||||||
|
galleryView: GalleryView;
|
||||||
batchImageNames: string[];
|
batchImageNames: string[];
|
||||||
isBatchEnabled: boolean;
|
isBatchEnabled: boolean;
|
||||||
};
|
};
|
||||||
@ -35,9 +32,10 @@ type GalleryState = {
|
|||||||
export const initialGalleryState: GalleryState = {
|
export const initialGalleryState: GalleryState = {
|
||||||
selection: [],
|
selection: [],
|
||||||
shouldAutoSwitch: true,
|
shouldAutoSwitch: true,
|
||||||
autoAddBoardId: null,
|
autoAddBoardId: undefined,
|
||||||
galleryImageMinimumWidth: 96,
|
galleryImageMinimumWidth: 96,
|
||||||
selectedBoardId: 'images',
|
selectedBoardId: undefined,
|
||||||
|
galleryView: 'images',
|
||||||
batchImageNames: [],
|
batchImageNames: [],
|
||||||
isBatchEnabled: false,
|
isBatchEnabled: false,
|
||||||
};
|
};
|
||||||
@ -46,14 +44,8 @@ export const gallerySlice = createSlice({
|
|||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
initialState: initialGalleryState,
|
initialState: initialGalleryState,
|
||||||
reducers: {
|
reducers: {
|
||||||
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
|
||||||
// TODO: port all instances of this to use RTK Query cache
|
|
||||||
// imagesAdapter.removeMany(state, action.payload);
|
|
||||||
// state.batchImageNames = state.batchImageNames.filter(
|
|
||||||
// (name) => !action.payload.includes(name)
|
|
||||||
// );
|
|
||||||
},
|
|
||||||
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
||||||
|
// MULTI SELECT LOGIC
|
||||||
// const rangeEndImageName = action.payload;
|
// const rangeEndImageName = action.payload;
|
||||||
// const lastSelectedImage = state.selection[state.selection.length - 1];
|
// const lastSelectedImage = state.selection[state.selection.length - 1];
|
||||||
// const filteredImages = selectFilteredImagesLocal(state);
|
// const filteredImages = selectFilteredImagesLocal(state);
|
||||||
@ -74,6 +66,7 @@ export const gallerySlice = createSlice({
|
|||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
imageSelectionToggled: (state, action: PayloadAction<string>) => {
|
imageSelectionToggled: (state, action: PayloadAction<string>) => {
|
||||||
|
// MULTI SELECT LOGIC
|
||||||
// if (
|
// if (
|
||||||
// state.selection.includes(action.payload) &&
|
// state.selection.includes(action.payload) &&
|
||||||
// state.selection.length > 1
|
// state.selection.length > 1
|
||||||
@ -96,6 +89,7 @@ export const gallerySlice = createSlice({
|
|||||||
},
|
},
|
||||||
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||||
state.selectedBoardId = action.payload;
|
state.selectedBoardId = action.payload;
|
||||||
|
state.galleryView = 'images';
|
||||||
},
|
},
|
||||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isBatchEnabled = action.payload;
|
state.isBatchEnabled = action.payload;
|
||||||
@ -125,23 +119,27 @@ export const gallerySlice = createSlice({
|
|||||||
state.batchImageNames = [];
|
state.batchImageNames = [];
|
||||||
state.selection = [];
|
state.selection = [];
|
||||||
},
|
},
|
||||||
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
|
autoAddBoardIdChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<string | undefined>
|
||||||
|
) => {
|
||||||
state.autoAddBoardId = action.payload;
|
state.autoAddBoardId = action.payload;
|
||||||
},
|
},
|
||||||
|
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
|
||||||
|
state.galleryView = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
const deletedBoardId = action.meta.arg.originalArgs;
|
||||||
(state, action) => {
|
if (deletedBoardId === state.selectedBoardId) {
|
||||||
const deletedBoardId = action.meta.arg.originalArgs;
|
state.selectedBoardId = undefined;
|
||||||
if (deletedBoardId === state.selectedBoardId) {
|
state.galleryView = 'images';
|
||||||
state.selectedBoardId = 'images';
|
|
||||||
}
|
|
||||||
if (deletedBoardId === state.autoAddBoardId) {
|
|
||||||
state.autoAddBoardId = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
if (deletedBoardId === state.autoAddBoardId) {
|
||||||
|
state.autoAddBoardId = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
builder.addMatcher(
|
builder.addMatcher(
|
||||||
boardsApi.endpoints.listAllBoards.matchFulfilled,
|
boardsApi.endpoints.listAllBoards.matchFulfilled,
|
||||||
(state, action) => {
|
(state, action) => {
|
||||||
@ -151,7 +149,7 @@ export const gallerySlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
||||||
state.autoAddBoardId = null;
|
state.autoAddBoardId = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -170,6 +168,12 @@ export const {
|
|||||||
imagesAddedToBatch,
|
imagesAddedToBatch,
|
||||||
imagesRemovedFromBatch,
|
imagesRemovedFromBatch,
|
||||||
autoAddBoardIdChanged,
|
autoAddBoardIdChanged,
|
||||||
|
galleryViewChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
export default gallerySlice.reducer;
|
||||||
|
|
||||||
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
|
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||||
|
boardsApi.endpoints.deleteBoardAndImages.matchFulfilled
|
||||||
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { SYSTEM_BOARDS } from 'services/api/endpoints/images';
|
|
||||||
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
|
|
||||||
import { ImageCategory } from 'services/api/types';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { ImageCategory, ImageDTO } from 'services/api/types';
|
||||||
|
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
|
||||||
|
|
||||||
export const getCategoriesQueryParamForBoard = (
|
export const getCategoriesQueryParamForBoard = (
|
||||||
board_id: BoardId
|
board_id: BoardId
|
||||||
@ -20,16 +19,11 @@ export const getCategoriesQueryParamForBoard = (
|
|||||||
|
|
||||||
export const getBoardIdQueryParamForBoard = (
|
export const getBoardIdQueryParamForBoard = (
|
||||||
board_id: BoardId
|
board_id: BoardId
|
||||||
): string | undefined => {
|
): string | null => {
|
||||||
if (board_id === 'no_board') {
|
if (board_id === undefined) {
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// system boards besides 'no_board'
|
|
||||||
if (SYSTEM_BOARDS.includes(board_id)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// user boards
|
// user boards
|
||||||
return board_id;
|
return board_id;
|
||||||
};
|
};
|
||||||
@ -52,3 +46,10 @@ export const getBoardIdFromBoardAndCategoriesQueryParam = (
|
|||||||
|
|
||||||
return board_id ?? 'UNKNOWN_BOARD';
|
return board_id ?? 'UNKNOWN_BOARD';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCategories = (imageDTO: ImageDTO) => {
|
||||||
|
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||||
|
return IMAGE_CATEGORIES;
|
||||||
|
}
|
||||||
|
return ASSETS_CATEGORIES;
|
||||||
|
};
|
||||||
|
@ -78,7 +78,6 @@ const ParametersDrawer = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
paddingTop={1.5}
|
|
||||||
paddingBottom={4}
|
paddingBottom={4}
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
@ -164,7 +164,7 @@ const ResizableDrawer = ({
|
|||||||
sx={{
|
sx={{
|
||||||
borderColor: mode('base.200', 'base.800')(colorMode),
|
borderColor: mode('base.200', 'base.800')(colorMode),
|
||||||
p: 4,
|
p: 4,
|
||||||
bg: mode('base.100', 'base.900')(colorMode),
|
bg: mode('base.50', 'base.900')(colorMode),
|
||||||
height: 'full',
|
height: 'full',
|
||||||
shadow: isOpen ? 'dark-lg' : undefined,
|
shadow: isOpen ? 'dark-lg' : undefined,
|
||||||
...containerStyles,
|
...containerStyles,
|
||||||
|
@ -76,7 +76,7 @@ export default function FoundModelsList() {
|
|||||||
dispatch(
|
dispatch(
|
||||||
addToast(
|
addToast(
|
||||||
makeToast({
|
makeToast({
|
||||||
title: 'Faile To Add Model',
|
title: 'Failed To Add Model',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -1,52 +1,36 @@
|
|||||||
import { ImageDTO, OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
|
import { api } from '..';
|
||||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
|
||||||
import { paths } from '../schema';
|
|
||||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
|
||||||
|
|
||||||
type ListBoardImagesArg =
|
|
||||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
|
||||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
|
|
||||||
|
|
||||||
type AddImageToBoardArg =
|
|
||||||
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
|
|
||||||
|
|
||||||
type RemoveImageFromBoardArg =
|
|
||||||
paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'];
|
|
||||||
|
|
||||||
export const boardImagesApi = api.injectEndpoints({
|
export const boardImagesApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
/**
|
/**
|
||||||
* Board Images Queries
|
* Board Images Queries
|
||||||
*/
|
*/
|
||||||
|
// listBoardImages: build.query<
|
||||||
listBoardImages: build.query<
|
// OffsetPaginatedResults_ImageDTO_,
|
||||||
OffsetPaginatedResults_ImageDTO_,
|
// ListBoardImagesArg
|
||||||
ListBoardImagesArg
|
// >({
|
||||||
>({
|
// query: ({ board_id, offset, limit }) => ({
|
||||||
query: ({ board_id, offset, limit }) => ({
|
// url: `board_images/${board_id}`,
|
||||||
url: `board_images/${board_id}`,
|
// method: 'GET',
|
||||||
method: 'GET',
|
// }),
|
||||||
}),
|
// providesTags: (result, error, arg) => {
|
||||||
providesTags: (result, error, arg) => {
|
// // any list of boardimages
|
||||||
// any list of boardimages
|
// const tags: ApiFullTagDescription[] = [
|
||||||
const tags: ApiFullTagDescription[] = [
|
// { type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
|
||||||
{ type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
|
// ];
|
||||||
];
|
// if (result) {
|
||||||
|
// // and individual tags for each boardimage
|
||||||
if (result) {
|
// tags.push(
|
||||||
// and individual tags for each boardimage
|
// ...result.items.map(({ board_id, image_name }) => ({
|
||||||
tags.push(
|
// type: 'BoardImage' as const,
|
||||||
...result.items.map(({ board_id, image_name }) => ({
|
// id: `${board_id}_${image_name}`,
|
||||||
type: 'BoardImage' as const,
|
// }))
|
||||||
id: `${board_id}_${image_name}`,
|
// );
|
||||||
}))
|
// }
|
||||||
);
|
// return tags;
|
||||||
}
|
// },
|
||||||
|
// }),
|
||||||
return tags;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useListBoardImagesQuery } = boardImagesApi;
|
// export const { useListBoardImagesQuery } = boardImagesApi;
|
||||||
|
@ -109,10 +109,25 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, board_id) => [
|
||||||
{ type: 'Board', id: arg },
|
{ type: 'Board', id: LIST_TAG },
|
||||||
// invalidate the 'No Board' cache
|
// invalidate the 'No Board' cache
|
||||||
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
|
{
|
||||||
|
type: 'ImageList',
|
||||||
|
id: getListImagesUrl({
|
||||||
|
board_id: 'none',
|
||||||
|
categories: IMAGE_CATEGORIES,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ImageList',
|
||||||
|
id: getListImagesUrl({
|
||||||
|
board_id: 'none',
|
||||||
|
categories: ASSETS_CATEGORIES,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ type: 'BoardImagesTotal', id: 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||||
/**
|
/**
|
||||||
@ -167,24 +182,14 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
'listImages',
|
'listImages',
|
||||||
queryArgs,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
const oldCount = imagesAdapter
|
const oldTotal = draft.total;
|
||||||
.getSelectors()
|
|
||||||
.selectTotal(draft);
|
|
||||||
const newState = imagesAdapter.updateMany(draft, updates);
|
const newState = imagesAdapter.updateMany(draft, updates);
|
||||||
const newCount = imagesAdapter
|
const delta = newState.total - oldTotal;
|
||||||
.getSelectors()
|
draft.total = draft.total + delta;
|
||||||
.selectTotal(newState);
|
|
||||||
draft.total = Math.max(
|
|
||||||
draft.total - (oldCount - newCount),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// after deleting a board, select the 'All Images' board
|
|
||||||
dispatch(boardIdSelected('images'));
|
|
||||||
} catch {
|
} catch {
|
||||||
//no-op
|
//no-op
|
||||||
}
|
}
|
||||||
@ -197,9 +202,24 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
params: { include_images: true },
|
params: { include_images: true },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, board_id) => [
|
||||||
{ type: 'Board', id: arg },
|
{ type: 'Board', id: LIST_TAG },
|
||||||
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
|
{
|
||||||
|
type: 'ImageList',
|
||||||
|
id: getListImagesUrl({
|
||||||
|
board_id: 'none',
|
||||||
|
categories: IMAGE_CATEGORIES,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ImageList',
|
||||||
|
id: getListImagesUrl({
|
||||||
|
board_id: 'none',
|
||||||
|
categories: ASSETS_CATEGORIES,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ type: 'BoardImagesTotal', id: 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||||
/**
|
/**
|
||||||
@ -231,27 +251,17 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
'listImages',
|
'listImages',
|
||||||
queryArgs,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
const oldCount = imagesAdapter
|
const oldTotal = draft.total;
|
||||||
.getSelectors()
|
|
||||||
.selectTotal(draft);
|
|
||||||
const newState = imagesAdapter.removeMany(
|
const newState = imagesAdapter.removeMany(
|
||||||
draft,
|
draft,
|
||||||
deleted_images
|
deleted_images
|
||||||
);
|
);
|
||||||
const newCount = imagesAdapter
|
const delta = newState.total - oldTotal;
|
||||||
.getSelectors()
|
draft.total = draft.total + delta;
|
||||||
.selectTotal(newState);
|
|
||||||
draft.total = Math.max(
|
|
||||||
draft.total - (oldCount - newCount),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// after deleting a board, select the 'All Images' board
|
|
||||||
dispatch(boardIdSelected('images'));
|
|
||||||
} catch {
|
} catch {
|
||||||
//no-op
|
//no-op
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,17 @@ import {
|
|||||||
BoardId,
|
BoardId,
|
||||||
IMAGE_CATEGORIES,
|
IMAGE_CATEGORIES,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { omit } from 'lodash-es';
|
import { getCategories } from 'features/gallery/store/util';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { ApiFullTagDescription, api } from '..';
|
import { ApiFullTagDescription, api } from '..';
|
||||||
import { components, paths } from '../schema';
|
import { components, paths } from '../schema';
|
||||||
import {
|
import {
|
||||||
ImageCategory,
|
ImageCategory,
|
||||||
ImageChanges,
|
|
||||||
ImageDTO,
|
ImageDTO,
|
||||||
OffsetPaginatedResults_ImageDTO_,
|
OffsetPaginatedResults_ImageDTO_,
|
||||||
PostUploadAction,
|
PostUploadAction,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getCacheAction } from './util';
|
import { getIsImageInDateRange } from './util';
|
||||||
|
|
||||||
export type ListImagesArgs = NonNullable<
|
export type ListImagesArgs = NonNullable<
|
||||||
paths['/api/v1/images/']['get']['parameters']['query']
|
paths['/api/v1/images/']['get']['parameters']['query']
|
||||||
@ -51,8 +50,6 @@ export const imagesSelectors = imagesAdapter.getSelectors();
|
|||||||
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
|
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
|
||||||
`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
|
`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
|
||||||
|
|
||||||
export const SYSTEM_BOARDS = ['images', 'assets', 'no_board', 'batch'];
|
|
||||||
|
|
||||||
export const imagesApi = api.injectEndpoints({
|
export const imagesApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
/**
|
/**
|
||||||
@ -155,6 +152,42 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
},
|
},
|
||||||
keepUnusedDataFor: 86400, // 24 hours
|
keepUnusedDataFor: 86400, // 24 hours
|
||||||
}),
|
}),
|
||||||
|
getBoardImagesTotal: build.query<number, string | undefined>({
|
||||||
|
query: (board_id) => ({
|
||||||
|
url: getListImagesUrl({
|
||||||
|
board_id: board_id ?? 'none',
|
||||||
|
categories: IMAGE_CATEGORIES,
|
||||||
|
is_intermediate: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
providesTags: (result, error, arg) => [
|
||||||
|
{ type: 'BoardImagesTotal', id: arg ?? 'none' },
|
||||||
|
],
|
||||||
|
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||||
|
return response.total;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getBoardAssetsTotal: build.query<number, string | undefined>({
|
||||||
|
query: (board_id) => ({
|
||||||
|
url: getListImagesUrl({
|
||||||
|
board_id: board_id ?? 'none',
|
||||||
|
categories: ASSETS_CATEGORIES,
|
||||||
|
is_intermediate: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
providesTags: (result, error, arg) => [
|
||||||
|
{ type: 'BoardAssetsTotal', id: arg ?? 'none' },
|
||||||
|
],
|
||||||
|
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||||
|
return response.total;
|
||||||
|
},
|
||||||
|
}),
|
||||||
clearIntermediates: build.mutation<number, void>({
|
clearIntermediates: build.mutation<number, void>({
|
||||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||||
invalidatesTags: ['IntermediatesCount'],
|
invalidatesTags: ['IntermediatesCount'],
|
||||||
@ -164,56 +197,42 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
url: `images/${image_name}`,
|
url: `images/${image_name}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, { board_id }) => [
|
||||||
{ type: 'Image', id: arg.image_name },
|
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
|
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `deleteImage`:
|
* Cache changes for `deleteImage`:
|
||||||
* - *remove* from "All Images" / "All Assets"
|
* - NOT POSSIBLE: *remove* from getImageDTO
|
||||||
* - IF it has a board:
|
* - $cache = [board_id|no_board]/[images|assets]
|
||||||
* - THEN *remove* from it's own board
|
* - *remove* from $cache
|
||||||
* - ELSE *remove* from "No Board"
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { image_name, board_id, image_category } = imageDTO;
|
const { image_name, board_id } = imageDTO;
|
||||||
|
|
||||||
// Figure out the `listImages` caches that we need to update
|
// Store patches so we can undo if the query fails
|
||||||
// That means constructing the possible query args that are serialized into the cache key...
|
const patches: PatchCollection[] = [];
|
||||||
|
|
||||||
const removeFromCacheKeys: ListImagesArgs[] = [];
|
|
||||||
|
|
||||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
// $cache = [board_id|no_board]/[images|assets]
|
||||||
? IMAGE_CATEGORIES
|
const categories = getCategories(imageDTO);
|
||||||
: ASSETS_CATEGORIES;
|
|
||||||
|
|
||||||
// remove from "All Images"
|
// *remove* from $cache
|
||||||
removeFromCacheKeys.push({ categories });
|
patches.push(
|
||||||
|
dispatch(
|
||||||
if (board_id) {
|
imagesApi.util.updateQueryData(
|
||||||
// remove from it's own board
|
'listImages',
|
||||||
removeFromCacheKeys.push({ board_id });
|
{ board_id: board_id ?? 'none', categories },
|
||||||
} else {
|
(draft) => {
|
||||||
// remove from "No Board"
|
const oldTotal = draft.total;
|
||||||
removeFromCacheKeys.push({ board_id: 'none' });
|
const newState = imagesAdapter.removeOne(draft, image_name);
|
||||||
}
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
const patches: PatchCollection[] = [];
|
}
|
||||||
removeFromCacheKeys.forEach((cacheKey) => {
|
|
||||||
patches.push(
|
|
||||||
dispatch(
|
|
||||||
imagesApi.util.updateQueryData(
|
|
||||||
'listImages',
|
|
||||||
cacheKey,
|
|
||||||
(draft) => {
|
|
||||||
imagesAdapter.removeOne(draft, image_name);
|
|
||||||
draft.total = Math.max(draft.total - 1, 0);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
});
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryFulfilled;
|
await queryFulfilled;
|
||||||
@ -222,122 +241,169 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
updateImage: build.mutation<
|
/**
|
||||||
|
* Change an image's `is_intermediate` property.
|
||||||
|
*/
|
||||||
|
changeImageIsIntermediate: build.mutation<
|
||||||
ImageDTO,
|
ImageDTO,
|
||||||
{
|
{ imageDTO: ImageDTO; is_intermediate: boolean }
|
||||||
imageDTO: ImageDTO;
|
|
||||||
// For now, we will not allow image categories to change
|
|
||||||
changes: Omit<ImageChanges, 'image_category'>;
|
|
||||||
}
|
|
||||||
>({
|
>({
|
||||||
query: ({ imageDTO, changes }) => ({
|
query: ({ imageDTO, is_intermediate }) => ({
|
||||||
url: `images/${imageDTO.image_name}`,
|
url: `images/${imageDTO.image_name}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: changes,
|
body: { is_intermediate },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, { imageDTO }) => [
|
invalidatesTags: (result, error, { imageDTO }) => [
|
||||||
{ type: 'Image', id: imageDTO.image_name },
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ imageDTO: oldImageDTO, changes: _changes },
|
{ imageDTO, is_intermediate },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled, getState }
|
||||||
) {
|
) {
|
||||||
// let's be extra-sure we do not accidentally change categories
|
|
||||||
const changes = omit(_changes, 'image_category');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache changes for "updateImage":
|
* Cache changes for `changeImageIsIntermediate`:
|
||||||
* - *update* "getImageDTO" cache
|
* - *update* getImageDTO
|
||||||
* - for "All Images" || "All Assets":
|
* - $cache = [board_id|no_board]/[images|assets]
|
||||||
* - IF it is not already in the cache
|
* - IF it is being changed to an intermediate:
|
||||||
* - THEN *add* it to "All Images" / "All Assets" and update the total
|
* - remove from $cache
|
||||||
* - ELSE *update* it
|
* - ELSE (it is being changed to a non-intermediate):
|
||||||
* - IF the image has a board:
|
* - IF it eligible for insertion into existing $cache:
|
||||||
* - THEN *update* it's own board
|
* - *upsert* to $cache
|
||||||
* - ELSE *update* the "No Board" board
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Store patches so we can undo if the query fails
|
||||||
const patches: PatchCollection[] = [];
|
const patches: PatchCollection[] = [];
|
||||||
const { image_name, board_id, image_category, is_intermediate } =
|
|
||||||
oldImageDTO;
|
|
||||||
|
|
||||||
const isChangingFromIntermediate = changes.is_intermediate === false;
|
// *update* getImageDTO
|
||||||
// do not add intermediates to gallery cache
|
|
||||||
if (is_intermediate && !isChangingFromIntermediate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
|
||||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
|
||||||
? IMAGE_CATEGORIES
|
|
||||||
: ASSETS_CATEGORIES;
|
|
||||||
|
|
||||||
// update `getImageDTO` cache
|
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'getImageDTO',
|
'getImageDTO',
|
||||||
image_name,
|
imageDTO.image_name,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
Object.assign(draft, changes);
|
Object.assign(draft, { is_intermediate });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the "All Image" or "All Assets" board
|
// $cache = [board_id|no_board]/[images|assets]
|
||||||
const queryArgsToUpdate: ListImagesArgs[] = [{ categories }];
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
// IF the image has a board:
|
if (is_intermediate) {
|
||||||
if (board_id) {
|
// IF it is being changed to an intermediate:
|
||||||
// THEN update it's own board
|
// remove from $cache
|
||||||
queryArgsToUpdate.push({ board_id });
|
patches.push(
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{ board_id: imageDTO.board_id ?? 'none', categories },
|
||||||
|
(draft) => {
|
||||||
|
const oldTotal = draft.total;
|
||||||
|
const newState = imagesAdapter.removeOne(
|
||||||
|
draft,
|
||||||
|
imageDTO.image_name
|
||||||
|
);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// ELSE update the "No Board" board
|
// ELSE (it is being changed to a non-intermediate):
|
||||||
queryArgsToUpdate.push({ board_id: 'none' });
|
console.log(imageDTO);
|
||||||
}
|
const queryArgs = {
|
||||||
|
board_id: imageDTO.board_id ?? 'none',
|
||||||
|
categories,
|
||||||
|
};
|
||||||
|
|
||||||
queryArgsToUpdate.forEach((queryArg) => {
|
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||||
const { data } = imagesApi.endpoints.listImages.select(queryArg)(
|
|
||||||
getState()
|
getState()
|
||||||
);
|
);
|
||||||
|
|
||||||
const cacheAction = getCacheAction(data, oldImageDTO);
|
// IF it eligible for insertion into existing $cache
|
||||||
|
// "eligible" means either:
|
||||||
|
// - The cache is fully populated, with all images in the db cached
|
||||||
|
// OR
|
||||||
|
// - The image's `created_at` is within the range of the cached images
|
||||||
|
|
||||||
if (['update', 'add'].includes(cacheAction)) {
|
const isCacheFullyPopulated =
|
||||||
|
currentCache.data &&
|
||||||
|
currentCache.data.ids.length >= currentCache.data.total;
|
||||||
|
|
||||||
|
const isInDateRange = getIsImageInDateRange(
|
||||||
|
currentCache.data,
|
||||||
|
imageDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCacheFullyPopulated || isInDateRange) {
|
||||||
|
// *upsert* to $cache
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
queryArg,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
// One of the common changes is to make a canvas intermediate a non-intermediate,
|
const oldTotal = draft.total;
|
||||||
// i.e. save a canvas image to the gallery.
|
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||||
// If that was the change, need to add the image to the cache instead of updating
|
const delta = newState.total - oldTotal;
|
||||||
// the existing cache entry.
|
draft.total = draft.total + delta;
|
||||||
if (
|
|
||||||
changes.is_intermediate === false ||
|
|
||||||
cacheAction === 'add'
|
|
||||||
) {
|
|
||||||
// add it to the cache
|
|
||||||
imagesAdapter.addOne(draft, {
|
|
||||||
...oldImageDTO,
|
|
||||||
...changes,
|
|
||||||
});
|
|
||||||
draft.total += 1;
|
|
||||||
} else if (cacheAction === 'update') {
|
|
||||||
// just update it
|
|
||||||
imagesAdapter.updateOne(draft, {
|
|
||||||
id: image_name,
|
|
||||||
changes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
} catch {
|
||||||
|
patches.forEach((patchResult) => patchResult.undo());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Change an image's `session_id` association.
|
||||||
|
*/
|
||||||
|
changeImageSessionId: build.mutation<
|
||||||
|
ImageDTO,
|
||||||
|
{ imageDTO: ImageDTO; session_id: string }
|
||||||
|
>({
|
||||||
|
query: ({ imageDTO, session_id }) => ({
|
||||||
|
url: `images/${imageDTO.image_name}`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { session_id },
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, { imageDTO }) => [
|
||||||
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
],
|
||||||
|
async onQueryStarted(
|
||||||
|
{ imageDTO, session_id },
|
||||||
|
{ dispatch, queryFulfilled, getState }
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Cache changes for `changeImageSessionId`:
|
||||||
|
* - *update* getImageDTO
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Store patches so we can undo if the query fails
|
||||||
|
const patches: PatchCollection[] = [];
|
||||||
|
|
||||||
|
// *update* getImageDTO
|
||||||
|
patches.push(
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'getImageDTO',
|
||||||
|
imageDTO.image_name,
|
||||||
|
(draft) => {
|
||||||
|
Object.assign(draft, { session_id });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryFulfilled;
|
await queryFulfilled;
|
||||||
@ -354,9 +420,18 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
is_intermediate: boolean;
|
is_intermediate: boolean;
|
||||||
postUploadAction?: PostUploadAction;
|
postUploadAction?: PostUploadAction;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
|
board_id?: string;
|
||||||
|
crop_visible?: boolean;
|
||||||
}
|
}
|
||||||
>({
|
>({
|
||||||
query: ({ file, image_category, is_intermediate, session_id }) => {
|
query: ({
|
||||||
|
file,
|
||||||
|
image_category,
|
||||||
|
is_intermediate,
|
||||||
|
session_id,
|
||||||
|
board_id,
|
||||||
|
crop_visible,
|
||||||
|
}) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return {
|
return {
|
||||||
@ -367,14 +442,32 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
image_category,
|
image_category,
|
||||||
is_intermediate,
|
is_intermediate,
|
||||||
session_id,
|
session_id,
|
||||||
|
board_id,
|
||||||
|
crop_visible,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ file, image_category, is_intermediate, postUploadAction },
|
{
|
||||||
|
file,
|
||||||
|
image_category,
|
||||||
|
is_intermediate,
|
||||||
|
postUploadAction,
|
||||||
|
session_id,
|
||||||
|
board_id,
|
||||||
|
},
|
||||||
{ dispatch, queryFulfilled }
|
{ dispatch, queryFulfilled }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
/**
|
||||||
|
* NOTE: PESSIMISTIC UPDATE
|
||||||
|
* Cache changes for `uploadImage`:
|
||||||
|
* - IF the image is an intermediate:
|
||||||
|
* - BAIL OUT
|
||||||
|
* - *add* to `getImageDTO`
|
||||||
|
* - *add* to no_board/assets
|
||||||
|
*/
|
||||||
|
|
||||||
const { data: imageDTO } = await queryFulfilled;
|
const { data: imageDTO } = await queryFulfilled;
|
||||||
|
|
||||||
if (imageDTO.is_intermediate) {
|
if (imageDTO.is_intermediate) {
|
||||||
@ -382,21 +475,42 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
// *add* to `getImageDTO`
|
||||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
dispatch(
|
||||||
? IMAGE_CATEGORIES
|
imagesApi.util.upsertQueryData(
|
||||||
: ASSETS_CATEGORIES;
|
'getImageDTO',
|
||||||
|
imageDTO.image_name,
|
||||||
|
imageDTO
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const queryArg = { categories };
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
|
// *add* to no_board/assets
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{
|
||||||
|
board_id: imageDTO.board_id ?? 'none',
|
||||||
|
categories,
|
||||||
|
},
|
||||||
|
(draft) => {
|
||||||
|
const oldTotal = draft.total;
|
||||||
|
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
|
imagesApi.util.invalidateTags([
|
||||||
imagesAdapter.addOne(draft, imageDTO);
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
draft.total = draft.total + 1;
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
})
|
])
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// no-op
|
// query failed, no action needed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -412,102 +526,102 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
body: { board_id, image_name },
|
body: { board_id, image_name },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, { board_id, imageDTO }) => [
|
||||||
{ type: 'BoardImage' },
|
{ type: 'Board', id: board_id },
|
||||||
{ type: 'Board', id: arg.board_id },
|
{ type: 'BoardImagesTotal', id: board_id },
|
||||||
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: board_id },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ board_id, imageDTO: oldImageDTO },
|
{ board_id, imageDTO },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled, getState }
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `addImageToBoard`:
|
* Cache changes for `addImageToBoard`:
|
||||||
* - *update* the `getImageDTO` cache
|
* - *update* getImageDTO
|
||||||
* - *remove* from "No Board"
|
* - IF it is intermediate:
|
||||||
* - IF the image has an old `board_id`:
|
* - BAIL OUT ON FURTHER CHANGES
|
||||||
* - THEN *remove* from it's old `board_id`
|
* - IF it has an old board_id:
|
||||||
* - IF the image's `created_at` is within the range of the board's cached images
|
* - THEN *remove* from old board_id/[images|assets]
|
||||||
* - OR the board cache has length of 0 or 1
|
* - ELSE *remove* from no_board/[images|assets]
|
||||||
* - THEN *add* it to new `board_id`
|
* - $cache = board_id/[images|assets]
|
||||||
|
* - IF it eligible for insertion into existing $cache:
|
||||||
|
* - THEN *add* to $cache
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { image_name, board_id: old_board_id } = oldImageDTO;
|
|
||||||
|
|
||||||
// Figure out the `listImages` caches that we need to update
|
|
||||||
const removeFromQueryArgs: ListImagesArgs[] = [];
|
|
||||||
|
|
||||||
// remove from "No Board"
|
|
||||||
removeFromQueryArgs.push({ board_id: 'none' });
|
|
||||||
|
|
||||||
// remove from old board
|
|
||||||
if (old_board_id) {
|
|
||||||
removeFromQueryArgs.push({ board_id: old_board_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store all patch results in case we need to roll back
|
|
||||||
const patches: PatchCollection[] = [];
|
const patches: PatchCollection[] = [];
|
||||||
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
// Updated imageDTO with new board_id
|
// *update* getImageDTO
|
||||||
const newImageDTO = { ...oldImageDTO, board_id };
|
|
||||||
|
|
||||||
// Update getImageDTO cache
|
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'getImageDTO',
|
'getImageDTO',
|
||||||
image_name,
|
imageDTO.image_name,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
Object.assign(draft, newImageDTO);
|
Object.assign(draft, { board_id });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Do the "Remove from" cache updates
|
if (!imageDTO.is_intermediate) {
|
||||||
removeFromQueryArgs.forEach((queryArgs) => {
|
// *remove* from [no_board|board_id]/[images|assets]
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
queryArgs,
|
{
|
||||||
|
board_id: imageDTO.board_id ?? 'none',
|
||||||
|
categories,
|
||||||
|
},
|
||||||
(draft) => {
|
(draft) => {
|
||||||
// sanity check
|
const oldTotal = draft.total;
|
||||||
if (draft.ids.includes(image_name)) {
|
const newState = imagesAdapter.removeOne(
|
||||||
imagesAdapter.removeOne(draft, image_name);
|
draft,
|
||||||
draft.total = Math.max(draft.total - 1, 0);
|
imageDTO.image_name
|
||||||
}
|
);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// We only need to add to the cache if the board is not a system board
|
// $cache = board_id/[images|assets]
|
||||||
if (!SYSTEM_BOARDS.includes(board_id)) {
|
const queryArgs = { board_id: board_id ?? 'none', categories };
|
||||||
const queryArgs = { board_id };
|
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(
|
|
||||||
getState()
|
getState()
|
||||||
);
|
);
|
||||||
|
|
||||||
const cacheAction = getCacheAction(data, oldImageDTO);
|
// IF it eligible for insertion into existing $cache
|
||||||
|
// "eligible" means either:
|
||||||
|
// - The cache is fully populated, with all images in the db cached
|
||||||
|
// OR
|
||||||
|
// - The image's `created_at` is within the range of the cached images
|
||||||
|
|
||||||
if (['add', 'update'].includes(cacheAction)) {
|
const isCacheFullyPopulated =
|
||||||
// Do the "Add to" cache updates
|
currentCache.data &&
|
||||||
|
currentCache.data.ids.length >= currentCache.data.total;
|
||||||
|
|
||||||
|
const isInDateRange = getIsImageInDateRange(
|
||||||
|
currentCache.data,
|
||||||
|
imageDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCacheFullyPopulated || isInDateRange) {
|
||||||
|
// THEN *add* to $cache
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
queryArgs,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
if (cacheAction === 'add') {
|
const oldTotal = draft.total;
|
||||||
imagesAdapter.addOne(draft, newImageDTO);
|
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||||
draft.total += 1;
|
const delta = newState.total - oldTotal;
|
||||||
} else {
|
draft.total = draft.total + delta;
|
||||||
imagesAdapter.updateOne(draft, {
|
|
||||||
id: image_name,
|
|
||||||
changes: { board_id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -531,87 +645,97 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
body: { board_id, image_name },
|
body: { board_id, image_name },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, { imageDTO }) => [
|
||||||
{ type: 'BoardImage' },
|
{ type: 'Board', id: imageDTO.board_id },
|
||||||
{ type: 'Board', id: arg.imageDTO.board_id },
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id },
|
||||||
|
{ type: 'BoardImagesTotal', id: 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id },
|
||||||
|
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ imageDTO },
|
{ imageDTO },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled, getState }
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `removeImageFromBoard`:
|
* Cache changes for removeImageFromBoard:
|
||||||
* - *update* `getImageDTO`
|
* - *update* getImageDTO
|
||||||
* - IF the image's `created_at` is within the range of the board's cached images
|
* - *remove* from board_id/[images|assets]
|
||||||
* - THEN *add* to "No Board"
|
* - $cache = no_board/[images|assets]
|
||||||
* - *remove* from `old_board_id`
|
* - IF it eligible for insertion into existing $cache:
|
||||||
|
* - THEN *upsert* to $cache
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { image_name, board_id: old_board_id } = imageDTO;
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
const patches: PatchCollection[] = [];
|
const patches: PatchCollection[] = [];
|
||||||
|
|
||||||
// Updated imageDTO with new board_id
|
// *update* getImageDTO
|
||||||
const newImageDTO = { ...imageDTO, board_id: undefined };
|
|
||||||
|
|
||||||
// Update getImageDTO cache
|
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'getImageDTO',
|
'getImageDTO',
|
||||||
image_name,
|
imageDTO.image_name,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
Object.assign(draft, newImageDTO);
|
Object.assign(draft, { board_id: undefined });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove from old board
|
// *remove* from board_id/[images|assets]
|
||||||
if (old_board_id) {
|
patches.push(
|
||||||
const oldBoardQueryArgs = { board_id: old_board_id };
|
dispatch(
|
||||||
patches.push(
|
imagesApi.util.updateQueryData(
|
||||||
dispatch(
|
'listImages',
|
||||||
imagesApi.util.updateQueryData(
|
{
|
||||||
'listImages',
|
board_id: imageDTO.board_id ?? 'none',
|
||||||
oldBoardQueryArgs,
|
categories,
|
||||||
(draft) => {
|
},
|
||||||
// sanity check
|
(draft) => {
|
||||||
if (draft.ids.includes(image_name)) {
|
const oldTotal = draft.total;
|
||||||
imagesAdapter.removeOne(draft, image_name);
|
const newState = imagesAdapter.removeOne(
|
||||||
draft.total = Math.max(draft.total - 1, 0);
|
draft,
|
||||||
}
|
imageDTO.image_name
|
||||||
}
|
);
|
||||||
)
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
// Add to "No Board"
|
// $cache = no_board/[images|assets]
|
||||||
const noBoardQueryArgs = { board_id: 'none' };
|
const queryArgs = { board_id: 'none', categories };
|
||||||
const { data } = imagesApi.endpoints.listImages.select(
|
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||||
noBoardQueryArgs
|
getState()
|
||||||
)(getState());
|
);
|
||||||
|
|
||||||
// Check if we need to make any cache changes
|
// IF it eligible for insertion into existing $cache
|
||||||
const cacheAction = getCacheAction(data, imageDTO);
|
// "eligible" means either:
|
||||||
|
// - The cache is fully populated, with all images in the db cached
|
||||||
|
// OR
|
||||||
|
// - The image's `created_at` is within the range of the cached images
|
||||||
|
|
||||||
if (['add', 'update'].includes(cacheAction)) {
|
const isCacheFullyPopulated =
|
||||||
|
currentCache.data &&
|
||||||
|
currentCache.data.ids.length >= currentCache.data.total;
|
||||||
|
|
||||||
|
const isInDateRange = getIsImageInDateRange(
|
||||||
|
currentCache.data,
|
||||||
|
imageDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCacheFullyPopulated || isInDateRange) {
|
||||||
|
// THEN *upsert* to $cache
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
noBoardQueryArgs,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
if (cacheAction === 'add') {
|
const oldTotal = draft.total;
|
||||||
imagesAdapter.addOne(draft, imageDTO);
|
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||||
draft.total += 1;
|
const delta = newState.total - oldTotal;
|
||||||
} else {
|
draft.total = draft.total + delta;
|
||||||
imagesAdapter.updateOne(draft, {
|
|
||||||
id: image_name,
|
|
||||||
changes: { board_id: undefined },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -635,7 +759,8 @@ export const {
|
|||||||
useGetImageDTOQuery,
|
useGetImageDTOQuery,
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataQuery,
|
||||||
useDeleteImageMutation,
|
useDeleteImageMutation,
|
||||||
useUpdateImageMutation,
|
useGetBoardImagesTotalQuery,
|
||||||
|
useGetBoardAssetsTotalQuery,
|
||||||
useUploadImageMutation,
|
useUploadImageMutation,
|
||||||
useAddImageToBoardMutation,
|
useAddImageToBoardMutation,
|
||||||
useRemoveImageFromBoardMutation,
|
useRemoveImageFromBoardMutation,
|
||||||
|
@ -25,27 +25,27 @@ export const getIsImageInDateRange = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Determines the action we should take when an image may need to be added or updated in a cache.
|
// * Determines the action we should take when an image may need to be added or updated in a cache.
|
||||||
*/
|
// */
|
||||||
export const getCacheAction = (
|
// export const getCacheAction = (
|
||||||
data: ImageCache | undefined,
|
// data: ImageCache | undefined,
|
||||||
imageDTO: ImageDTO
|
// imageDTO: ImageDTO
|
||||||
): 'add' | 'update' | 'none' => {
|
// ): 'add' | 'update' | 'none' => {
|
||||||
const isInDateRange = getIsImageInDateRange(data, imageDTO);
|
// const isInDateRange = getIsImageInDateRange(data, imageDTO);
|
||||||
const isCacheFullyPopulated = data && data.total === data.ids.length;
|
// const isCacheFullyPopulated = data && data.total === data.ids.length;
|
||||||
const shouldUpdateCache =
|
// const shouldUpdateCache =
|
||||||
Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
|
// Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
|
||||||
|
|
||||||
const isImageInCache = data && data.ids.includes(imageDTO.image_name);
|
// const isImageInCache = data && data.ids.includes(imageDTO.image_name);
|
||||||
|
|
||||||
if (shouldUpdateCache && isImageInCache) {
|
// if (shouldUpdateCache && isImageInCache) {
|
||||||
return 'update';
|
// return 'update';
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (shouldUpdateCache && !isImageInCache) {
|
// if (shouldUpdateCache && !isImageInCache) {
|
||||||
return 'add';
|
// return 'add';
|
||||||
}
|
// }
|
||||||
|
|
||||||
return 'none';
|
// return 'none';
|
||||||
};
|
// };
|
||||||
|
@ -4,19 +4,8 @@ import { useListAllBoardsQuery } from '../endpoints/boards';
|
|||||||
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
||||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||||
selectFromResult: ({ data }) => {
|
selectFromResult: ({ data }) => {
|
||||||
let boardName = '';
|
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||||
if (board_id === 'images') {
|
const boardName = selectedBoard?.board_name || 'Uncategorized';
|
||||||
boardName = 'Images';
|
|
||||||
} else if (board_id === 'assets') {
|
|
||||||
boardName = 'Assets';
|
|
||||||
} else if (board_id === 'no_board') {
|
|
||||||
boardName = 'No Board';
|
|
||||||
} else if (board_id === 'batch') {
|
|
||||||
boardName = 'Batch';
|
|
||||||
} else {
|
|
||||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
|
||||||
boardName = selectedBoard?.board_name || 'Unknown Board';
|
|
||||||
}
|
|
||||||
|
|
||||||
return { boardName };
|
return { boardName };
|
||||||
},
|
},
|
||||||
|
@ -1,53 +1,21 @@
|
|||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||||
ASSETS_CATEGORIES,
|
|
||||||
BoardId,
|
|
||||||
IMAGE_CATEGORIES,
|
|
||||||
INITIAL_IMAGE_LIMIT,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ListImagesArgs, useListImagesQuery } from '../endpoints/images';
|
import {
|
||||||
|
useGetBoardAssetsTotalQuery,
|
||||||
|
useGetBoardImagesTotalQuery,
|
||||||
|
} from '../endpoints/images';
|
||||||
|
|
||||||
const baseQueryArg: ListImagesArgs = {
|
export const useBoardTotal = (board_id: BoardId) => {
|
||||||
offset: 0,
|
const galleryView = useAppSelector((state) => state.gallery.galleryView);
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
|
||||||
is_intermediate: false,
|
const { data: totalImages } = useGetBoardImagesTotalQuery(board_id);
|
||||||
};
|
const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
|
||||||
|
|
||||||
const imagesQueryArg: ListImagesArgs = {
|
const currentViewTotal = useMemo(
|
||||||
categories: IMAGE_CATEGORIES,
|
() => (galleryView === 'images' ? totalImages : totalAssets),
|
||||||
...baseQueryArg,
|
[galleryView, totalAssets, totalImages]
|
||||||
};
|
);
|
||||||
|
|
||||||
const assetsQueryArg: ListImagesArgs = {
|
return { totalImages, totalAssets, currentViewTotal };
|
||||||
categories: ASSETS_CATEGORIES,
|
|
||||||
...baseQueryArg,
|
|
||||||
};
|
|
||||||
|
|
||||||
const noBoardQueryArg: ListImagesArgs = {
|
|
||||||
board_id: 'none',
|
|
||||||
...baseQueryArg,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBoardTotal = (board_id: BoardId | null | undefined) => {
|
|
||||||
const queryArg = useMemo(() => {
|
|
||||||
if (!board_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (board_id === 'images') {
|
|
||||||
return imagesQueryArg;
|
|
||||||
} else if (board_id === 'assets') {
|
|
||||||
return assetsQueryArg;
|
|
||||||
} else if (board_id === 'no_board') {
|
|
||||||
return noBoardQueryArg;
|
|
||||||
} else {
|
|
||||||
return { board_id, ...baseQueryArg };
|
|
||||||
}
|
|
||||||
}, [board_id]);
|
|
||||||
|
|
||||||
const { total } = useListImagesQuery(queryArg ?? skipToken, {
|
|
||||||
selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return total;
|
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,8 @@ import { $authToken, $baseUrl } from 'services/api/client';
|
|||||||
|
|
||||||
export const tagTypes = [
|
export const tagTypes = [
|
||||||
'Board',
|
'Board',
|
||||||
|
'BoardImagesTotal',
|
||||||
|
'BoardAssetsTotal',
|
||||||
'Image',
|
'Image',
|
||||||
'ImageNameList',
|
'ImageNameList',
|
||||||
'ImageList',
|
'ImageList',
|
||||||
|
@ -255,6 +255,18 @@ export type paths = {
|
|||||||
/** Get Config */
|
/** Get Config */
|
||||||
get: operations["get_config"];
|
get: operations["get_config"];
|
||||||
};
|
};
|
||||||
|
"/api/v1/app/logging": {
|
||||||
|
/**
|
||||||
|
* Get Log Level
|
||||||
|
* @description Returns the log level
|
||||||
|
*/
|
||||||
|
get: operations["get_log_level"];
|
||||||
|
/**
|
||||||
|
* Set Log Level
|
||||||
|
* @description Sets the log verbosity level
|
||||||
|
*/
|
||||||
|
post: operations["set_log_level"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
@ -1293,7 +1305,7 @@ export type components = {
|
|||||||
* @description The nodes in this graph
|
* @description The nodes in this graph
|
||||||
*/
|
*/
|
||||||
nodes?: {
|
nodes?: {
|
||||||
[key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
|
[key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Edges
|
* Edges
|
||||||
@ -1336,7 +1348,7 @@ export type components = {
|
|||||||
* @description The results of node executions
|
* @description The results of node executions
|
||||||
*/
|
*/
|
||||||
results: {
|
results: {
|
||||||
[key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
|
[key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Errors
|
* Errors
|
||||||
@ -2910,6 +2922,12 @@ export type components = {
|
|||||||
*/
|
*/
|
||||||
image?: components["schemas"]["ImageField"];
|
image?: components["schemas"]["ImageField"];
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* LogLevel
|
||||||
|
* @description An enumeration.
|
||||||
|
* @enum {integer}
|
||||||
|
*/
|
||||||
|
LogLevel: 0 | 10 | 20 | 30 | 40 | 50;
|
||||||
/** LoraInfo */
|
/** LoraInfo */
|
||||||
LoraInfo: {
|
LoraInfo: {
|
||||||
/**
|
/**
|
||||||
@ -5338,11 +5356,11 @@ export type components = {
|
|||||||
image?: components["schemas"]["ImageField"];
|
image?: components["schemas"]["ImageField"];
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* StableDiffusion1ModelFormat
|
* StableDiffusion2ModelFormat
|
||||||
* @description An enumeration.
|
* @description An enumeration.
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||||
/**
|
/**
|
||||||
* StableDiffusionXLModelFormat
|
* StableDiffusionXLModelFormat
|
||||||
* @description An enumeration.
|
* @description An enumeration.
|
||||||
@ -5350,11 +5368,11 @@ export type components = {
|
|||||||
*/
|
*/
|
||||||
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
||||||
/**
|
/**
|
||||||
* StableDiffusion2ModelFormat
|
* StableDiffusion1ModelFormat
|
||||||
* @description An enumeration.
|
* @description An enumeration.
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
parameters: never;
|
parameters: never;
|
||||||
@ -5465,7 +5483,7 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
@ -5502,7 +5520,7 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
@ -6028,8 +6046,12 @@ export type operations = {
|
|||||||
image_category: components["schemas"]["ImageCategory"];
|
image_category: components["schemas"]["ImageCategory"];
|
||||||
/** @description Whether this is an intermediate image */
|
/** @description Whether this is an intermediate image */
|
||||||
is_intermediate: boolean;
|
is_intermediate: boolean;
|
||||||
|
/** @description The board to add this image to, if any */
|
||||||
|
board_id?: string;
|
||||||
/** @description The session ID associated with this upload, if any */
|
/** @description The session ID associated with this upload, if any */
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
|
/** @description Whether to crop the image */
|
||||||
|
crop_visible?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
@ -6500,4 +6522,43 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Get Log Level
|
||||||
|
* @description Returns the log level
|
||||||
|
*/
|
||||||
|
get_log_level: {
|
||||||
|
responses: {
|
||||||
|
/** @description The operation was successful */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["LogLevel"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Set Log Level
|
||||||
|
* @description Sets the log verbosity level
|
||||||
|
*/
|
||||||
|
set_log_level: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["LogLevel"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description The operation was successful */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["LogLevel"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -64,9 +64,23 @@ const invokeAI = defineStyle((props) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invokeAIOutline = defineStyle((props) => {
|
||||||
|
const { colorScheme: c } = props;
|
||||||
|
const borderColor = mode(`gray.200`, `whiteAlpha.300`)(props);
|
||||||
|
return {
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: c === 'gray' ? borderColor : 'currentColor',
|
||||||
|
'.chakra-button__group[data-attached][data-orientation=horizontal] > &:not(:last-of-type)':
|
||||||
|
{ marginEnd: '-1px' },
|
||||||
|
'.chakra-button__group[data-attached][data-orientation=vertical] > &:not(:last-of-type)':
|
||||||
|
{ marginBottom: '-1px' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const buttonTheme = defineStyleConfig({
|
export const buttonTheme = defineStyleConfig({
|
||||||
variants: {
|
variants: {
|
||||||
invokeAI,
|
invokeAI,
|
||||||
|
invokeAIOutline,
|
||||||
},
|
},
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
variant: 'invokeAI',
|
variant: 'invokeAI',
|
||||||
|
@ -78,12 +78,12 @@ export const theme: ThemeOverride = {
|
|||||||
hoverSelected: {
|
hoverSelected: {
|
||||||
light:
|
light:
|
||||||
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-500)',
|
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-500)',
|
||||||
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-300)',
|
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-400)',
|
||||||
},
|
},
|
||||||
hoverUnselected: {
|
hoverUnselected: {
|
||||||
light:
|
light:
|
||||||
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-200)',
|
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 3px var(--invokeai-colors-accent-500)',
|
||||||
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-600)',
|
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 3px var(--invokeai-colors-accent-400)',
|
||||||
},
|
},
|
||||||
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`,
|
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`,
|
||||||
},
|
},
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "3.0.0+b10"
|
__version__ = "3.0.0"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
- [ ] No, because:
|
- [ ] No, because:
|
||||||
|
|
||||||
|
|
||||||
## Have you updated relevant documentation?
|
## Have you updated all relevant documentation?
|
||||||
- [ ] Yes
|
- [ ] Yes
|
||||||
- [ ] No
|
- [ ] No
|
||||||
|
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ldm.invoke import __app_name__, __version__
|
from invokeai.version import __version__
|
||||||
|
|
||||||
local_version = str(__version__).replace("-", "")
|
local_version = str(__version__).replace("-", "")
|
||||||
package_name = str(__app_name__)
|
package_name = 'InvokeAI'
|
||||||
|
|
||||||
|
|
||||||
def get_pypi_versions(package_name=package_name) -> list[str]:
|
def get_pypi_versions(package_name=package_name) -> list[str]:
|
||||||
"""Get the versions of the package from PyPI"""
|
"""Get the versions of the package from PyPI"""
|
||||||
|
Loading…
Reference in New Issue
Block a user