Compare commits

...

53 Commits

Author SHA1 Message Date
Lincoln Stein
907ff165be Update communityNodes.md (#3873)
Added the Ideal Size node

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [X] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [ ] Yes
- [X] No, because: It's a community node addition

      
## Have you updated all relevant documentation?
- [X] Yes
- [ ] No


## Description

Added a reference to my community node that calculates the ideal size
for initial latent generation that avoids duplication. This is the logic
that was present in 2.3.5's first pass of high-res optimization.

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Added/updated tests?

- [ ] Yes
- [X] No : This is a documentation change that references my community
node.

## [optional] Are there any post deployment tasks we need to perform?
2023-07-21 15:17:28 -04:00
Lincoln Stein
53c8c3b4f5 Merge branch 'main' into JPPhoto-add-ideal-size 2023-07-21 15:17:06 -04:00
Lincoln Stein
8262c31866 Update communityNodes.md (#3876)
Add Face Mask to communityNodes.md

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [x] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [x] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [x] Yes
- [ ] No


## Description

Add Face Mask to communituNodes.md list.
2023-07-21 15:16:41 -04:00
Lincoln Stein
b940ae8dbb Merge branch 'main' into facemask/communitynodes 2023-07-21 15:16:14 -04:00
Lincoln Stein
845d1524ad warn, do not crash, when duplicate models encountered 2023-07-21 15:00:55 -04:00
ymgenesis
6c82b694a7 Update communityNodes.md
Add Face Mask to communityNodes.md
2023-07-21 19:05:37 +02:00
Lincoln Stein
f1fcc3fb74 fix pypi helper for correct pypi updating 2023-07-21 12:36:09 -04:00
Lincoln Stein
2dd59d31d0 fix mkdocs push 2023-07-21 12:27:53 -04:00
psychedelicious
3f79812dc6 fix: mps attention fix for sd2 2023-07-21 09:22:37 -04:00
Kent Keirsey
055b2207cb Update CONTRIBUTORS.md 2023-07-21 08:24:17 -04:00
Lincoln Stein
19cdd5a99b rebuild frontend for release 2023-07-21 07:48:30 -04:00
Jonathan
5db66e00b6 Update communityNodes.md
Added the Ideal Size node
2023-07-21 06:38:42 -05:00
Lincoln Stein
76337e13f5 Last 3.0.0 tweaks (#3872)
Updated contributors
2023-07-21 07:38:28 -04:00
Lincoln Stein
eb4ca4042e Merge branch 'main' into release/3-0-0 2023-07-21 07:38:02 -04:00
psychedelicious
594bf6fef1 fix(api,ui): fix canvas saved images have extra transparent regions
- add `crop_visible` param to upload image & set to true only for canvas saves
2023-07-21 07:26:12 -04:00
psychedelicious
6f2e8d5217 chore(ui): regen types 2023-07-21 07:26:12 -04:00
psychedelicious
52ae15c167 fix(ui): fix console error related to css 2023-07-21 07:26:12 -04:00
psychedelicious
2c4128d44e fix(ui): deleting board does not reset selected board/image 2023-07-21 07:26:12 -04:00
psychedelicious
01b106d939 fix(ui): fix no image selected on first load 2023-07-21 07:26:12 -04:00
psychedelicious
68f1f87c6f feat(ui): board styles 2023-07-21 07:26:12 -04:00
psychedelicious
c2c99b8650 feat(ui): fix more caching bugs 2023-07-21 07:26:12 -04:00
psychedelicious
896b77cf56 feat(api,db): allow creating an image with a board_id 2023-07-21 07:26:12 -04:00
Lincoln Stein
6f7d221f57 Couple doc tweaks (#3870)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [x] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [ ] Yes
- [x] No, because: just updated docs to try to help lead new users to
installs a little easier

      
## Have you updated relevant documentation?
- [x] Yes
- [ ] No


## Description
Some minor docs tweaks

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Added/updated tests?

- [ ] Yes
- [x] No : _please replace this line with details on why tests
      have not been included_

## [optional] Are there any post deployment tasks we need to perform?
2023-07-21 06:43:03 -04:00
Lincoln Stein
fba4085939 ui: boards 2: electric boogaloo (#3869)
## What type of PR is this? (check all applicable)

- [x] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [x] Yes
- [ ] No, because:


## Description

Revised boards logic and UI

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue # discord convos
- Closes #

## QA Instructions, Screenshots, Recordings

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Added/updated tests?

- [ ] Yes
- [x] No : n/a

## [optional] Are there any post deployment tasks we need to perform?
2023-07-21 06:42:16 -04:00
Millun Atluri
48ad005732 Couple doc tweaks 2023-07-21 16:35:41 +10:00
blessedcoolant
9ce4bd1182 fix: Simplify gallery board name layout 2023-07-21 18:15:55 +12:00
blessedcoolant
39b7ace273 fix: Differentiate no boards from the user boards 2023-07-21 18:15:12 +12:00
blessedcoolant
319c56f844 fix: Make auto add icon be a tad bit smaller 2023-07-21 18:14:57 +12:00
psychedelicious
389a0d2810 feat(ui): use badge for autoadd 2023-07-21 16:01:40 +10:00
psychedelicious
fe33acedad fix(ui): fix crash when removing last image 2023-07-21 15:57:09 +10:00
psychedelicious
eab18c7385 fix(ui): fix incorrect gallery tab 2023-07-21 15:56:50 +10:00
psychedelicious
8e98085530 fix(ui): fix missing 'none' on no-board cache updates 2023-07-21 15:53:41 +10:00
psychedelicious
5396e998b3 feat(ui): simplify auto-add context menu 2023-07-21 15:47:12 +10:00
psychedelicious
fc98089960 fix(ui): debounce metadata query on context menu 2023-07-21 15:37:33 +10:00
psychedelicious
dd0b4dc744 fix(ui): fix next prev buttons 2023-07-21 15:37:20 +10:00
psychedelicious
ddeba190bc fix(ui): really fixed autoadd context menu 2023-07-21 15:18:48 +10:00
psychedelicious
3a610e1a65 fix(ui): more fixing of auto-add 2023-07-21 15:00:07 +10:00
psychedelicious
e10e22440d fix(ui): restore auto-add to board functionality 2023-07-21 14:29:42 +10:00
psychedelicious
f4e8a91bcf fix(ui): update boardIdSelected 2023-07-21 14:22:18 +10:00
Lincoln Stein
ce7fbdb01d bump version; update contributors list 2023-07-21 00:17:21 -04:00
psychedelicious
4da6623700 fix(ui): fix deleteboard cache changes 2023-07-21 14:16:19 +10:00
psychedelicious
0e3ca59e49 feat(ui): refactor boards hierarchy 2023-07-21 13:48:15 +10:00
Lincoln Stein
e06f2229ac Replace SlicedAttnProcessor with patched to chunk memory on mps (#3868)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [x] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Description
On mps generating images with resolution above ~1536x1536 results in
"fried" output. Main problem that such resolution results in tensors in
size more then 4gb. Looks like that some of mps internals can't handle
properly this, so to mitigate it I break attention calculation in
chunks.

## QA Instructions, Screenshots, Recordings
Example of bad output:

![image](https://github.com/invoke-ai/InvokeAI/assets/7768370/cd373458-c0a5-4a2f-8ea5-402020de5b4b)
2023-07-20 23:32:29 -04:00
Lincoln Stein
5962d96f27 Merge branch 'main' into fix/long_tensors_mps 2023-07-20 23:24:47 -04:00
Lincoln Stein
d4854c4fac Release 3.0.0 RC Series (#3844)
## What type of PR is this? (check all applicable)

- [ X] Documentation Update


## Have you discussed this change with the InvokeAI team?
- [X ] Yes
- [ ] No, because:

## Description

This is a WIP to collect documentation enhancements and other polish
prior to final 3.0.0 release. Minor bug fixes may go in here if
non-controversial. It should be merged into main prior to the final
release.
2023-07-20 23:22:40 -04:00
Lincoln Stein
46801c076f Merge branch 'main' into release/invokeai-3-0-rc 2023-07-20 23:16:05 -04:00
Lincoln Stein
9370572169 prettify startup messages 2023-07-20 22:45:35 -04:00
blessedcoolant
ace65325ff Update FoundModelsList.tsx (#3867)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [ ] Yes
- [ ] No, because:

      
## Have you updated relevant documentation?
- [ ] Yes
- [ ] No


## Description


## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Added/updated tests?

- [ ] Yes
- [ ] No : _please replace this line with details on why tests
      have not been included_

## [optional] Are there any post deployment tasks we need to perform?
2023-07-21 13:14:32 +12:00
Sergey Borisov
e6d890888c Replace SlicedAttnProcessor with patched to chunk memory consumption less then 4gb in each attention calculation pass 2023-07-21 04:08:49 +03:00
Kent Keirsey
8e7f581065 Update FoundModelsList.tsx 2023-07-20 20:51:54 -04:00
Lincoln Stein
85ef3f51e7 extra check for empty hftoken 2023-07-20 15:16:06 -04:00
blessedcoolant
8fdc8a8da5 fix: No board name being displayed if it is empty (#3863)
## What type of PR is this? (check all applicable)

- [x] Bug Fix

## Desc

Fixes a bug where the board name is not displayed in the header if there
are no images in it.
2023-07-21 05:10:11 +12:00
blessedcoolant
52d56e96a5 fix: No board name being displayed if it is empty 2023-07-21 05:07:50 +12:00
79 changed files with 2149 additions and 1434 deletions

View File

@@ -2,7 +2,7 @@ name: mkdocs-material
on:
push:
branches:
- 'refs/heads/v2.3'
- 'refs/heads/main'
permissions:
contents: write

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,67 +17,267 @@ We thank them for all of their time and hard work.
* @lstein (Lincoln Stein) - Co-maintainer
* @blessedcoolant - Co-maintainer
* @hipsterusername (Kent Keirsey) - Product Manager
* @psychedelicious - Web Team Leader
* @hipsterusername (Kent Keirsey) - Co-maintainer, CEO, Positive Vibes
* @psychedelicious (Spencer Mabrito) - Web Team Leader
* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard
* @damian0815 - Attention Systems and Gameplay Engineer
* @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
* @damian0815 - Attention Systems and Compel Maintainer
* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
* @jpphoto (Jonathan Pollack) - Inference and rendering engine optimization
* @genomancer (Gregg Helt) - Model training and merging
* @genomancer (Gregg Helt) - Controlnet support
* @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)
- [Kevin Gibbons](https://github.com/bakkot)
- [Tesseract Cat](https://github.com/TesseractCat)
- [blessedcoolant](https://github.com/blessedcoolant)
- [David Ford](https://github.com/david-ford)
- [yunsaki](https://github.com/yunsaki)
- [James Reynolds](https://github.com/magnusviri)
- [David Wager](https://github.com/maddavid123)
- [Jason Toffaletti](https://github.com/toffaletti)
- [tildebyte](https://github.com/tildebyte)
- [Cragin Godley](https://github.com/cgodley)
- [BlueAmulet](https://github.com/BlueAmulet)
- [Benjamin Warner](https://github.com/warner-benjamin)
- [Cora Johnson-Roberson](https://github.com/corajr)
- [veprogames](https://github.com/veprogames)
- [JigenD](https://github.com/JigenD)
- [Niek van der Maas](https://github.com/Niek)
- [Henry van Megen](https://github.com/hvanmegen)
- [Håvard Gulldahl](https://github.com/havardgulldahl)
- [greentext2](https://github.com/greentext2)
- [Simon Vans-Colina](https://github.com/simonvc)
- [Gabriel Rotbart](https://github.com/gabrielrotbart)
- [Eric Khun](https://github.com/erickhun)
- [Brent Ozar](https://github.com/BrentOzar)
- [nderscore](https://github.com/nderscore)
- [Mikhail Tishin](https://github.com/tishin)
- [Tom Elovi Spruce](https://github.com/ilovecomputers)
- [spezialspezial](https://github.com/spezialspezial)
- [Yosuke Shinya](https://github.com/shinya7y)
- [Andy Pilate](https://github.com/Cubox)
- [Muhammad Usama](https://github.com/SMUsamaShah)
- [Arturo Mendivil](https://github.com/artmen1516)
- [Paul Sajna](https://github.com/sajattack)
- [Samuel Husso](https://github.com/shusso)
- [nicolai256](https://github.com/nicolai256)
- [Mihai](https://github.com/mh-dm)
- [Any Winter](https://github.com/any-winter-4079)
- [Doggettx](https://github.com/doggettx)
- [Matthias Wild](https://github.com/mauwii)
- [Kyle Schouviller](https://github.com/kyle0654)
- [rabidcopy](https://github.com/rabidcopy)
- [Dominic Letz](https://github.com/dominicletz)
- [Dmitry T.](https://github.com/ArDiouscuros)
- [Kent Keirsey](https://github.com/hipsterusername)
- [psychedelicious](https://github.com/psychedelicious)
- [damian0815](https://github.com/damian0815)
- [Eugene Brodsky](https://github.com/ebr)
- AbdBarho
- ablattmann
- AdamOStark
- Adam Rice
- Airton Silva
- Alexander Eichhorn
- Alexandre D. Roberge
- Andreas Rozek
- Andre LaBranche
- Andy Bearman
- Andy Luhrs
- Andy Pilate
- Any-Winter-4079
- apolinario
- ArDiouscuros
- Armando C. Santisbon
- Arthur Holstvoogd
- artmen1516
- Artur
- Arturo Mendivil
- Ben Alkov
- Benjamin Warner
- Bernard Maltais
- blessedcoolant
- blhook
- BlueAmulet
- Bouncyknighter
- Brandon Rising
- Brent Ozar
- Brian Racer
- bsilvereagle
- c67e708d
- CapableWeb
- Carson Katri
- Chloe
- Chris Dawson
- Chris Hayes
- Chris Jones
- chromaticist
- Claus F. Strasburger
- cmdr2
- cody
- Conor Reid
- Cora Johnson-Roberson
- coreco
- cosmii02
- 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**

View File

@@ -58,7 +58,8 @@ class ApiDependencies:
@staticmethod
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}")
events = FastAPIEventService(event_handler_id)

View File

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

View File

@@ -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")
# 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_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")
#fmt: on

View File

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

View File

@@ -299,10 +299,11 @@ class ModelManagerService(ModelManagerServiceBase):
else:
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())
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
if precision == "auto":

View File

@@ -661,7 +661,7 @@ def write_opts(opts: Namespace, init_file: Path):
with open(init_file,'w', encoding='utf-8') as file:
file.write(new_config.to_yaml())
if hasattr(opts,'hf_token'):
if hasattr(opts,'hf_token') and opts.hf_token:
HfLogin(opts.hf_token)
# -------------------------------------

View File

@@ -3,6 +3,6 @@ Initialization file for invokeai.backend.model_management
"""
from .model_manager import ModelManager, ModelInfo, AddModelResult, SchedulerPredictionType
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

View File

@@ -251,7 +251,9 @@ from .model_search import ModelSearch
from .models import (
BaseModelType, ModelType, SubModelType,
ModelError, SchedulerPredictionType, MODEL_CLASSES,
ModelConfigBase, ModelNotFoundException, InvalidModelException,
ModelConfigBase,
ModelNotFoundException, InvalidModelException,
DuplicateModelException,
)
# We are only starting to number the config file with release 3.
@@ -858,7 +860,7 @@ class ModelManager(object):
loaded_files = set()
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):
for model_key, model_config in list(self.models.items()):
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_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:
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))
self.models[model_key] = model_config
new_models_found = True
except DuplicateModelException as e:
self.logger.warning(e)
except InvalidModelException:
self.logger.warning(f"Not a valid model: {model_path}")
except NotImplementedError as e:

View File

@@ -2,7 +2,11 @@ import inspect
from enum import Enum
from pydantic import BaseModel
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 .sdxl import StableDiffusionXLModel
from .vae import VaeModel

View File

@@ -15,6 +15,9 @@ from contextlib import suppress
from pydantic import BaseModel, Field
from typing import List, Dict, Optional, Type, Literal, TypeVar, Generic, Callable, Any, Union
class DuplicateModelException(Exception):
pass
class InvalidModelException(Exception):
pass

View File

@@ -1,4 +1,6 @@
import math
import torch
import diffusers
if torch.backends.mps.is_available():
@@ -61,3 +63,150 @@ def new_torch_interpolate(input, size=None, scale_factor=None, mode='nearest', a
return _torch_interpolate(input, size, scale_factor, mode, align_corners, recompute_scale_factor, antialias)
torch.nn.functional.interpolate = new_torch_interpolate
# TODO: refactor it
_SlicedAttnProcessor = diffusers.models.attention_processor.SlicedAttnProcessor
class ChunkedSlicedAttnProcessor:
r"""
Processor for implementing sliced attention.
Args:
slice_size (`int`, *optional*):
The number of steps to compute attention. Uses as many slices as `attention_head_dim // slice_size`, and
`attention_head_dim` must be a multiple of the `slice_size`.
"""
def __init__(self, slice_size):
assert isinstance(slice_size, int)
slice_size = 1 # TODO: maybe implement chunking in batches too when enough memory
self.slice_size = slice_size
self._sliced_attn_processor = _SlicedAttnProcessor(slice_size)
def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None):
if self.slice_size != 1 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

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

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,7 @@
margin: 0;
}
</style>
<script type="module" crossorigin src="./assets/index-3a8b43e1.js"></script>
<script type="module" crossorigin src="./assets/index-e2437518.js"></script>
</head>
<body dir="ltr">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -76,7 +76,7 @@ export default function FoundModelsList() {
dispatch(
addToast(
makeToast({
title: 'Faile To Add Model',
title: 'Failed To Add Model',
status: 'error',
})
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
__version__ = "3.0.0rc2"
__version__ = "3.0.0"

View File

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

View File

@@ -2,11 +2,10 @@
import requests
from ldm.invoke import __app_name__, __version__
from invokeai.version import __version__
local_version = str(__version__).replace("-", "")
package_name = str(__app_name__)
package_name = 'InvokeAI'
def get_pypi_versions(package_name=package_name) -> list[str]:
"""Get the versions of the package from PyPI"""