Compare commits

...

678 Commits

Author SHA1 Message Date
psychedelicious
f8dadc8c0a chore: release v5.0.0.dev13 2024-09-06 21:33:24 +10:00
psychedelicious
d421eaeff0 feat(ui): move seed out of advanced, hide HRF settings 2024-09-06 21:27:58 +10:00
psychedelicious
d434e80e99 chore(ui): lint 2024-09-06 21:27:58 +10:00
psychedelicious
918ed5c264 feat(ui): tweak padding on entity group header 2024-09-06 21:27:58 +10:00
psychedelicious
2f98c876df feat(ui): use plurals for entity group header hidden tooltip 2024-09-06 21:27:58 +10:00
psychedelicious
2b9f9f3d68 feat(ui): move delete entity button down to entity list item 2024-09-06 21:27:58 +10:00
psychedelicious
f326bfe1b0 feat(ui): add fit bbox to layers 2024-09-06 21:27:58 +10:00
psychedelicious
c57966ec38 fix(ui): tidy incorrect component name 2024-09-06 21:27:58 +10:00
psychedelicious
3c5bf48810 feat(ui): do not allow invoke while transforming or filtering 2024-09-06 21:27:58 +10:00
psychedelicious
c0db830398 feat(ui): do not allow transform, filter or merge while staging 2024-09-06 21:27:58 +10:00
psychedelicious
3e3e00b24d fix(ui): prevent stage scale/size from being invalid 2024-09-06 21:27:58 +10:00
psychedelicious
640af85c93 fix(ui): do not save filtered previews to gallery 2024-09-06 21:27:58 +10:00
psychedelicious
81060eef71 feat(ui): filter UI layout 2024-09-06 21:27:58 +10:00
psychedelicious
ea193903ee feat(ui): revised entity list action bars
- Global action bar on top
- Selected Entity action bar below
2024-09-06 21:27:58 +10:00
psychedelicious
5a251a4876 feat(ui): fit bbox to stage on canvas reset 2024-09-06 21:27:58 +10:00
psychedelicious
ce93fdd076 chore(ui): lint 2024-09-06 21:27:58 +10:00
psychedelicious
788e03fbc3 feat(ui): reworked image context menu
- Add `Open in Viewer`
- Remove `Send to Image to Image`
- Fix `Send to Canvas`
- Split out logic for composability
2024-09-06 21:27:57 +10:00
psychedelicious
8cb1584379 feat(ui): restore aspect ratio preview component 2024-09-06 21:27:57 +10:00
psychedelicious
63a2b6cdfe fix(ui): transformer rendered behind layer objects 2024-09-06 21:27:57 +10:00
psychedelicious
b193bc7b14 feat(ui): inverted shift behavior for transformer 2024-09-06 21:27:57 +10:00
psychedelicious
dce488c691 fix(ui): ignore filters when calculating bbox 2024-09-06 21:27:57 +10:00
psychedelicious
615b09d453 feat(ui): cancel by destination, not origin 2024-09-06 21:27:57 +10:00
psychedelicious
1dd521a792 chore(ui): typegen 2024-09-06 21:27:57 +10:00
psychedelicious
f6a703c367 feat(app): cancel by destination, not origin
When resetting the canvas or staging area, we don't want to cancel generations that are going to the gallery - only those going to the canvas.

Thus the method should not cancel by origin, but instead cancel by destination.

Update the queue method and route.
2024-09-06 21:27:57 +10:00
psychedelicious
5a69b62a1a fix(ui): scaled size not correctly reset when canvas reset 2024-09-06 21:27:57 +10:00
psychedelicious
c4b9ba8e12 feat(ui): use black bg when rasterizing control images 2024-09-06 21:27:57 +10:00
psychedelicious
c04790158f fix(ui): ignore Konva filters when previewing filter 2024-09-06 21:27:57 +10:00
psychedelicious
a0ed7baf73 fix(ui): filter preview accidentally committed to layer 2024-09-06 21:27:57 +10:00
psychedelicious
f96148ca52 feat(ui): improved transparency effect
Use the min of each pixel's alpha value and lightness for the output alpha. This prevents artifacts when using the transparency effect, especially with non-black pixels with low alpha.
2024-09-06 21:27:57 +10:00
psychedelicious
96786ed62b chore: release v4.2.9.dev12 2024-09-06 21:27:57 +10:00
psychedelicious
cc9cc62707 fix(ui): missing translation 2024-09-06 21:27:57 +10:00
psychedelicious
07cf5807be fix(ui): save to gallery uses auto-add board 2024-09-06 21:27:57 +10:00
psychedelicious
013414e962 fix(ui): cancel transform/filter when deleting entity 2024-09-06 21:27:57 +10:00
psychedelicious
10bba49e28 chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
7888b4913a feat(ui): iterate on state flow and rendering 2
- Rely on redux + reselect more
- Remove all nanostores that simply "mirrored" redux state in favor of direct subscriptions to redux store
- Add abstractions for creating redux subs and running selectors
- Add `initialize` method to CanvasModuleBase, for post-instantiation tasks
- Reduce local caching of state in modules to a minimum
2024-09-06 21:27:57 +10:00
psychedelicious
27b00cedc5 feat(ui): iterate on state flow and rendering 2024-09-06 21:27:57 +10:00
psychedelicious
0bfdd5acb8 feat(ui): slight layout change for staging area toolbar 2024-09-06 21:27:57 +10:00
psychedelicious
6e6629ad74 feat(ui): clean up adapter API 2024-09-06 21:27:57 +10:00
psychedelicious
fe257e3e46 feat(ui): streamlined state flow 2024-09-06 21:27:57 +10:00
psychedelicious
0b1dc365d0 fix(ui): handle optimal dimension when resetting canvas 2024-09-06 21:27:57 +10:00
psychedelicious
c75a933b17 feat(ui): background and staging area modules have own store subscription and render themselves 2024-09-06 21:27:57 +10:00
psychedelicious
f1525c2a82 feat(ui): make rendering methods not need args
They should pull from the entity's state directly. This allows more freedom with updating the canvas.
2024-09-06 21:27:57 +10:00
psychedelicious
8266c10c53 feat(ui): restore size of invoke button 2024-09-06 21:27:57 +10:00
psychedelicious
34554f8555 tidy(ui): remove unnecessary awaits in rendering module 2024-09-06 21:27:57 +10:00
psychedelicious
200dbbbf55 tidy(ui): rename some classes to better represent their responsibilities 2024-09-06 21:27:57 +10:00
psychedelicious
5a30ff6045 feat(ui): abstract out CanvasEntityAdapterBase
Things were getting to complex to reason about & classes a bit complicated. Trying to simplify...
2024-09-06 21:27:57 +10:00
psychedelicious
73e8837d45 feat(ui): revise entity rendering flow 2024-09-06 21:27:57 +10:00
psychedelicious
6ba78e7632 tidy(ui): remove unused id on konva nodes 2024-09-06 21:27:57 +10:00
psychedelicious
736fabe327 tidy(ui): remove commented code 2024-09-06 21:27:57 +10:00
psychedelicious
ad53169eba tidy(ui): remove extraneous docstrings 2024-09-06 21:27:57 +10:00
psychedelicious
0a22e6b5a1 feat(ui): clean up unused tool module state 2024-09-06 21:27:57 +10:00
psychedelicious
5eb9e2ba04 tidy(ui): disable isDebugging flag on root component 2024-09-06 21:27:57 +10:00
psychedelicious
376a626419 fix(ui): unable to drag while transforming after switching tools 2024-09-06 21:27:57 +10:00
psychedelicious
5d089ecc77 feat(ui): prevent layer interactions when transforming or filtering 2024-09-06 21:27:57 +10:00
psychedelicious
3ed85d821b feat(ui): add compositeMaskedRegions setting 2024-09-06 21:27:57 +10:00
psychedelicious
7c29caa245 tidy(ui): merge tool slice, sendToCanvas into settings slice 2024-09-06 21:27:57 +10:00
psychedelicious
6161c71aa7 build(ui): add csstype dev dependency 2024-09-06 21:27:57 +10:00
psychedelicious
f7e68ac6d6 feat(ui): clean up tool preview rendering 2024-09-06 21:27:57 +10:00
psychedelicious
5074c97683 feat(ui): tool buttons are only disabled when currently selected 2024-09-06 21:27:57 +10:00
psychedelicious
bb6b5c4941 feat(ui): better types on CanvasStateApiModule.getEntity 2024-09-06 21:27:57 +10:00
psychedelicious
ff12f16343 feat(ui): update default logging context path to be string 2024-09-06 21:27:57 +10:00
psychedelicious
397038e6e8 tidy(ui): mark canvas module attrs readonly 2024-09-06 21:27:57 +10:00
psychedelicious
fcacc0f70a chore: release v4.2.9.dev11 2024-09-06 21:27:57 +10:00
psychedelicious
3ed4cf63ae feat(ui): tidy stateApi atoms & add docstrings 2024-09-06 21:27:57 +10:00
psychedelicious
4b753941e4 feat(ui): streamline manager -> react transform interface 2024-09-06 21:27:57 +10:00
psychedelicious
dbff929be5 tidy(ui): remove unused $isProcessingTransform atom 2024-09-06 21:27:57 +10:00
psychedelicious
44be058ac7 docs(ui): docstrings for $canvasCache 2024-09-06 21:27:57 +10:00
psychedelicious
437129cf70 feat(ui): tweak bookmark verbiage 2024-09-06 21:27:57 +10:00
psychedelicious
2e4a48b71c feat(ui): move transformer state to nanostores
This provides some free reactivity for this canvas-manager-managed state.
2024-09-06 21:27:57 +10:00
psychedelicious
618473db02 fix(ui): transform should ignore konva filters (e.g. transparency effect) 2024-09-06 21:27:57 +10:00
psychedelicious
4f4eee76b9 feat(ui): add fit to bbox as transform helper 2024-09-06 21:27:57 +10:00
psychedelicious
1628eb5900 tidy(ui): transformer organisation 2024-09-06 21:27:57 +10:00
psychedelicious
92c754df79 fix(ui): disable merge visible when 1 or fewer layers of type 2024-09-06 21:27:57 +10:00
psychedelicious
921ade4b0a feat(ui): brush preview opacity at 0.5 when drawing on mask 2024-09-06 21:27:57 +10:00
psychedelicious
c38b05c002 chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
277708d69e fix(ui): edge cases in quick switch, simpler logic 2024-09-06 21:27:57 +10:00
psychedelicious
02061faaf0 chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
a4fb8eb171 feat(ui): add bookmark for quick switch 2024-09-06 21:27:57 +10:00
psychedelicious
d3c2432bfb fix(ui): force dims on scaled bbox when manual scaling + locked aspect ratio
Closes #5590
2024-09-06 21:27:57 +10:00
psychedelicious
0c3e2c45f1 feat(ui): "Control Layers" -> "Layers" 2024-09-06 21:27:57 +10:00
psychedelicious
d019dbdbd3 feat(ui): "IP Adapter" -> "Global IP Adapter" 2024-09-06 21:27:57 +10:00
psychedelicious
2538b348c7 tidy(ui): canvas hotkey hooks 2024-09-06 21:27:57 +10:00
psychedelicious
420178c09f feat(ui): add alt+[ and alt+] hotkeys to cycle through layers 2024-09-06 21:27:57 +10:00
psychedelicious
75ecc56ca5 feat(ui): add layer quick switch
Q toggles between the last-selected layers.
2024-09-06 21:27:57 +10:00
psychedelicious
874b96c67d feat(ui): bbox hotkey is c 2024-09-06 21:27:57 +10:00
psychedelicious
b0eddd19d4 fix(ui): select nonexistent entity 2024-09-06 21:27:57 +10:00
psychedelicious
cb9d0bce56 feat(ui): brush & eraser width ui/ux
Use same pattern as canvas scale & opacity sliders w/ scaled slider values for precision at low values.
2024-09-06 21:27:57 +10:00
psychedelicious
fd6d080fb0 tidy(ui): canvas scale & entity opacity sliders 2024-09-06 21:27:57 +10:00
psychedelicious
8f6e6960e0 feat(ui): hotkeys for brush/eraser size 2024-09-06 21:27:57 +10:00
psychedelicious
58064a1ea5 feat(ui): use default IP adapter when creating IP adapter 2024-09-06 21:27:57 +10:00
psychedelicious
0d94a89d98 tidy(ui): organise files 2024-09-06 21:27:57 +10:00
psychedelicious
eb4d447e2f feat(ui): remove object count from entity title
This was used for troubleshooting only.
2024-09-06 21:27:57 +10:00
psychedelicious
487422a53f tidy(ui): misc cleanup 2024-09-06 21:27:57 +10:00
psychedelicious
b24f8e2b26 docs(ui): docstrings for classes (wip) 2024-09-06 21:27:57 +10:00
psychedelicious
fd22ff7c9f feat(ui): revised canvas module base class
Big cleanup. Makes these classes easier to implement, lots of comments and docstrings to clarify how it all works.

- Add default implementations for `destroy`, `repr` and `getLoggingContext`
- Tidy individual module configs
- Update `CanvasManager.buildLogger` to accept a canvas module as the arg
- Add `CanvasManager.buildPath`
2024-09-06 21:27:57 +10:00
psychedelicious
9cd5107955 feat(ui): split canvas tool previews into modules 2024-09-06 21:27:57 +10:00
psychedelicious
fd7f0056e3 fix(ui): reject on dataURLToImageData 2024-09-06 21:27:57 +10:00
psychedelicious
b1673b9222 fix(ui): correctly set last cursor pos to null 2024-09-06 21:27:57 +10:00
psychedelicious
f4e8799561 chore: release v4.2.9.dev10 2024-09-06 21:27:57 +10:00
psychedelicious
6f85743996 feat(ui): remove entity list context menu (again)
stupid events
2024-09-06 21:27:57 +10:00
psychedelicious
243e7c954e fix(ui): entity groups not collapsing 2024-09-06 21:27:57 +10:00
psychedelicious
33d25c7977 chore: release v4.2.9.dev9 2024-09-06 21:27:57 +10:00
psychedelicious
d11251f380 fix(ui): entity opacity number input focus prevents slider from opening 2024-09-06 21:27:57 +10:00
psychedelicious
d5e5cae398 feat(ui): add merge visible for raster and inpaint mask layers
I don't think it makes sense to merge control layers or regional guidance layers because they have additional state.
2024-09-06 21:27:57 +10:00
psychedelicious
fa80452abf fix(ui): save to gallery rect too large
Was including all layer types in the rect - only want the raster layers.
2024-09-06 21:27:57 +10:00
psychedelicious
c9e396bee9 fix(ui): canvasToBlob not raising error correctly 2024-09-06 21:27:57 +10:00
psychedelicious
fd6de78746 feat(ui): add save to gallery button 2024-09-06 21:27:57 +10:00
psychedelicious
6096c8ed0a fix(ui): fix getRectUnion util, add some tests 2024-09-06 21:27:57 +10:00
psychedelicious
5126ec114a fix(ui): modals not staying open
TBH not sure exactly why this broke. Fixed by rollback back the use of a render prop in favor of global state. Also revised the API of `useBoolean` and `buildUseBoolean`.
2024-09-06 21:27:57 +10:00
psychedelicious
221c51b498 fix(ui): correct labels for generation tab origin 2024-09-06 21:27:57 +10:00
psychedelicious
28db39a932 fix(ui): context menu doesn't work for new entities
I do not understand why this fixes the issue, doesn't seem like it should. But it does.
2024-09-06 21:27:57 +10:00
psychedelicious
5512f80066 tidy(ui): organise tool module 2024-09-06 21:27:57 +10:00
psychedelicious
70e27d6fbb fix(ui): staging hotkeys enabled at wrong times 2024-09-06 21:27:57 +10:00
psychedelicious
e91149111e fix(ui): incorrect batch origin preventing progress/staging 2024-09-06 21:27:57 +10:00
psychedelicious
ad7919b3ec feat(ui): restore minimal HUD 2024-09-06 21:27:57 +10:00
psychedelicious
908f4117fc feat(ui): remove unused asPreview for StageComponent 2024-09-06 21:27:57 +10:00
psychedelicious
87b294708a chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
a0be7c47cd chore: release v4.2.9.dev8 2024-09-06 21:27:57 +10:00
psychedelicious
87bc3f5f0a feat(ui): revise generation mode logic
- Canvas generation mode is replace with a boolean `sendToCanvas` flag. When off, images generated on the canvas go to the gallery. When on, they get added to the staging area.
- When an image result is received, if its destination is the canvas, staging is automatically started.
- Updated queue list to show the destination column.
- Added `IconSwitch` component to represent binary choices, used for the new `sendToCanvas` flag and image viewer toggle.
- Remove the queue actions menu in `QueueControls`. Move the queue count badge to the cancel button.
- Redo layout of `QueueControls` to prevent duplicate queue count badges.
- Fix issue where gallery and options panels could show thru transparent regions of queue tab.
- Disable panel hotkeys when on mm/queue tabs.
2024-09-06 21:27:57 +10:00
psychedelicious
14dea8bad3 chore(ui): typegen 2024-09-06 21:27:57 +10:00
psychedelicious
9ced9efc64 feat(app): add destination column to session_queue
The frontend needs to know where queue items came from (i.e. which tab), and where results are going to (i.e. send images to gallery or canvas). The `origin` column is not quite enough to represent this cleanly.

A `destination` column provides the frontend what it needs to handle incoming generations.
2024-09-06 21:27:57 +10:00
psychedelicious
b0414a81ec tidy(ui): ViewerToggleMenu -> ViewerToggle 2024-09-06 21:27:57 +10:00
psychedelicious
f078e325f8 feat(ui): alt quick switches to color picker 2024-09-06 21:27:57 +10:00
psychedelicious
1a5dc202cb feat(ui): tweak add entity button layout 2024-09-06 21:27:57 +10:00
psychedelicious
79d7f023e8 feat(ui): restore context menu for entity list 2024-09-06 21:27:57 +10:00
psychedelicious
913912d96b feat(ui): add delete button to each layer 2024-09-06 21:27:57 +10:00
psychedelicious
cfa22b345a feat(ui): add + buttons to entity categories 2024-09-06 21:27:57 +10:00
psychedelicious
ce4948ec23 feat(ui): tweak brush fill UI 2024-09-06 21:27:57 +10:00
psychedelicious
86d10ca87b feat(ui): do not select layer on staging accept 2024-09-06 21:27:57 +10:00
psychedelicious
057b5ec646 fix(ui): more fiddly queue count layout stuff 2024-09-06 21:27:57 +10:00
psychedelicious
16eede4288 fix(ui): floating params panel invoke button loading state 2024-09-06 21:27:57 +10:00
psychedelicious
e3c0c638c1 feat(ui): move canvas undo/redo to hook 2024-09-06 21:27:57 +10:00
psychedelicious
e57ee8db35 fix(ui): queue count badge positioning 2024-09-06 21:27:57 +10:00
psychedelicious
cd2a80cf77 fix(ui): add node cmdk only enabled on workflows tab 2024-09-06 21:27:57 +10:00
psychedelicious
300ecd9b16 chore: release v4.2.9.dev7 2024-09-06 21:27:57 +10:00
psychedelicious
a95fbfe6c6 fix(ui): pending node connection stuck 2024-09-06 21:27:57 +10:00
psychedelicious
79168b637e chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
42c7ddebaa chore: release v4.2.9.dev6 2024-09-06 21:27:56 +10:00
psychedelicious
ac349573f2 feat(ui): migrate add node popover to cmdk
Put this together as a way to figure out the library before moving on to the full app cmdk. Works great.
2024-09-06 21:27:56 +10:00
psychedelicious
28feb013d7 fix(ui): schema parsing now that node_pack is guaranteed to be present 2024-09-06 21:27:56 +10:00
psychedelicious
4129cddbeb chore(ui): typegen 2024-09-06 21:27:56 +10:00
psychedelicious
4463e6da5d fix(app): node_pack not added to openapi schema correctly 2024-09-06 21:27:56 +10:00
psychedelicious
db0b97cca1 fix(ui): unnecessary z-index on invoke button 2024-09-06 21:27:56 +10:00
psychedelicious
be58b03137 feat(ui): split settings modal 2024-09-06 21:27:56 +10:00
psychedelicious
2df0056ef1 perf(ui): disable useInert on modals
This hook forcibly updates _all_ portals with `data-hidden=true` when the modal opens - then reverts it when the modal closes. It's intended to help screen readers. Unfortunately, this absolutely tanks performance because we have many portals. React needs to do alot of layout calculations (not re-renders).

IMO this behaviour is a bug in chakra. The modals which generated the portals are hidden by default, so this data attr should really be set by default. Dunno why it isn't.
2024-09-06 21:27:56 +10:00
psychedelicious
eeca521baf feat(ui): fix queue item count badge positioning
Previously this badge, floating over the queue menu button next to the invoke button, was rendered within the existing layout. When I initially positioned it, the app layout interfered - it would extend into an area reserved for a flex gap, which cut off the badge.

As a (bad) workaround, I had shifted the whole app down a few pixels to make room for it. What I should have done is what I've done in this commit - render the badge in a portal to take it out of the layout so we don't need that extra vertical padding.

Sleekified some styling a bit too.
2024-09-06 21:27:56 +10:00
psychedelicious
93b9792096 fix(ui): transparency effect not updating 2024-09-06 21:27:56 +10:00
psychedelicious
a8079e5854 feat(ui): tidy canvas toolbar buttons 2024-09-06 21:27:56 +10:00
psychedelicious
61f9ba5b46 feat(ui): revised viewer toggle @joshistoast 2024-09-06 21:27:56 +10:00
psychedelicious
a99ff467f6 fix(ui): opacity reset value incorrect 2024-09-06 21:27:56 +10:00
psychedelicious
5abad87ce3 revert(ui): roll back flip, doesn't work with rotate yet 2024-09-06 21:27:56 +10:00
psychedelicious
931a4ddc88 fix(ui): disable opacity slider fully when no valid entity selected 2024-09-06 21:27:56 +10:00
psychedelicious
784a615864 fix(ui): layer preview image sometimes not rendering
The canvas size was dynamic based on the container div's size. When the div was hidden (e.g. when selecting another tab), the container's effective size is 0. This resulted in the preview image canvas being drawn at a scale of 0.

Fixed by using an absolute size for the canvas container.
2024-09-06 21:27:56 +10:00
psychedelicious
697c81b74e feat(ui): tweak regional prompt box styles 2024-09-06 21:27:56 +10:00
psychedelicious
d6b8cb9e7c feat(ui): tweak enabled/locked toggle styles 2024-09-06 21:27:56 +10:00
psychedelicious
f4a031a412 feat(ui): tweak filter styling 2024-09-06 21:27:56 +10:00
psychedelicious
1749abbd97 feat(ui): add flip & reset to transform 2024-09-06 21:27:56 +10:00
psychedelicious
1497afcfda tidy(ui): use helper to sync scaled bbox size on model change 2024-09-06 21:27:56 +10:00
psychedelicious
ec9fdace22 fix(ui): randomize seed toggle linked to prompt concat 2024-09-06 21:27:56 +10:00
psychedelicious
c1a039ef91 chore: release v4.2.9.dev5 2024-09-06 21:27:56 +10:00
psychedelicious
69d1edc036 chore(ui): lint 2024-09-06 21:27:56 +10:00
psychedelicious
b69b001755 feat(ui): generalize mask fill, add to action bar 2024-09-06 21:27:56 +10:00
psychedelicious
97414f1886 feat(ui): implement interaction locking on layers 2024-09-06 21:27:56 +10:00
psychedelicious
478daf8614 feat(ui): iterate on layer actions
- Add lock toggle
- Tweak lock and enabled styles
- Update entity list action bar w/ delete & delete all
- Move add layer menu to action bar
- Adjust opacity slider style
2024-09-06 21:27:56 +10:00
psychedelicious
c74b130303 feat(ui): collapsible entity groups 2024-09-06 21:27:56 +10:00
psychedelicious
11bc318d8d tidy(ui): rename some classes to be consistent 2024-09-06 21:27:56 +10:00
psychedelicious
77125bee7e feat(ui): tuned canvas undo/redo
- Throttle pushing to history for actions of the same type, starting with 1000ms throttle.
- History has a limit of 64 items, same as workflow editor
- Add clear history button
- Fix an issue where entity transformers would reset the entity state when the entity is fully transparent, resetting the redo stack. This could happen when you undo to the starting state of a layer
2024-09-06 21:27:56 +10:00
psychedelicious
4354cd7c38 tidy(ui): move all undoable reducers back to canvas slice 2024-09-06 21:27:56 +10:00
psychedelicious
0e31ccaea0 fix(ui): dnd image count 2024-09-06 21:27:56 +10:00
psychedelicious
e32fa8bd35 fix(ui): canvas entity opacity scale 2024-09-06 21:27:56 +10:00
psychedelicious
d209652caa perf(ui): optimize all selectors 2
Mostly selector optimization. Still a few places to tidy up but I'll get to that later.
2024-09-06 21:27:56 +10:00
psychedelicious
fe672ba5e0 perf(ui): optimize all selectors 1
I learned that the inline selector syntax recreates the selector function on every render:

```ts
const val = useAppSelector((s) => s.slice.val)
```

Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier.
2024-09-06 21:27:56 +10:00
psychedelicious
04d41085a3 feat(ui): rough out undo/redo on canvas 2024-09-06 21:27:56 +10:00
psychedelicious
36604f752e chore: release v4.2.9.dev4
Canvas dev build.
2024-09-06 21:27:56 +10:00
psychedelicious
9056f446bb fix(ui): handle error from internal konva method
We are dipping into konva's private API for preview images and it appears to be unsafe (got an error once). Wrapped in a try/catch.
2024-09-06 21:27:56 +10:00
psychedelicious
a33d1b979d feat(ui): split out loras state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
4670f82e65 feat(ui): split out session state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
a07346b364 feat(ui): split out settings state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
2b1e930cdf feat(ui): split out tool state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
6b75ea3b01 feat(ui): split out params/compositing state from canvas rendering state
First step to restoring undo/redo - the undoable state must be in its own slice. So params and settings must be isolated.
2024-09-06 21:27:56 +10:00
psychedelicious
ff8bc93080 feat(ui): add CanvasModuleBase class to standardize canvas APIs
I did this ages ago but undid it for some reason, not sure why. Caught a few issues related to subscriptions.
2024-09-06 21:27:56 +10:00
psychedelicious
85eff566dd feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-09-06 21:27:56 +10:00
psychedelicious
9480691de5 feat(ui): move ephemeral state into canvas classes
Things like `$lastCursorPos` are now created within the canvas drawing classes. Consumers in react access them via `useCanvasManager`.

For example:
```tsx
const canvasManager = useCanvasManager();
const lastCursorPos = useStore(canvasManager.stateApi.$lastCursorPos);
```
2024-09-06 21:27:56 +10:00
psychedelicious
463f3dbb35 feat(ui): normalize all actions to accept an entityIdentifier
Previously, canvas actions specific to an entity type only needed the id of that entity type. This allowed you to pass in the id of an entity of the wrong type.

All actions for a specific entity now take a full entity identifier, and the entity identifier type can be narrowed.

`selectEntity` and `selectEntityOrThrow` now need a full entity identifier, and narrow their return values to a specific entity type _if_ the entity identifier is narrowed.

The types for canvas entities are updated with optional type parameters for this purpose.

All reducers, actions and components have been updated.
2024-09-06 21:27:56 +10:00
psychedelicious
72f304baa5 feat(ui): move events into modules who care about them 2024-09-06 21:27:56 +10:00
psychedelicious
b4d01659e0 fix(ui): color picker resets brush opacity 2024-09-06 21:27:56 +10:00
psychedelicious
a733d72089 fix(ui): scaled bbox loses sync 2024-09-06 21:27:56 +10:00
psychedelicious
8d0a75cb5f feat(ui): add context menu to entity list 2024-09-06 21:27:56 +10:00
psychedelicious
8a56702341 chore(ui): bump @invoke-ai/ui-library 2024-09-06 21:27:56 +10:00
psychedelicious
af638cf5ce fix(ui): missing vae precision in graph builders 2024-09-06 21:27:56 +10:00
psychedelicious
839248c74c chore: release v4.2.9.dev3
Instead of using dates, just going to increment.
2024-09-06 21:27:56 +10:00
psychedelicious
4b488de10e feat(ui): use new Result utils for enqueueing 2024-09-06 21:27:56 +10:00
psychedelicious
112d6ead91 fix(ui): graph building issue w/ controlnet 2024-09-06 21:27:56 +10:00
psychedelicious
a5e2e78dee feat(ui): add Result type & helpers
Wrappers to capture errors and turn into results:
- `withResult` wraps a sync function
- `withResultAsync` wraps an async function

Comments, tests.
2024-09-06 21:27:56 +10:00
psychedelicious
b91f79b0bb chore: release v4.2.9.dev20240824 2024-09-06 21:27:56 +10:00
psychedelicious
bc75b0259b fix(ui): lint & fix issues with adding regional ip adapters 2024-09-06 21:27:56 +10:00
psychedelicious
51663c5439 feat(ui): add knipignore tag
I'm not ready to delete some things but still want to build the app.
2024-09-06 21:27:56 +10:00
psychedelicious
86a5e2538c feat(ui): duplicate entity 2024-09-06 21:27:56 +10:00
psychedelicious
ee94b0ce17 feat(ui): autocomplete on getPrefixeId 2024-09-06 21:27:56 +10:00
psychedelicious
03b3139705 feat(ui): paste canvas gens back on source in generate mode 2024-09-06 21:27:56 +10:00
psychedelicious
44b8480832 chore(ui): typegen 2024-09-06 21:27:56 +10:00
psychedelicious
e6960320dd feat(nodes): CanvasV2MaskAndCropInvocation can paste generated image back on source
This is needed for `Generate` mode.
2024-09-06 21:27:56 +10:00
psychedelicious
775353fe82 fix(ui): extraneous entity preview updates 2024-09-06 21:27:56 +10:00
psychedelicious
e013aff9fe fix(ui): newly-added entities are selected 2024-09-06 21:27:56 +10:00
psychedelicious
a22bf4c296 feat(ui): add crosshair to color picker 2024-09-06 21:27:56 +10:00
psychedelicious
12a00de2ec fix(ui): color picker ignores alpha 2024-09-06 21:27:56 +10:00
psychedelicious
6cbaf7e0ae fix(ui): calculate renderable entities correctly in tool module 2024-09-06 21:27:56 +10:00
psychedelicious
9b8d814082 feat(ui): better color picker 2024-09-06 21:27:56 +10:00
psychedelicious
20a0ed81c5 feat(ui): colored mask preview image 2024-09-06 21:27:56 +10:00
psychedelicious
a040d1d2a6 fix(ui): new rectangles don't trigger rerender 2024-09-06 21:27:56 +10:00
psychedelicious
7c68b889eb chore: bump version v4.2.9.dev20240823 2024-09-06 21:27:56 +10:00
psychedelicious
5ea3c11883 feat(ui): disable most interaction while filtering 2024-09-06 21:27:37 +10:00
psychedelicious
f355d907e6 fix(ui): filter preview offset 2024-09-06 21:27:37 +10:00
psychedelicious
da5b99c840 feat(ui): tweak layout of staging area toolbar 2024-09-06 21:27:37 +10:00
psychedelicious
4f9c4f7b40 chore(ui): typegen 2024-09-06 21:27:37 +10:00
psychedelicious
143c47c887 tidy(app): clean up app changes for canvas v2 2024-09-06 21:27:37 +10:00
psychedelicious
2ca5aeda96 feat(ui): use singleton for clear q confirm dialog 2024-09-06 21:27:37 +10:00
psychedelicious
9b1ee633e6 fix(ui): rip out broken recall logic, NO TS ERRORS 2024-09-06 21:27:37 +10:00
psychedelicious
c06998184e chore(ui): lint 2024-09-06 21:27:37 +10:00
psychedelicious
84aa2b94e5 fix(ui): staging area interaction scopes 2024-09-06 21:27:37 +10:00
psychedelicious
64f7c7bf36 fix(ui): staging area actions 2024-09-06 21:27:37 +10:00
psychedelicious
18d9263d60 tidy(ui): more cleanup 2024-09-06 21:27:37 +10:00
psychedelicious
e0e8165992 fix(ui): upscale tab graph 2024-09-06 21:27:37 +10:00
psychedelicious
e826f8a020 fix(ui): sdxl graph builder 2024-09-06 21:27:37 +10:00
psychedelicious
9486c50e67 fix(ui): select next entity in the list when deleting 2024-09-06 21:27:37 +10:00
psychedelicious
66424c3c93 feat(ui): fix delete layer hotkey 2024-09-06 21:27:37 +10:00
psychedelicious
cff871e8a6 tidy(ui): "eye dropper" -> "color picker" 2024-09-06 21:27:37 +10:00
psychedelicious
51cd435ad8 tidy(ui): regional guidance buttons 2024-09-06 21:27:37 +10:00
psychedelicious
bc8bf989f3 feat(ui): update entity list menu 2024-09-06 21:27:37 +10:00
psychedelicious
87150b7c6b feat(ui): add log debug button 2024-09-06 21:27:37 +10:00
psychedelicious
5797797904 chore(ui): lint 2024-09-06 21:27:37 +10:00
psychedelicious
ab7b9c4523 chore(ui): prettier 2024-09-06 21:27:37 +10:00
psychedelicious
68bf4459c3 chore(ui): eslint 2024-09-06 21:27:37 +10:00
psychedelicious
05052fd21c tidy(ui): remove unused stuff 4 2024-09-06 21:27:37 +10:00
psychedelicious
f4e3f87c2e tidy(ui): remove unused stuff 3 2024-09-06 21:27:37 +10:00
psychedelicious
f47e1afd57 tidy(ui): remove unused pkg @chakra-ui/react-use-size 2024-09-06 21:27:37 +10:00
psychedelicious
aa4be01bdf feat(ui): revise graph building for control layers, fix issues w/ invocation complete events 2024-09-06 21:27:37 +10:00
psychedelicious
f8d4d5e546 feat(ui): use unique id for metadata in Graph class 2024-09-06 21:27:37 +10:00
psychedelicious
f628b8ad9d tidy(ui): remove unused stuff 2 2024-09-06 21:27:37 +10:00
psychedelicious
bb4eb70a4a tidy(ui): remove unused stuff 2024-09-06 21:27:37 +10:00
psychedelicious
7b1a533bf2 tidy(ui): reduce use of parseify util 2024-09-06 21:27:37 +10:00
psychedelicious
d32526f04a feat(ui): refine canvas entity list items & menus 2024-09-06 21:27:37 +10:00
psychedelicious
76bc2cf5db feat(ui): canvas layer preview, revised reactivity for adapters 2024-09-06 21:27:37 +10:00
psychedelicious
90b3ebb27d feat(ui): add SyncableMap
Can be used with useSyncExternal store to make a `Map` reactive.
2024-09-06 21:27:37 +10:00
psychedelicious
4b65d9145e tidy(ui): removed unused transform methods from canvasmanager 2024-09-06 21:27:37 +10:00
psychedelicious
9badbb8dff feat(ui): transform tool ux 2024-09-06 21:27:37 +10:00
psychedelicious
70bcca23a4 feat(ui): rough out canvas mode 2024-09-06 21:27:37 +10:00
psychedelicious
5ba15d0db5 feat(ui): add canvas autosave checkbox 2024-09-06 21:27:37 +10:00
psychedelicious
fef70fdac0 fix(ui): memory leak when getting image DTO
must unsubscribe!
2024-09-06 21:27:37 +10:00
psychedelicious
62eb00aacf feat(ui): rework settings menu 2024-09-06 21:27:37 +10:00
psychedelicious
3f41ea574b feat(ui): no entities fallback buttons 2024-09-06 21:27:37 +10:00
psychedelicious
e865c1a2b5 perf(ui): optimize gallery image delete button rendering 2024-09-06 21:27:37 +10:00
psychedelicious
f4a798c64f feat(ui): remove "solid" background option 2024-09-06 21:27:37 +10:00
psychedelicious
7806604666 tidy(ui): organise files and classes 2024-09-06 21:27:37 +10:00
psychedelicious
917f8087b4 tidy(ui): abstract compositing logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
b7a9a6153a fix(ui): fix canvas cache property access 2024-09-06 21:27:37 +10:00
psychedelicious
fdc5f89060 tidy(ui): clean up CanvasFilter class 2024-09-06 21:27:37 +10:00
psychedelicious
7fc0959c83 tidy(ui): clean up a few bits and bobs 2024-09-06 21:27:37 +10:00
psychedelicious
95ff64d1d6 tidy(ui): abstract canvas rendering logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
67f94007d0 tidy(ui): abstract caching logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
930f27d009 tidy(ui): abstract worker logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
6125e0fd13 tidy(ui): abstract stage logic into module 2024-09-06 21:27:37 +10:00
psychedelicious
c8e8cf9dfb feat(ui): add entity group hiding 2024-09-06 21:27:37 +10:00
psychedelicious
ac8122a154 feat(ui): move all caching out of redux
While we lose the benefit of the caches persisting across reloads, this is a much simpler way to handle things. If we need a persistent cache, we can explore it in the future.
2024-09-06 21:27:37 +10:00
psychedelicious
aa7c909f14 feat(ui): revised rasterization caching
- use `stable-hash` to generate stable, non-crypto hashes for cache entries, instead of using deep object comparisons
- use an object to store image name caches
2024-09-06 21:27:37 +10:00
psychedelicious
40d294b29c feat(ui): revise filter implementation 2024-09-06 21:27:36 +10:00
psychedelicious
ff1fd5b5b5 fix(ui): add button to delete inpaint mask 2024-09-06 21:27:36 +10:00
psychedelicious
6aa87d7df2 feat(ui): add contexts/hooks to access entity adapters directly 2024-09-06 21:27:36 +10:00
psychedelicious
7732eeb815 feat(ui): add CanvasManagerProviderGate
This context waits to render its children its until the canvas manager is available. Then its children have access to the manager directly via hook.
2024-09-06 21:27:36 +10:00
psychedelicious
9eb37feb6c feat(ui) do not set $canvasManager until ready 2024-09-06 21:27:36 +10:00
psychedelicious
e3b38339a0 fix(ui): inpaint mask naming 2024-09-06 21:27:36 +10:00
psychedelicious
f92695e677 feat(ui): efficient canvas compositing
Also solves issue of exporting layers at different opacities than what is visible
2024-09-06 21:27:36 +10:00
psychedelicious
9473d54fc7 feat(ui): allow multiple inpaint masks
This is easier than making it a nullable singleton
2024-09-06 21:27:36 +10:00
psychedelicious
33783eae4f fix(ui): missing rasterization cache invalidations 2024-09-06 21:27:36 +10:00
psychedelicious
f6c3570c26 feat(ui): iterate on filter UI, flow 2024-09-06 21:27:36 +10:00
psychedelicious
96a76a763e fix(ui): rehydration data loss 2024-09-06 21:27:36 +10:00
psychedelicious
209bc925c0 feat(ui): sort log namespaces 2024-09-06 21:27:36 +10:00
psychedelicious
a77fb427d0 fix(ui): do not merge arrays by index during rehydration 2024-09-06 21:27:36 +10:00
psychedelicious
6fc358b9ea fix(ui): clone parsed data during state rehydration
Without this, the objects and arrays in `parsed` could be mutated, and the log statment would show the mutated data.
2024-09-06 21:27:36 +10:00
psychedelicious
742ac3088d fix(ui): fix logger filter
was accidetnally replacing the filter instead of appending to it.
2024-09-06 21:27:36 +10:00
psychedelicious
8b958e5280 fix(ui): race condition queue status
Sequence of events causing the race condition:
- Enqueue batch
- Invalidate `SessionQueueStatus` tag
- Request updated queue status via HTTP - batch still processing at this point
- Batch completes
- Event emitted saying so
- Optimistically update the queue status cache, it is correct
- HTTP request makes it back and overwrites the optimistic update, indicating the batch is still in progress

FIxed by not invalidating the cache.
2024-09-06 21:27:36 +10:00
psychedelicious
71c727e586 fix(ui): handle opacity for masks 2024-09-06 21:27:36 +10:00
psychedelicious
c590e8bd6f feat(ui): default background to checkerboard 2024-09-06 21:27:36 +10:00
psychedelicious
71a13c3e91 feat(ui): clean up logging namespaces, allow skipping namespaces 2024-09-06 21:27:36 +10:00
psychedelicious
d7e7f049e7 chore(ui): bump ui library 2024-09-06 21:27:36 +10:00
psychedelicious
b23332e167 fix(ui): do not allow drawing if layer disabled 2024-09-06 21:27:36 +10:00
psychedelicious
21c637c365 fix(ui): stale state causing race conditions & extraneous renders 2024-09-06 21:27:36 +10:00
psychedelicious
2b7aecb346 fix(ui): do not clear buffer when rendering "real" objects 2024-09-06 21:27:36 +10:00
psychedelicious
5c669f7925 tidy(ui): remove "filter" from CanvasImageState 2024-09-06 21:27:36 +10:00
psychedelicious
748f445de5 feat(ui): better editable title 2024-09-06 21:27:36 +10:00
psychedelicious
2127ee9a09 fix(ui): stroke eraserline 2024-09-06 21:27:36 +10:00
psychedelicious
7ccb1c5938 feat(ui): restore transparency effect for control layers 2024-09-06 21:27:36 +10:00
psychedelicious
26d1a2ebfe feat(ui): use text cursor for entity title 2024-09-06 21:27:36 +10:00
psychedelicious
e0bd683d88 tidy(ui): remove extraneous logging in CanvasStateApi 2024-09-06 21:27:36 +10:00
psychedelicious
3c5332b236 feat(ui): better buffer commit logic 2024-09-06 21:27:36 +10:00
psychedelicious
d4e847bd9a feat(ui): render buffer separately from "real" objects 2024-09-06 21:27:36 +10:00
psychedelicious
cee16536cd fix(ui): pixelRect should always be integer 2024-09-06 21:27:36 +10:00
psychedelicious
811e75dc67 fix(ui): only update stage attrs when stage itself is dragged 2024-09-06 21:27:36 +10:00
psychedelicious
a4a77d70f7 feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
2024-09-06 21:27:36 +10:00
psychedelicious
13e2de3cb4 fix(ui): various things listening when they need not listen 2024-09-06 21:27:36 +10:00
psychedelicious
7925bbf454 feat(ui): layer opacity via caching 2024-09-06 21:27:36 +10:00
psychedelicious
39eb0a01d2 feat(ui): reset view fits all visible objects 2024-09-06 21:27:36 +10:00
psychedelicious
305e50004a fix(ui): rerenders when changing canvas scale 2024-09-06 21:27:36 +10:00
psychedelicious
9974784596 fix(ui): do not render rasterized layer unless renderObjects=true 2024-09-06 21:27:36 +10:00
psychedelicious
105c5a7fd4 feat(ui): revise app layout strategy, add interaction scopes for hotkeys 2024-09-06 21:27:36 +10:00
psychedelicious
7baad9c72e feat(ui): tweak mask patterns 2024-09-06 21:27:36 +10:00
psychedelicious
a5e8705ea3 fix(ui): dynamic prompts recalcs when presets are loaded 2024-09-06 21:27:36 +10:00
psychedelicious
7b2a5c3a30 fix(ui): use style preset prompts correctly 2024-09-06 21:27:36 +10:00
psychedelicious
3dcb33026a fix(ui): discard selected staging image not all other images 2024-09-06 21:27:36 +10:00
psychedelicious
e956ea5482 fix(ui): respect image size in staging preview 2024-09-06 21:27:36 +10:00
psychedelicious
64f50ab278 tidy(ui): cleanup after events change 2024-09-06 21:27:36 +10:00
psychedelicious
b152937f30 feat(ui): move socket event handling out of redux
Download events and invocation status events (including progress images) are very frequent. There's no real need for these to pass through redux. Handling them outside redux is a significant performance win - far fewer store subscription calls, far fewer trips through middleware.

All event handling is moved outside middleware. Cleanup of unused actions and listeners to follow.
2024-09-06 21:27:36 +10:00
psychedelicious
4e6a9d990c fix(ui): rebase conflicts 2024-09-06 21:27:36 +10:00
psychedelicious
d6fb220b2c fix(ui): update compositing rect when fill changes 2024-09-06 21:27:36 +10:00
psychedelicious
13fc539f8b feat(ui): add canvas background style 2024-09-06 21:27:36 +10:00
psychedelicious
d2b5d6342c feat(ui): mask layers choose own opacity 2024-09-06 21:27:36 +10:00
psychedelicious
eb644d4e6a feat(ui): mask fill patterns 2024-09-06 21:27:36 +10:00
psychedelicious
d8a2efc691 build(ui): add vite types to tsconfig 2024-09-06 21:27:36 +10:00
psychedelicious
163687aef3 fix(ui): do not smooth pixel data when using eyeDropper 2024-09-06 21:27:36 +10:00
psychedelicious
b41f1a897d tidy(ui): tool components & translations 2024-09-06 21:27:36 +10:00
psychedelicious
006a5723da feat(ui): rough out eyedropper tool
It's a bit slow bc we are converting the stage to canvas on every mouse move. Also need to improve the visual but it works.
2024-09-06 21:27:36 +10:00
psychedelicious
daede9c9cf fix(ui): ip adapters work 2024-09-06 21:27:36 +10:00
psychedelicious
af7d14cd59 feat(ui): rename layers 2024-09-06 21:27:36 +10:00
psychedelicious
ba14fe3600 feat(ui): revise entity menus 2024-09-06 21:27:36 +10:00
psychedelicious
af726d1f15 feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers 2024-09-06 21:27:36 +10:00
psychedelicious
3a2efb351d feat(ui): implement cache for image rasterization, rip out some old controladapters code 2024-09-06 21:27:36 +10:00
psychedelicious
c9dc61c311 feat(ui, app): use layer as control (wip) 2024-09-06 21:27:36 +10:00
psychedelicious
500f151d96 feat(ui): add contextmenu for canvas entities 2024-09-06 21:27:36 +10:00
psychedelicious
9708fc5d6c feat(ui): more better logging & naming 2024-09-06 21:27:36 +10:00
psychedelicious
c697501285 feat(ui): better logging w/ path 2024-09-06 21:27:36 +10:00
psychedelicious
e213cfc2ba feat(ui): always show marks on canvas scale slider 2024-09-06 21:27:36 +10:00
psychedelicious
270c1304a8 fix(ui): do not import button from chakra 2024-09-06 21:27:36 +10:00
psychedelicious
b887cf4612 fix(ui): scaled bbox preview 2024-09-06 21:27:36 +10:00
psychedelicious
e0573b721e feat(ui): tidy up atoms 2024-09-06 21:27:36 +10:00
psychedelicious
22712c5dac feat(ui): convert all my pubsubs to atoms
its the same but better
2024-09-06 21:27:36 +10:00
psychedelicious
7339b3d8cc feat(ui): add trnalsation 2024-09-06 21:27:36 +10:00
psychedelicious
0eda34b41f fix(ui): give up on thumbnail loading, causes flash during transformer 2024-09-06 21:27:36 +10:00
psychedelicious
9d9e845198 fix(ui): depth anything v2 2024-09-06 21:27:36 +10:00
psychedelicious
6b7ead4461 tidy(ui): remove unused code, comments 2024-09-06 21:27:36 +10:00
psychedelicious
3ce3056c4a fix(ui): staging area works 2024-09-06 21:27:36 +10:00
psychedelicious
aa73cbf459 feat(nodes): temp disable canvas output crop 2024-09-06 21:27:36 +10:00
psychedelicious
fa3f109eb9 fix(ui): max scale 1 when reset view 2024-09-06 21:27:36 +10:00
psychedelicious
c22afa5725 feat(ui): better scale changer component, reset view functionality 2024-09-06 21:27:36 +10:00
psychedelicious
318086571d fix(ui): img2img 2024-09-06 21:27:36 +10:00
psychedelicious
260ef8edd5 feat(ui): add manual scale controls 2024-09-06 21:27:36 +10:00
psychedelicious
a3a933a797 fix(ui): do not await clearBuffer 2024-09-06 21:27:36 +10:00
psychedelicious
d2d298604c feat(ui): dnd image into layer 2024-09-06 21:27:36 +10:00
psychedelicious
318f2ee003 fix(ui): do not await commitBuffer 2024-09-06 21:27:36 +10:00
psychedelicious
657d59268c fix(ui): properly destroy entities in manager cleanup 2024-09-06 21:27:36 +10:00
psychedelicious
54b7931779 tidy(ui): clearer component names for regional guidance 2024-09-06 21:27:36 +10:00
psychedelicious
aadba55796 tidy(ui): clearer component names for ip adapter 2024-09-06 21:27:36 +10:00
psychedelicious
1a8d65d7a9 tidy(ui): clearer component names for inpaint mask 2024-09-06 21:27:36 +10:00
psychedelicious
36f7d0957a tidy(ui): clearer component names for control adapters 2024-09-06 21:27:36 +10:00
psychedelicious
85a33ff6aa feat(ui): simplify canvas list item headers 2024-09-06 21:27:36 +10:00
psychedelicious
ebef4feddb fix(ui): ip adapter list item 2024-09-06 21:27:36 +10:00
psychedelicious
0a34bebd9c tidy(ui): clean up unused logic 2024-09-06 21:27:36 +10:00
psychedelicious
d6acd96dec feat(ui): clean up state, add mutex for image loading, add thumbnail loading 2024-09-06 21:27:36 +10:00
psychedelicious
3ac88acf11 chore(ui): add async-mutex dep 2024-09-06 21:27:36 +10:00
psychedelicious
2a7cffed2a feat(ui): txt2img, img2img, inpaint & outpaint working 2024-09-06 21:27:36 +10:00
psychedelicious
08c2089b3d feat(ui): no padding on transformer outlines 2024-09-06 21:27:36 +10:00
psychedelicious
87521b07ce feat(ui): restore object count to layer titles 2024-09-06 21:27:36 +10:00
psychedelicious
5de7efc1dc tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" 2024-09-06 21:27:36 +10:00
psychedelicious
d4fb80772e tidy(ui): move transformer statics into class 2024-09-06 21:27:36 +10:00
psychedelicious
97886bf62e tidy(ui): massive cleanup
- create a context for entity identifiers, massively simplifying UI for each entity int he list
- consolidate common redux actions
- remove now-unused code
2024-09-06 21:27:36 +10:00
psychedelicious
83657e0a68 perf(ui): do not add duplicate points to lines 2024-09-06 21:27:36 +10:00
psychedelicious
adf293f6c9 feat(ui): up line tension to 0.3 2024-09-06 21:27:36 +10:00
psychedelicious
d35120feb9 perf(ui): disable stroke, perfect draw on compositing rect 2024-09-06 21:27:36 +10:00
psychedelicious
00020f49fe tidy(ui): remove unused code, initial image 2024-09-06 21:27:36 +10:00
psychedelicious
206d1b231a tidy(ui): remove unused state & actions 2024-09-06 21:27:36 +10:00
psychedelicious
008d8f491c feat(ui): region mask rendering 2024-09-06 21:27:36 +10:00
psychedelicious
ae5543b6fa feat(ui): esc cancels drawing buffer
maybe this is not wanted? we'll see
2024-09-06 21:27:36 +10:00
psychedelicious
2c2e6c5c25 fix(ui): render transformer over objects, fix issue w/ inpaint rect color 2024-09-06 21:27:36 +10:00
psychedelicious
718bed5758 fix(ui): brush preview fill for inpaint/region 2024-09-06 21:27:36 +10:00
psychedelicious
d2c524e7fd fix(ui): no objects rendered until vis toggled 2024-09-06 21:27:36 +10:00
psychedelicious
c172221038 feat(ui): inpaint mask transform 2024-09-06 21:27:36 +10:00
psychedelicious
b006edf9c4 fix(ui): layer accidental early set isFirstRender=false 2024-09-06 21:27:36 +10:00
psychedelicious
9f2dc4d6d1 fix(ui): inpaint mask rendering 2024-09-06 21:27:36 +10:00
psychedelicious
8816d6c0c5 feat(ui): wip inpaint mask uses new API 2024-09-06 21:27:36 +10:00
psychedelicious
6dddad01fc feat(ui): move updatePosition to transformer 2024-09-06 21:27:36 +10:00
psychedelicious
7b7f0a7380 feat(ui): move resetScale to transformer 2024-09-06 21:27:36 +10:00
psychedelicious
8f780e79e7 tidy(ui): more imperative naming 2024-09-06 21:27:36 +10:00
psychedelicious
5a2b00fb95 tidy(ui): use imperative names for setters in stateapi 2024-09-06 21:27:36 +10:00
psychedelicious
25158d4d92 fix(ui): commit drawing buffer on tool change, fixing bbox not calculating 2024-09-06 21:27:36 +10:00
psychedelicious
3c9e9fa746 fix(ui): sync transformer when requesting bbox calc 2024-09-06 21:27:36 +10:00
psychedelicious
e5a7e932cf tidy(ui): rename union CanvasEntity -> CanvasEntityState 2024-09-06 21:27:36 +10:00
psychedelicious
84d15b0c7f fix(ui): request rect calc immediately on transform, hiding rect 2024-09-06 21:27:36 +10:00
psychedelicious
79f216a491 feat(ui): move bbox calculation to transformer 2024-09-06 21:27:36 +10:00
psychedelicious
b86645c1a7 feat(ui): use set for transformer subscriptions 2024-09-06 21:27:36 +10:00
psychedelicious
3f359f1059 tidy(ui): clean up worker tasks when complete 2024-09-06 21:27:36 +10:00
psychedelicious
7c7117c39a tidy(ui): remove unused code in CanvasTool 2024-09-06 21:27:36 +10:00
psychedelicious
92b8c68b94 feat(ui): use pubsub for isTransforming on manager 2024-09-06 21:27:36 +10:00
psychedelicious
7b7d722bae docs(ui): update transformer docstrings 2024-09-06 21:27:36 +10:00
psychedelicious
227c3197b4 feat(ui): revised event pubsub, transformer logic split out 2024-09-06 21:27:35 +10:00
psychedelicious
cab1ba8970 feat(ui): add simple pubsub 2024-09-06 21:27:35 +10:00
psychedelicious
7395c94d00 feat(ui): document & clean up object renderer 2024-09-06 21:27:35 +10:00
psychedelicious
2a2dadb952 feat(ui): split out object renderer 2024-09-06 21:27:35 +10:00
psychedelicious
b60dcf8036 fix(ui): unable to hold shit while transforming to retain ratio 2024-09-06 21:27:35 +10:00
psychedelicious
edeb706d19 tidy(ui): rename canvas stuff 2024-09-06 21:27:35 +10:00
psychedelicious
cd76a2d217 tidy(ui): consolidate getLoggingContext builders 2024-09-06 21:27:35 +10:00
psychedelicious
2aaa6c3986 fix(ui): align all tools to 1px grid
- Offset brush tool by 0.5px when width is odd, ensuring each stroke edge is exactly on a pixel boundary
- Round the rect tool also
2024-09-06 21:27:35 +10:00
psychedelicious
dfdd56dbec feat(ui): disable image smoothing on layers 2024-09-06 21:27:35 +10:00
psychedelicious
f521704ade fix(ui): round position when rasterizing layer 2024-09-06 21:27:35 +10:00
psychedelicious
0e0cf9cd3e feat(ui): continue modularizing transform 2024-09-06 21:27:35 +10:00
psychedelicious
60484bd45e feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions 2024-09-06 21:27:35 +10:00
psychedelicious
5978ba32c0 feat(ui): merge bbox outline into transformer 2024-09-06 21:27:35 +10:00
psychedelicious
45efa8f40d fix(ui): update parent's pos not transformers 2024-09-06 21:27:35 +10:00
psychedelicious
3392e1f0bf feat(ui): merge interaction rect into transformer class 2024-09-06 21:27:35 +10:00
psychedelicious
c734385f18 feat(ui): prepare staging area 2024-09-06 21:27:35 +10:00
psychedelicious
148434d833 feat(ui): typing for logging context 2024-09-06 21:27:35 +10:00
psychedelicious
d5e4a965cc feat(ui): remove inheritance of CanvasObject
JS is terrible
2024-09-06 21:27:35 +10:00
psychedelicious
c8ac4d9e1b feat(ui): split & document transformer logic, iterate on class structures 2024-09-06 21:27:35 +10:00
psychedelicious
1e17461601 feat(ui): rotation snap to nearest 45deg when holding shift 2024-09-06 21:27:35 +10:00
psychedelicious
4432244bc3 feat(ui): expose subscribe method for nanostores 2024-09-06 21:27:35 +10:00
psychedelicious
0d087f84a5 tidy(ui): remove layer scaling reducers 2024-09-06 21:27:35 +10:00
psychedelicious
286deb277b fix(ui): pixel-perfect transforms 2024-09-06 21:27:35 +10:00
psychedelicious
3f2c1139ea fix(ui): layer visibility toggle 2024-09-06 21:27:35 +10:00
psychedelicious
1f3163942a fix(nodes): fix canvas mask erode
it wasn't eroding enough and caused incorrect transparency in result images
2024-09-06 21:27:35 +10:00
psychedelicious
28affcc60a fix(ui): do not reset layer on first render 2024-09-06 21:27:35 +10:00
psychedelicious
f59c2cf7f5 feat(ui): revised logging and naming setup, fix staging area 2024-09-06 21:27:35 +10:00
psychedelicious
86ff52ec36 feat(ui): add repr methods to layer and object classes 2024-09-06 21:27:35 +10:00
psychedelicious
17e7364bdb feat(ui): use nanoid(10) instead of uuidv4 for canvas
Shorter ids makes it much more readable
2024-09-06 21:27:35 +10:00
psychedelicious
71e4b60a30 build(ui): add nanoid as explicit dep 2024-09-06 21:27:35 +10:00
psychedelicious
a3e1ff637d fix(ui): move CanvasImage's konva image to correct object 2024-09-06 21:27:35 +10:00
psychedelicious
a3d2ba1444 fix(ui): prevent flash when applying transform 2024-09-06 21:27:35 +10:00
psychedelicious
9baa594a56 build(ui): add eslint rules for async stuff 2024-09-06 21:27:35 +10:00
psychedelicious
49de11c3ae feat(ui): trying to fix flicker after transform 2024-09-06 21:27:35 +10:00
psychedelicious
690fbdc73d feat(ui): transform cleanup 2024-09-06 21:27:35 +10:00
psychedelicious
4d52824895 feat(ui): fix transform when rotated 2024-09-06 21:27:35 +10:00
psychedelicious
2e13e75fc6 fix(ui): use pixel bbox when image is in layer 2024-09-06 21:27:35 +10:00
psychedelicious
f6f6462590 fix(ui): transforming when axes flipped 2024-09-06 21:27:35 +10:00
psychedelicious
79fee16629 feat(ui): hallelujah (???) 2024-09-06 21:27:35 +10:00
psychedelicious
073f63251a feat(ui): add debug button 2024-09-06 21:27:35 +10:00
psychedelicious
20125dc04b fix(ui): transformer padding 2024-09-06 21:27:35 +10:00
psychedelicious
9f1f8d62f0 feat(ui): wip transform mode 2 2024-09-06 21:27:35 +10:00
psychedelicious
99d432785c feat(ui): wip transform mode 2024-09-06 21:27:35 +10:00
psychedelicious
54e5401a96 feat(ui): wip transform mode 2024-09-06 21:27:35 +10:00
psychedelicious
6b5d7406d6 fix(ui): dnd to canvas broke 2024-09-06 21:27:35 +10:00
psychedelicious
c6bfeba61a fix(ui): conflicts after rebasing 2024-09-06 21:27:35 +10:00
psychedelicious
af1c8cc7e0 fix(ui): imageDropped listener 2024-09-06 21:27:35 +10:00
psychedelicious
879161ed4c wip 2024-09-06 21:27:35 +10:00
psychedelicious
a15944774c fix(ui): transform tool seems to be working 2024-09-06 21:27:35 +10:00
psychedelicious
42612a4f92 fix(ui): move tool fixes, add transform tool 2024-09-06 21:27:35 +10:00
psychedelicious
5c39935e88 feat(ui): move tool now only moves 2024-09-06 21:27:35 +10:00
psychedelicious
35c941c540 feat(ui): layer bbox calc in worker 2024-09-06 21:27:35 +10:00
psychedelicious
92931b0d4d feat(ui): tweaked entity & group selection styles 2024-09-06 21:27:35 +10:00
psychedelicious
7c3d2f5578 feat(ui): canvas entity list headers 2024-09-06 21:27:35 +10:00
psychedelicious
9c809ba147 tidy(ui): CanvasRegion 2024-09-06 21:27:35 +10:00
psychedelicious
25fb1bb837 tidy(ui): CanvasRect 2024-09-06 21:27:35 +10:00
psychedelicious
2d01086a3e tidy(ui): CanvasLayer 2024-09-06 21:27:35 +10:00
psychedelicious
f8af1e9014 tidy(ui): CanvasInpaintMask 2024-09-06 21:27:35 +10:00
psychedelicious
2b34a5c646 tidy(ui): CanvasInitialImage 2024-09-06 21:27:35 +10:00
psychedelicious
22ca3db870 tidy(ui): CanvasImage 2024-09-06 21:27:35 +10:00
psychedelicious
247ca97fbd tidy(ui): CanvasEraserLine 2024-09-06 21:27:35 +10:00
psychedelicious
b346b25a7b tidy(ui): CanvasControlAdapter 2024-09-06 21:27:35 +10:00
psychedelicious
496cf3da4f tidy(ui): CanvasBrushLine 2024-09-06 21:27:35 +10:00
psychedelicious
f1b0130389 tidy(ui): CanvasBbox 2024-09-06 21:27:35 +10:00
psychedelicious
39db3be151 tidy(ui): CanvasBackground 2024-09-06 21:27:35 +10:00
psychedelicious
517ad7e77c tidy(ui): update canvas classes, organise location of konva nodes 2024-09-06 21:27:35 +10:00
psychedelicious
5fa10a3f8e feat(ui): add names to all konva objects
Makes troubleshooting much simpler
2024-09-06 21:27:35 +10:00
psychedelicious
aa3986e9f2 fix(ui): do not await creating new canvas image
If you await this, it causes a race condition where multiple images are created.
2024-09-06 21:27:35 +10:00
psychedelicious
9105c02681 feat(ui): use position and dimensions instead of separate x,y,width,height attrs 2024-09-06 21:27:35 +10:00
psychedelicious
6632727d00 fix(ui): remove weird rtkq hook wrapper
I do not understand why I did that initially but it doesn't work with TS.
2024-09-06 21:27:35 +10:00
psychedelicious
34ccd5aa86 feat(ui): rename types size and position to dimensions and coordinate 2024-09-06 21:27:35 +10:00
psychedelicious
23979bdbee tidy(ui): hide layer settings by default 2024-09-06 21:27:35 +10:00
psychedelicious
dda53292bf fix(ui): layer rendering when starting as disabled 2024-09-06 21:27:35 +10:00
psychedelicious
c98c5f13f7 feat(invocation): reduce canvas v2 mask & crop mask dilation 2024-09-06 21:27:35 +10:00
psychedelicious
2a69967863 feat(ui): de-jank staging area and progress images 2024-09-06 21:27:35 +10:00
psychedelicious
781ef806de feat(ui): update staging handling to work w/ cropped mask 2024-09-06 21:27:35 +10:00
psychedelicious
8794c51e42 chore(ui): typegen 2024-09-06 21:27:35 +10:00
psychedelicious
9e89ddf2f1 feat(app): update CanvasV2MaskAndCropInvocation 2024-09-06 21:27:35 +10:00
psychedelicious
f480a89e7a feat(ui): use new canvas output node 2024-09-06 21:27:35 +10:00
psychedelicious
df44fb9827 chore(ui): typegen 2024-09-06 21:27:35 +10:00
psychedelicious
fca9cacc4e feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput
This handles some masking and cropping that the canvas needs.
2024-09-06 21:27:35 +10:00
psychedelicious
fa7783972b fix(ui): restore nodes output tracking 2024-09-06 21:27:35 +10:00
psychedelicious
e50b4280f2 feat(ui): rip out document size
barely knew ye
2024-09-06 21:27:35 +10:00
psychedelicious
17c9ccfdb0 feat(ui): convert initial image to layer when starting canvas session 2024-09-06 21:27:35 +10:00
psychedelicious
d35af4c048 fix(ui): fix layer transparency calculation 2024-09-06 21:27:35 +10:00
psychedelicious
5fef9bbceb fix(ui): reset initial image when resetting canvas 2024-09-06 21:27:35 +10:00
psychedelicious
507acaa7c9 fix(ui): reset node executions states when loading workflow 2024-09-06 21:27:35 +10:00
psychedelicious
28eb9b62a8 fix(ui): entity display list 2024-09-06 21:27:35 +10:00
psychedelicious
061eeb809f feat(ui): img2img working 2024-09-06 21:27:35 +10:00
psychedelicious
0db5c6ac8e feat(ui): rough out img2img on canvas 2024-09-06 21:27:35 +10:00
psychedelicious
2fd6fd4624 UNDO ME WIP 2024-09-06 21:27:35 +10:00
psychedelicious
db67ae2de4 feat(ui): log invocation source id on socket event 2024-09-06 21:27:35 +10:00
psychedelicious
fedcebbe4d feat(ui): restore document size overlay renderer 2024-09-06 21:27:35 +10:00
psychedelicious
eaaeb356d7 feat(ui): make documnet size a rect 2024-09-06 21:27:35 +10:00
psychedelicious
0cddbc2420 refactor(ui): remove modular imagesize components
This is no longer necessary with canvas v2 and added a ton of extraneous redux actions when changing the image size. Also renamed to document size
2024-09-06 21:27:35 +10:00
psychedelicious
1b44520ff7 feat(ui): initialState is for generation mode 2024-09-06 21:27:35 +10:00
psychedelicious
ee4dc86c15 feat(ui): split out canvas entity list component 2024-09-06 21:27:35 +10:00
psychedelicious
cbc61daa56 feat(ui): hide bbox button when no canvas session active 2024-09-06 21:27:35 +10:00
psychedelicious
f1cd6a06ec tidy(ui): remove unused naming objects/utils
The canvas manager means we don't need to worry about konva node names as we never directly select konva nodes.
2024-09-06 21:27:35 +10:00
psychedelicious
70248b9684 feat(ui): split up tool chooser buttons
Prep for distinct toolbars for generation vs canvas modes
2024-09-06 21:27:35 +10:00
psychedelicious
0a5cf8ae42 feat(ui): add useAssertSingleton util hook
This simple hook asserts that it is only ever called once. Particularly useful for things like hotkeys hooks.
2024-09-06 21:27:35 +10:00
psychedelicious
4dc5b18671 feat(ui): "stagingArea" -> "session" 2024-09-06 21:27:35 +10:00
psychedelicious
daffb950c3 feat(ui): add reset button to canvas 2024-09-06 21:27:35 +10:00
psychedelicious
1a6ebb9606 feat(ui): add snapToRect util 2024-09-06 21:27:35 +10:00
psychedelicious
e397efb622 fix(ui): fiddle with control adapter filters
some jank still
2024-09-06 21:27:35 +10:00
psychedelicious
748cb0bb8d feat(ui): temp disable doc size overlay 2024-09-06 21:27:35 +10:00
psychedelicious
233a449e53 feat(ui): no animation on layer selection
Felt sluggish
2024-09-06 21:27:35 +10:00
psychedelicious
09ad8b6238 feat(ui): use canvas as source for control images (wip) 2024-09-06 21:27:35 +10:00
psychedelicious
778f703d3d fix(ui): control adapter translate & scale 2024-09-06 21:27:35 +10:00
psychedelicious
8234e4e168 tidy(ui): removed unused state related to non-buffered drawing 2024-09-06 21:27:35 +10:00
psychedelicious
08e588a6c4 feat(ui): control adapter image rendering 2024-09-06 21:27:35 +10:00
psychedelicious
43f550e48b fix(ui): do not floor bbox calc, it cuts off the last pixels 2024-09-06 21:27:35 +10:00
psychedelicious
5663029ba6 feat(ui): fix issue where creating line needs 2 points 2024-09-06 21:27:35 +10:00
psychedelicious
0869e23dc1 fix(ui): edge cases when holding shift and drawing lines 2024-09-06 21:27:35 +10:00
psychedelicious
fc4779e095 fix(ui): set buffered rect color to full alpha 2024-09-06 21:27:35 +10:00
psychedelicious
1667603d1c fix(ui): handle mouseup correctly 2024-09-06 21:27:35 +10:00
psychedelicious
81b210cf14 feat(ui): buffered rect drawing 2024-09-06 21:27:35 +10:00
psychedelicious
ca5fde8ef5 fix(ui): buffered drawing edge cases 2024-09-06 21:27:35 +10:00
psychedelicious
fef88734d0 perf(ui): do not use stage.find 2024-09-06 21:27:35 +10:00
psychedelicious
aa1727d16f perf(ui): object groups do not listen 2024-09-06 21:27:35 +10:00
psychedelicious
7c1afb6493 perf(ui): buffered drawing (wip) 2024-09-06 21:27:35 +10:00
psychedelicious
64e7757872 tidy(ui): organise files 2024-09-06 21:27:35 +10:00
psychedelicious
3b08250331 tidy(ui): organise files 2024-09-06 21:27:35 +10:00
psychedelicious
cd02638db6 tidy(ui): organise files 2024-09-06 21:27:35 +10:00
psychedelicious
5109811182 fix(ui): background rendering 2024-09-06 21:27:35 +10:00
psychedelicious
33ba8cabd1 pkg(ui): remove unused deps react-konva & use-image 2024-09-06 21:27:35 +10:00
psychedelicious
dcac6028a2 feat(ui): organize konva state and files 2024-09-06 21:27:35 +10:00
psychedelicious
25ab435129 fix(ui): merge conflicts in image deletion listener 2024-09-06 21:27:35 +10:00
psychedelicious
cea5dc6216 fix(ui): region rendering 2024-09-06 21:27:35 +10:00
psychedelicious
bc525d29e1 fix(ui): inpaint mask rendering 2024-09-06 21:27:35 +10:00
psychedelicious
cdfe0ca150 fix(ui): staging area rendering 2024-09-06 21:27:35 +10:00
psychedelicious
bd679e018d fix(ui): stale selected entity 2024-09-06 21:27:35 +10:00
psychedelicious
a6ee18448a fix(ui): staging area image offset 2024-09-06 21:27:35 +10:00
psychedelicious
7d96b3e89e feat(ui): tweak layer ui component 2024-09-06 21:27:35 +10:00
psychedelicious
e6723f194a fix(ui): resetting layer resets position 2024-09-06 21:27:35 +10:00
psychedelicious
e55541ea87 feat(ui): updated layer list component styling 2024-09-06 21:27:35 +10:00
psychedelicious
2676ff8ee3 feat(ui): transformable layers 2024-09-06 21:27:34 +10:00
psychedelicious
f024fe4488 feat(ui): move tool icon is pointer like in other apps 2024-09-06 21:27:34 +10:00
psychedelicious
0d3dfb8d0f feat(ui): do not floor cursor position 2024-09-06 21:27:34 +10:00
psychedelicious
65e1951f5d feat(ui): disable gallery hotkeys while staging 2024-09-06 21:27:34 +10:00
psychedelicious
fd6eb91f79 feat(ui): revised canvas progress & staging image handling 2024-09-06 21:27:34 +10:00
psychedelicious
2aea0f2ac5 feat(ui): show queue item origin in queue list 2024-09-06 21:27:34 +10:00
psychedelicious
e1b5aa7011 chore(ui): typegen 2024-09-06 21:27:34 +10:00
psychedelicious
cdc4d29745 feat(app): add origin to session queue
The origin is an optional field indicating the queue item's origin. For example, "canvas" when the queue item originated from the canvas or "workflows" when the queue item originated from the workflows tab. If omitted, we assume the queue item originated from the API directly.

- Add migration to add the nullable column to the `session_queue` table.
- Update relevant event payloads with the new field.
- Add `cancel_by_origin` method to `session_queue` service and corresponding route. This is required for the canvas to bail out early when staging images.
- Add `origin` to both `SessionQueueItem` and `Batch` - it needs to be provided initially via the batch and then passed onto the queue item.
-
2024-09-06 21:27:34 +10:00
psychedelicious
5d00792e1f fix(ui): denoise start on outpainting 2024-09-06 21:27:34 +10:00
psychedelicious
1c334f3231 feat(ui): add redux events for queue cleared & batch enqueued socket events 2024-09-06 21:27:34 +10:00
psychedelicious
9ba3182d19 feat(ui): canvas staging area works 2024-09-06 21:27:34 +10:00
psychedelicious
285ad448d7 feat(ui): switch to view tool when staging 2024-09-06 21:27:34 +10:00
psychedelicious
e81fedba43 tidy(ui): disable preview images on every enqueue 2024-09-06 21:27:34 +10:00
psychedelicious
6a47f973b7 feat(ui): rough out save staging image 2024-09-06 21:27:34 +10:00
psychedelicious
7aa918cd46 feat(ui): staging area image visibility toggle 2024-09-06 21:27:34 +10:00
psychedelicious
183c9dd736 fix(ui): batch building after removing canvas files 2024-09-06 21:27:34 +10:00
psychedelicious
5621075cb7 feat(ui): make Graph class's getMetadataNode public 2024-09-06 21:27:34 +10:00
psychedelicious
b13d2087c2 tidy(ui): remove old canvas graphs 2024-09-06 21:27:34 +10:00
psychedelicious
89740af2ab fix(ui): do not select already-selected entity 2024-09-06 21:27:34 +10:00
psychedelicious
4329dfd128 tidy(ui): naming things 2024-09-06 21:27:34 +10:00
psychedelicious
bb18a82a9c tidy(ui): file organisation 2024-09-06 21:27:34 +10:00
psychedelicious
d073fe467d fix(ui): reset cursor pos when fitting document 2024-09-06 21:27:34 +10:00
psychedelicious
e37e885546 feat(ui): staging area works more better 2024-09-06 21:27:34 +10:00
psychedelicious
471ded85f7 feat(ui): staging area barely works 2024-09-06 21:27:34 +10:00
psychedelicious
e084655e69 feat(ui): consolidate konva API 2024-09-06 21:27:34 +10:00
psychedelicious
330acb55f4 feat(ui): consolidate konva API 2024-09-06 21:27:34 +10:00
psychedelicious
e1ace99e05 feat(ui): staging area (rendering wip) 2024-09-06 21:27:34 +10:00
psychedelicious
c0bfa07ea7 tidy(ui): type "Dimensions" -> "Size" 2024-09-06 21:27:34 +10:00
psychedelicious
f4fceac372 feat(ui): add updateNode to Graph 2024-09-06 21:27:34 +10:00
psychedelicious
c1f5345987 feat(ui): sdxl graphs 2024-09-06 21:27:34 +10:00
psychedelicious
05992130d0 feat(ui): sd1 outpaint graph 2024-09-06 21:27:34 +10:00
psychedelicious
9c646712e0 tests(ui): add missing tests for Graph class 2024-09-06 21:27:34 +10:00
psychedelicious
0edff49957 feat(ui): add Graph.getid() util 2024-09-06 21:27:34 +10:00
psychedelicious
0f709cb06a feat(ui): outpaint graph, organize builder a bit 2024-09-06 21:27:34 +10:00
psychedelicious
656dbbb9f1 feat(ui): inpaint sd1 graph 2024-09-06 21:27:34 +10:00
psychedelicious
9497a75c95 feat(ui): temp disable image caching while testing 2024-09-06 21:27:34 +10:00
psychedelicious
30c4ed87b5 feat(ui): txt2img & img2img graphs 2024-09-06 21:27:34 +10:00
psychedelicious
e839765ddc feat(ui): minor change to canvas bbox state type 2024-09-06 21:27:34 +10:00
psychedelicious
055737a6e8 feat(ui): simplified konva node to blob/imagedata utils 2024-09-06 21:27:34 +10:00
psychedelicious
fbc609230a feat(ui): node manager getter/setter 2024-09-06 21:27:34 +10:00
psychedelicious
46f86a54c1 feat(ui): generation mode calculation, fudged graphs 2024-09-06 21:27:34 +10:00
psychedelicious
70f5231020 feat(ui): add utils for getting images from canvas 2024-09-06 21:27:34 +10:00
psychedelicious
0ec0feed2c feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering 2024-09-06 21:27:34 +10:00
psychedelicious
60cd505ee1 feat(ui): revised docstrings for renderers & simplified api 2024-09-06 21:27:34 +10:00
psychedelicious
205a719649 feat(ui): inpaint mask UI components 2024-09-06 21:27:34 +10:00
psychedelicious
af7d222a1e feat(ui): inpaint mask rendering (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
70f430f635 fix(ui): models loaded handler 2024-09-06 21:27:34 +10:00
psychedelicious
2a92a223f6 feat(ui): internal state for inpaint mask 2024-09-06 21:27:34 +10:00
psychedelicious
1ed43614c2 refactor(ui): divvy up canvas state a bit 2024-09-06 21:27:34 +10:00
psychedelicious
3a5295574f feat(ui): get region and base layer canvas to blob logic working 2024-09-06 21:27:34 +10:00
psychedelicious
475b1cb1b8 refactor(ui): node manager handles more tedious annoying stuff 2024-09-06 21:27:34 +10:00
psychedelicious
55260a886d feat(ui): use node manager for addRegions 2024-09-06 21:27:34 +10:00
psychedelicious
f77e03ceb0 feat(ui): persist bbox 2024-09-06 21:27:34 +10:00
psychedelicious
4f7bf5ad58 fix(ui): fix generation graphs 2024-09-06 21:27:34 +10:00
psychedelicious
460ea1aa07 feat(ui): add toggle for clipToBbox 2024-09-06 21:27:34 +10:00
psychedelicious
7aa5f5cb80 feat(ui): rename konva node manager 2024-09-06 21:27:34 +10:00
psychedelicious
9cc4133184 refactor(ui): create classes to abstract mgmt of konva nodes 2024-09-06 21:27:34 +10:00
psychedelicious
0d3721324d tidy(ui): organise renderers 2024-09-06 21:27:34 +10:00
psychedelicious
510249d282 refactor(ui): create entity to konva node map abstraction (wip)
Instead of chaining konva `find` and `findOne` methods, all konva nodes are added to a mapping object. Finding and manipulating them is much simpler.

Done for regions and layers, wip for control adapters.
2024-09-06 21:27:34 +10:00
psychedelicious
77d840593b perf(ui): fix lag w/ region rendering
Needed to memoize these selectors
2024-09-06 21:27:34 +10:00
psychedelicious
8c9a5c4ab5 feat(ui): move canvas fill color picker to right 2024-09-06 21:27:34 +10:00
psychedelicious
ce497fff27 refactor(ui): remove unused ellipse & polygon objects 2024-09-06 21:27:34 +10:00
psychedelicious
c3cada6bd5 fix(ui): incorrect rect/brush/eraser positions 2024-09-06 21:27:34 +10:00
psychedelicious
bbacfe403c refactor(ui): enable global debugging flag 2024-09-06 21:27:34 +10:00
psychedelicious
159031a071 refactor(ui): disable the preview renderer for now 2024-09-06 21:27:34 +10:00
psychedelicious
4067927a23 tweak(ui): canvas editor layout 2024-09-06 21:27:34 +10:00
psychedelicious
d090343083 perf(ui): memoize layeractionsmenu valid actions 2024-09-06 21:27:34 +10:00
psychedelicious
ed130366db refactor(ui): decouple konva renderer from react
Subscribe to redux store directly, skipping all the react overhead.

With react in dev mode, a typical frame while using the brush tool on almost-empty canvas is reduced from ~7.5ms to ~3.5ms. All things considered, this still feels slow, but it's a massive improvement.
2024-09-06 21:27:34 +10:00
psychedelicious
d4ae40fec4 feat(ui): clip lines to bbox 2024-09-06 21:27:34 +10:00
psychedelicious
17c864dba3 fix(ui): document fit positioning 2024-09-06 21:27:34 +10:00
psychedelicious
398d6d1efd feat(ui): document bounds overlay 2024-09-06 21:27:34 +10:00
psychedelicious
30b46b0f9b tidy(ui): background layer 2024-09-06 21:27:34 +10:00
psychedelicious
2d123fa11c refactor(ui): use "entity" instead of "data" for canvas 2024-09-06 21:27:34 +10:00
psychedelicious
352a651ac0 feat(ui): brush size border radius = 1 2024-09-06 21:27:34 +10:00
psychedelicious
26b971978d fix(ui): canvas HUD doesn't interrupt tool 2024-09-06 21:27:34 +10:00
psychedelicious
478324ea62 refactor(ui): split up canvas entity renderers, temp disable preview 2024-09-06 21:27:34 +10:00
psychedelicious
fa447b2813 fix(ui): delete all layers button 2024-09-06 21:27:34 +10:00
psychedelicious
322790bfdb fix(ui): ignore keyboard shortcuts in input/textarea elements 2024-09-06 21:27:34 +10:00
psychedelicious
9ea7c18d66 fix(ui): canvas entity ids getting clobbered 2024-09-06 21:27:34 +10:00
psychedelicious
2a0d16d57d fix(ui): move lora followup fixes 2024-09-06 21:27:34 +10:00
psychedelicious
033e2b27a9 chore(ui): lint 2024-09-06 21:27:34 +10:00
psychedelicious
c7af454bf5 refactor(ui): move loras to canvas slice 2024-09-06 21:27:34 +10:00
psychedelicious
d2be9df7a7 fix(ui): layer is selected when added 2024-09-06 21:27:34 +10:00
psychedelicious
ae81f4e20a feat(ui): r to center & fit stage on document 2024-09-06 21:27:34 +10:00
psychedelicious
261c24b704 feat(ui): better HUD 2024-09-06 21:27:34 +10:00
psychedelicious
cc1995170e fix(ui): always use current brush width when making straight lines 2024-09-06 21:27:34 +10:00
psychedelicious
86c785fded feat(ui): hold shift w/ brush to draw straight line 2024-09-06 21:27:34 +10:00
psychedelicious
31c2b1af19 fix(ui): update bg on canvas resize 2024-09-06 21:27:34 +10:00
psychedelicious
90b973f529 refactor(ui): better hud 2024-09-06 21:27:34 +10:00
psychedelicious
1164763e1a refactor(ui): scaled tool preview border 2024-09-06 21:27:34 +10:00
psychedelicious
b4319630b1 refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code 2024-09-06 21:27:34 +10:00
psychedelicious
0586c6bdf2 refactor(ui): fix more types 2024-09-06 21:27:34 +10:00
psychedelicious
c2f60f33e6 refactor(ui): metadata recall (wip)
just enough let the app run
2024-09-06 21:27:34 +10:00
psychedelicious
b118bc959d refactor(ui): undo/redo button temp fix 2024-09-06 21:27:34 +10:00
psychedelicious
6ef5c2e0c3 refactor(ui): fix renderer stuff 2024-09-06 21:27:34 +10:00
psychedelicious
728fd5b758 refactor(ui): fix misc types 2024-09-06 21:27:34 +10:00
psychedelicious
5bae455e4e refactor(ui): fix gallery stuff 2024-09-06 21:27:34 +10:00
psychedelicious
9387903491 refactor(ui): fix delete image stuff 2024-09-06 21:27:34 +10:00
psychedelicious
12b67bd556 refactor(ui): fix useIsReadyToEnqueue for new adapterType field 2024-09-06 21:27:34 +10:00
psychedelicious
9a30bcbd94 refactor(ui): update generation tab graphs 2024-09-06 21:27:34 +10:00
psychedelicious
ab0f096bf2 refactor(ui): add adapterType to ControlAdapterData 2024-09-06 21:27:34 +10:00
psychedelicious
2d8a29d2fd refactor(ui): update components & logic to use new unified slice (again) 2024-09-06 21:27:34 +10:00
psychedelicious
8d7de1543b refactor(ui): update components & logic to use new unified slice 2024-09-06 21:27:34 +10:00
psychedelicious
b1b41a9b0c refactor(ui): merge compositing, params into canvasV2 slice 2024-09-06 21:27:34 +10:00
psychedelicious
e9033040f6 refactor(ui): add scaled bbox state 2024-09-06 21:27:34 +10:00
psychedelicious
4388f00607 refactor(ui): update dnd/image upload 2024-09-06 21:27:34 +10:00
psychedelicious
9b8db06349 refactor(ui): update size/prompts state 2024-09-06 21:27:34 +10:00
psychedelicious
a4f55f6e5d refactor(ui): rip out old control adapter implementation 2024-09-06 21:27:34 +10:00
psychedelicious
d0cde66e92 refactor(ui): canvas v2 (wip)
fix entity count select
2024-09-06 21:27:34 +10:00
psychedelicious
f636c0eb88 refactor(ui): canvas v2 (wip)
delete unused file
2024-09-06 21:27:34 +10:00
psychedelicious
5760d3180e refactor(ui): canvas v2 (wip)
merge all canvas state reducers into one big slice (but with the logic split across files so it's not hell)
2024-09-06 21:27:34 +10:00
psychedelicious
8c4f98131b refactor(ui): canvas v2 (wip)
Fix a few more components
2024-09-06 21:27:34 +10:00
psychedelicious
959a433e61 refactor(ui): canvas v2 (wip)
missed a spot
2024-09-06 21:27:34 +10:00
psychedelicious
f72845a1b4 refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
2024-09-06 21:27:34 +10:00
psychedelicious
254f4ba574 refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
d6f3b1b85f refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
47b5b7c4b4 refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
bcfdae62e3 refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
c60f1c0031 feat(ui): bbox tool 2024-09-06 21:27:34 +10:00
psychedelicious
d76802f563 fix(ui): rect tool preview 2024-09-06 21:27:34 +10:00
psychedelicious
7b39b31f6c fix(ui): multiple stages 2024-09-06 21:27:34 +10:00
psychedelicious
a2585a8bb1 feat(ui): decouple konva logic from nanostores 2024-09-06 21:27:34 +10:00
psychedelicious
fe0c4767c7 feat(ui): store all stage attrs in nanostores 2024-09-06 21:27:34 +10:00
psychedelicious
e0b8b82a15 feat(ui): round stage scale 2024-09-06 21:27:34 +10:00
psychedelicious
f90fa85e77 chore(ui): bump konva 2024-09-06 21:27:34 +10:00
psychedelicious
6d5b4e4471 feat(ui): generation bbox transformation working
whew
2024-09-06 21:27:34 +10:00
psychedelicious
1172a33117 feat(ui): wip generation bbox 2024-09-06 21:27:34 +10:00
psychedelicious
8530f8ddcc feat(ui): wip generation bbox 2024-09-06 21:27:34 +10:00
psychedelicious
e66e4fefed feat(ui): CL zoom and pan, some rendering optimizations 2024-09-06 21:27:34 +10:00
psychedelicious
1237e839ca Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects"
This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd.
2024-09-06 21:27:34 +10:00
psychedelicious
8ec08063f4 feat(ui): layers manage their own bbox 2024-09-06 21:27:34 +10:00
psychedelicious
0d0004018b docs(ui): konva image object docstrings 2024-09-06 21:27:34 +10:00
psychedelicious
9124604dc4 feat(ui): add x,y,scaleX,scaleY,rotation to objects 2024-09-06 21:27:34 +10:00
psychedelicious
4f05a7b8d0 fix(ui): show color picker when using rect tool 2024-09-06 21:27:34 +10:00
psychedelicious
db766bd9ae feat(ui): image loading fallback for raster layers 2024-09-06 21:27:34 +10:00
psychedelicious
557603d2ae feat(ui): bbox calc for raster layers 2024-09-06 21:27:34 +10:00
psychedelicious
b46ca55c7d feat(ui): do not fill brush preview when drawing 2024-09-06 21:27:33 +10:00
psychedelicious
4788172206 fix(ui): brush spacing handling 2024-09-06 21:27:33 +10:00
psychedelicious
6a118f172e fix(ui): jank when starting a shape when not already focused on stage 2024-09-06 21:27:33 +10:00
psychedelicious
d5abdfa3b0 feat(ui): wip raster layers
I meant to split this up into smaller commits and undo some of it, but I committed afterwards and it's tedious to undo.
2024-09-06 21:27:33 +10:00
psychedelicious
1d24cb94b4 feat(ui): support image objects on raster layers
Just the UI and internal state, not rendering yet.
2024-09-06 21:27:33 +10:00
psychedelicious
86e7f24238 tidy(ui): clean up event handlers
Separate logic for each tool in preparation for ellipse and polygon tools.
2024-09-06 21:27:33 +10:00
psychedelicious
0356f970f3 feat(ui): raster layer reset, object group util 2024-09-06 21:27:33 +10:00
psychedelicious
d340038f48 feat(ui): rect shape preview now has fill 2024-09-06 21:27:33 +10:00
psychedelicious
1d5e8e07c6 feat(ui): cancel shape drawing on esc 2024-09-06 21:27:33 +10:00
psychedelicious
24f17479de feat(ui): temp disable history on CL 2024-09-06 21:27:33 +10:00
psychedelicious
84b8096cc9 feat(ui): raster layer logic
- Deduplicate shared logic
- Split up giant renderers file into separate cohesive files
- Tons of cleanup
- Progress on raster layer functionality
2024-09-06 21:27:33 +10:00
psychedelicious
5c38241e33 feat(ui): add raster layer rendering and interaction (WIP) 2024-09-06 21:27:33 +10:00
psychedelicious
b590c73c08 feat(ui): scaffold out raster layers
Raster layers may have images, lines and shapes. These will replace initial image layers and provide sketching functionality like we have on canvas.
2024-09-06 21:27:33 +10:00
psychedelicious
68fcf9d2df refactor(ui): revise types for line and rect objects
- Create separate object types for brush and eraser lines, instead of a single type that has a `tool` field.
- Create new object type for rect shapes.
- Add logic to schemas to migrate old object types to new.
- Update renderers & reducers.
2024-09-06 21:27:33 +10:00
886 changed files with 26585 additions and 30894 deletions

View File

@@ -11,6 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByDestinationResult,
ClearResult,
EnqueueBatchResult,
PruneResult,
@@ -105,6 +106,21 @@ async def cancel_by_batch_ids(
return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids(queue_id=queue_id, batch_ids=batch_ids)
@session_queue_router.put(
"/{queue_id}/cancel_by_destination",
operation_id="cancel_by_destination",
responses={200: {"model": CancelByBatchIDsResult}},
)
async def cancel_by_destination(
queue_id: str = Path(description="The queue id to perform this operation on"),
destination: str = Query(description="The destination to cancel all queue items for"),
) -> CancelByDestinationResult:
"""Immediately cancels all queue items with the given origin"""
return ApiDependencies.invoker.services.session_queue.cancel_by_destination(
queue_id=queue_id, destination=destination
)
@session_queue_router.put(
"/{queue_id}/clear",
operation_id="clear",

View File

@@ -20,7 +20,6 @@ from typing import (
Type,
TypeVar,
Union,
cast,
)
import semver
@@ -80,7 +79,7 @@ class UIConfigBase(BaseModel):
version: str = Field(
description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
)
node_pack: Optional[str] = Field(default=None, description="Whether or not this is a custom node")
node_pack: str = Field(description="The node pack that this node belongs to, will be 'invokeai' for built-in nodes")
classification: Classification = Field(default=Classification.Stable, description="The node's classification")
model_config = ConfigDict(
@@ -230,18 +229,16 @@ class BaseInvocation(ABC, BaseModel):
@staticmethod
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
"""Adds various UI-facing attributes to the invocation's OpenAPI schema."""
uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None))
if uiconfig is not None:
if uiconfig.title is not None:
schema["title"] = uiconfig.title
if uiconfig.tags is not None:
schema["tags"] = uiconfig.tags
if uiconfig.category is not None:
schema["category"] = uiconfig.category
if uiconfig.node_pack is not None:
schema["node_pack"] = uiconfig.node_pack
schema["classification"] = uiconfig.classification
schema["version"] = uiconfig.version
if title := model_class.UIConfig.title:
schema["title"] = title
if tags := model_class.UIConfig.tags:
schema["tags"] = tags
if category := model_class.UIConfig.category:
schema["category"] = category
if node_pack := model_class.UIConfig.node_pack:
schema["node_pack"] = node_pack
schema["classification"] = model_class.UIConfig.classification
schema["version"] = model_class.UIConfig.version
if "required" not in schema or not isinstance(schema["required"], list):
schema["required"] = []
schema["class"] = "invocation"
@@ -312,7 +309,7 @@ class BaseInvocation(ABC, BaseModel):
json_schema_extra={"field_kind": FieldKind.NodeAttribute},
)
UIConfig: ClassVar[Type[UIConfigBase]]
UIConfig: ClassVar[UIConfigBase]
model_config = ConfigDict(
protected_namespaces=(),
@@ -441,30 +438,25 @@ def invocation(
validate_fields(cls.model_fields, invocation_type)
# Add OpenAPI schema extras
uiconfig_name = cls.__qualname__ + ".UIConfig"
if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconfig_name:
cls.UIConfig = type(uiconfig_name, (UIConfigBase,), {})
cls.UIConfig.title = title
cls.UIConfig.tags = tags
cls.UIConfig.category = category
cls.UIConfig.classification = classification
# Grab the node pack's name from the module name, if it's a custom node
is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations"
if is_custom_node:
cls.UIConfig.node_pack = cls.__module__.split(".")[0]
else:
cls.UIConfig.node_pack = None
uiconfig: dict[str, Any] = {}
uiconfig["title"] = title
uiconfig["tags"] = tags
uiconfig["category"] = category
uiconfig["classification"] = classification
# The node pack is the module name - will be "invokeai" for built-in nodes
uiconfig["node_pack"] = cls.__module__.split(".")[0]
if version is not None:
try:
semver.Version.parse(version)
except ValueError as e:
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
cls.UIConfig.version = version
uiconfig["version"] = version
else:
logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"')
cls.UIConfig.version = "1.0.0"
uiconfig["version"] = "1.0.0"
cls.UIConfig = UIConfigBase(**uiconfig)
if use_cache is not None:
cls.model_fields["use_cache"].default = use_cache

View File

@@ -6,13 +6,19 @@ import cv2
import numpy
from PIL import Image, ImageChops, ImageFilter, ImageOps
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
Classification,
invocation,
invocation_output,
)
from invokeai.app.invocations.constants import IMAGE_MODES
from invokeai.app.invocations.fields import (
ColorField,
FieldDescriptions,
ImageField,
InputField,
OutputField,
WithBoard,
WithMetadata,
)
@@ -1007,3 +1013,62 @@ class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard):
image_dto = context.images.save(image=mask, image_category=ImageCategory.MASK)
return ImageOutput.build(image_dto)
@invocation_output("canvas_v2_mask_and_crop_output")
class CanvasV2MaskAndCropOutput(ImageOutput):
offset_x: int = OutputField(description="The x offset of the image, after cropping")
offset_y: int = OutputField(description="The y offset of the image, after cropping")
@invocation(
"canvas_v2_mask_and_crop",
title="Canvas V2 Mask and Crop",
tags=["image", "mask", "id"],
category="image",
version="1.0.0",
classification=Classification.Prototype,
)
class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Handles Canvas V2 image output masking and cropping"""
source_image: ImageField | None = InputField(
default=None,
description="The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.",
)
generated_image: ImageField = InputField(description="The image to apply the mask to")
mask: ImageField = InputField(description="The mask to apply")
mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by")
def _prepare_mask(self, mask: Image.Image) -> Image.Image:
mask_array = numpy.array(mask)
kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8)
dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3)
dilated_mask = Image.fromarray(dilated_mask_array)
if self.mask_blur > 0:
mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur))
return ImageOps.invert(mask.convert("L"))
def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
if self.source_image:
generated_image = context.images.get_pil(self.generated_image.image_name)
source_image = context.images.get_pil(self.source_image.image_name)
source_image.paste(generated_image, (0, 0), mask)
image_dto = context.images.save(image=source_image)
else:
generated_image = context.images.get_pil(self.generated_image.image_name)
generated_image.putalpha(mask)
image_dto = context.images.save(image=generated_image)
# bbox = image.getbbox()
# image = image.crop(bbox)
return CanvasV2MaskAndCropOutput(
image=ImageField(image_name=image_dto.image_name),
offset_x=0,
offset_y=0,
width=image_dto.width,
height=image_dto.height,
)

View File

@@ -88,6 +88,8 @@ class QueueItemEventBase(QueueEventBase):
item_id: int = Field(description="The ID of the queue item")
batch_id: str = Field(description="The ID of the queue batch")
origin: str | None = Field(default=None, description="The origin of the queue item")
destination: str | None = Field(default=None, description="The destination of the queue item")
class InvocationEventBase(QueueItemEventBase):
@@ -95,8 +97,6 @@ class InvocationEventBase(QueueItemEventBase):
session_id: str = Field(description="The ID of the session (aka graph execution state)")
queue_id: str = Field(description="The ID of the queue")
item_id: int = Field(description="The ID of the queue item")
batch_id: str = Field(description="The ID of the queue batch")
session_id: str = Field(description="The ID of the session (aka graph execution state)")
invocation: AnyInvocation = Field(description="The ID of the invocation")
invocation_source_id: str = Field(description="The ID of the prepared invocation's source node")
@@ -114,6 +114,8 @@ class InvocationStartedEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -147,6 +149,8 @@ class InvocationDenoiseProgressEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -184,6 +188,8 @@ class InvocationCompleteEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -216,6 +222,8 @@ class InvocationErrorEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -253,6 +261,8 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
status=queue_item.status,
error_type=queue_item.error_type,
@@ -279,12 +289,14 @@ class BatchEnqueuedEvent(QueueEventBase):
description="The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)"
)
priority: int = Field(description="The priority of the batch")
origin: str | None = Field(default=None, description="The origin of the batch")
@classmethod
def build(cls, enqueue_result: EnqueueBatchResult) -> "BatchEnqueuedEvent":
return cls(
queue_id=enqueue_result.queue_id,
batch_id=enqueue_result.batch.batch_id,
origin=enqueue_result.batch.origin,
enqueued=enqueue_result.enqueued,
requested=enqueue_result.requested,
priority=enqueue_result.priority,

View File

@@ -6,6 +6,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -95,6 +96,11 @@ class SessionQueueBase(ABC):
"""Cancels all queue items with matching batch IDs"""
pass
@abstractmethod
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
"""Cancels all queue items with the given batch destination"""
pass
@abstractmethod
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
"""Cancels all queue items with matching queue ID"""

View File

@@ -77,6 +77,14 @@ BatchDataCollection: TypeAlias = list[list[BatchDatum]]
class Batch(BaseModel):
batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
origin: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
)
destination: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
)
data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
graph: Graph = Field(description="The graph to initialize the session with")
workflow: Optional[WorkflowWithoutID] = Field(
@@ -195,6 +203,14 @@ class SessionQueueItemWithoutGraph(BaseModel):
status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
priority: int = Field(default=0, description="The priority of this queue item")
batch_id: str = Field(description="The ID of the batch associated with this queue item")
origin: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
)
destination: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
)
session_id: str = Field(
description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
)
@@ -294,6 +310,8 @@ class SessionQueueStatus(BaseModel):
class BatchStatus(BaseModel):
queue_id: str = Field(..., description="The ID of the queue")
batch_id: str = Field(..., description="The ID of the batch")
origin: str | None = Field(..., description="The origin of the batch")
destination: str | None = Field(..., description="The destination of the batch")
pending: int = Field(..., description="Number of queue items with status 'pending'")
in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
completed: int = Field(..., description="Number of queue items with status 'complete'")
@@ -328,6 +346,12 @@ class CancelByBatchIDsResult(BaseModel):
canceled: int = Field(..., description="Number of queue items canceled")
class CancelByDestinationResult(CancelByBatchIDsResult):
"""Result of canceling by a destination"""
pass
class CancelByQueueIDResult(CancelByBatchIDsResult):
"""Result of canceling by queue id"""
@@ -433,6 +457,8 @@ class SessionQueueValueToInsert(NamedTuple):
field_values: Optional[str] # field_values json
priority: int # priority
workflow: Optional[str] # workflow json
origin: str | None
destination: str | None
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
@@ -453,6 +479,8 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
json.dumps(field_values, default=to_jsonable_python) if field_values else None, # field_values (json)
priority, # priority
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
batch.origin, # origin
batch.destination, # destination
)
)
return values_to_insert

View File

@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -127,8 +128,8 @@ class SqliteSessionQueue(SessionQueueBase):
self.__cursor.executemany(
"""--sql
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
values_to_insert,
)
@@ -417,11 +418,7 @@ class SqliteSessionQueue(SessionQueueBase):
)
self.__conn.commit()
if current_queue_item is not None and current_queue_item.batch_id in batch_ids:
batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id)
queue_status = self.get_queue_status(queue_id=queue_id)
self.__invoker.services.events.emit_queue_item_status_changed(
current_queue_item, batch_status, queue_status
)
self._set_queue_item_status(current_queue_item.item_id, "canceled")
except Exception:
self.__conn.rollback()
raise
@@ -429,6 +426,46 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.release()
return CancelByBatchIDsResult(canceled=count)
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
try:
current_queue_item = self.get_current(queue_id)
self.__lock.acquire()
where = """--sql
WHERE
queue_id == ?
AND destination == ?
AND status != 'canceled'
AND status != 'completed'
AND status != 'failed'
"""
params = (queue_id, destination)
self.__cursor.execute(
f"""--sql
SELECT COUNT(*)
FROM session_queue
{where};
""",
params,
)
count = self.__cursor.fetchone()[0]
self.__cursor.execute(
f"""--sql
UPDATE session_queue
SET status = 'canceled'
{where};
""",
params,
)
self.__conn.commit()
if current_queue_item is not None and current_queue_item.destination == destination:
self._set_queue_item_status(current_queue_item.item_id, "canceled")
except Exception:
self.__conn.rollback()
raise
finally:
self.__lock.release()
return CancelByDestinationResult(canceled=count)
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:
current_queue_item = self.get_current(queue_id)
@@ -541,7 +578,9 @@ class SqliteSessionQueue(SessionQueueBase):
started_at,
session_id,
batch_id,
queue_id
queue_id,
origin,
destination
FROM session_queue
WHERE queue_id = ?
"""
@@ -621,7 +660,7 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.acquire()
self.__cursor.execute(
"""--sql
SELECT status, count(*)
SELECT status, count(*), origin, destination
FROM session_queue
WHERE
queue_id = ?
@@ -633,6 +672,8 @@ class SqliteSessionQueue(SessionQueueBase):
result = cast(list[sqlite3.Row], self.__cursor.fetchall())
total = sum(row[1] for row in result)
counts: dict[str, int] = {row[0]: row[1] for row in result}
origin = result[0]["origin"] if result else None
destination = result[0]["destination"] if result else None
except Exception:
self.__conn.rollback()
raise
@@ -641,6 +682,8 @@ class SqliteSessionQueue(SessionQueueBase):
return BatchStatus(
batch_id=batch_id,
origin=origin,
destination=destination,
queue_id=queue_id,
pending=counts.get("pending", 0),
in_progress=counts.get("in_progress", 0),

View File

@@ -17,6 +17,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_11 import
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import build_migration_12
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import build_migration_14
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -51,6 +52,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_12(app_config=config))
migrator.register_migration(build_migration_13())
migrator.register_migration(build_migration_14())
migrator.register_migration(build_migration_15())
migrator.run_migrations()
return db

View File

@@ -0,0 +1,34 @@
import sqlite3
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
class Migration15Callback:
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._add_origin_col(cursor)
def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
"""
- Adds `origin` column to the session queue table.
- Adds `destination` column to the session queue table.
"""
cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
cursor.execute("ALTER TABLE session_queue ADD COLUMN destination TEXT;")
def build_migration_15() -> Migration:
"""
Build the migration from database version 14 to 15.
This migration does the following:
- Adds `origin` column to the session queue table.
- Adds `destination` column to the session queue table.
"""
migration_15 = Migration(
from_version=14,
to_version=15,
callback=Migration15Callback(),
)
return migration_15

View File

@@ -12,6 +12,10 @@ module.exports = {
'i18next/no-literal-string': 'error',
// https://eslint.org/docs/latest/rules/no-console
'no-console': 'error',
// https://eslint.org/docs/latest/rules/no-promise-executor-return
'no-promise-executor-return': 'error',
// https://eslint.org/docs/latest/rules/require-await
'require-await': 'error',
},
overrides: [
/**

View File

@@ -1,5 +1,5 @@
import { PropsWithChildren, memo, useEffect } from 'react';
import { modelChanged } from '../src/features/parameters/store/generationSlice';
import { modelChanged } from '../src/features/controlLayers/store/paramsSlice';
import { useAppDispatch } from '../src/app/store/storeHooks';
import { useGlobalModifiersInit } from '@invoke-ai/ui-library';
/**
@@ -10,7 +10,9 @@ export const ReduxInit = memo((props: PropsWithChildren) => {
const dispatch = useAppDispatch();
useGlobalModifiersInit();
useEffect(() => {
dispatch(modelChanged({ key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }));
dispatch(
modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } })
);
}, []);
return props.children;

View File

@@ -9,6 +9,10 @@ const config: KnipConfig = {
'src/services/api/schema.ts',
'src/features/nodes/types/v1/**',
'src/features/nodes/types/v2/**',
// TODO(psyche): maybe we can clean up these utils after canvas v2 release
'src/features/controlLayers/konva/util.ts',
// TODO(psyche): restore HRF functionality?
'src/features/hrf/**',
],
ignoreBinaries: ['only-allow'],
paths: {

View File

@@ -24,7 +24,7 @@
"build": "pnpm run lint && vite build",
"typegen": "node scripts/typegen.js",
"preview": "vite preview",
"lint:knip": "knip",
"lint:knip": "knip --tags=-knipignore",
"lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:1 src/main.tsx",
"lint:eslint": "eslint --max-warnings=0 .",
"lint:prettier": "prettier --check .",
@@ -52,18 +52,19 @@
}
},
"dependencies": {
"@chakra-ui/react-use-size": "^2.1.0",
"@dagrejs/dagre": "^1.1.3",
"@dagrejs/graphlib": "^2.2.3",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.0.20",
"@invoke-ai/ui-library": "^0.0.29",
"@invoke-ai/ui-library": "^0.0.32",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.1",
"cmdk": "^1.0.0",
"compare-versions": "^6.1.1",
"dateformat": "^5.0.3",
"fracturedjsonjs": "^4.0.2",
@@ -74,6 +75,8 @@
"jsondiffpatch": "^0.6.0",
"konva": "^9.3.14",
"lodash-es": "^4.17.21",
"lru-cache": "^11.0.0",
"nanoid": "^5.0.7",
"nanostores": "^0.11.2",
"new-github-issue-url": "^1.0.0",
"overlayscrollbars": "^2.10.0",
@@ -88,10 +91,8 @@
"react-hotkeys-hook": "4.5.0",
"react-i18next": "^14.1.3",
"react-icons": "^5.2.1",
"react-konva": "^18.2.10",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.0.23",
"react-select": "5.8.0",
"react-use": "^17.5.1",
"react-virtuoso": "^4.9.0",
"reactflow": "^11.11.4",
@@ -102,9 +103,9 @@
"roarr": "^7.21.1",
"serialize-error": "^11.0.3",
"socket.io-client": "^4.7.5",
"stable-hash": "^0.0.4",
"use-debounce": "^10.0.2",
"use-device-pixel-ratio": "^1.1.2",
"use-image": "^1.1.1",
"uuid": "^10.0.0",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.1"
@@ -135,6 +136,7 @@
"@vitest/coverage-v8": "^1.5.0",
"@vitest/ui": "^1.5.0",
"concurrently": "^8.2.2",
"csstype": "^3.1.3",
"dpdm": "^3.14.0",
"eslint": "^8.57.0",
"eslint-plugin-i18next": "^6.0.9",

File diff suppressed because it is too large Load Diff

View File

@@ -80,6 +80,7 @@
"aboutDesc": "Using Invoke for work? Check out:",
"aboutHeading": "Own Your Creative Power",
"accept": "Accept",
"apply": "Apply",
"add": "Add",
"advanced": "Advanced",
"ai": "ai",
@@ -115,6 +116,7 @@
"githubLabel": "Github",
"goTo": "Go to",
"hotkeysLabel": "Hotkeys",
"loadingImage": "Loading Image",
"imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image",
"inpaint": "inpaint",
@@ -132,6 +134,7 @@
"nodes": "Workflows",
"notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
"openInViewer": "Open in Viewer",
"orderBy": "Order By",
"outpaint": "outpaint",
"outputs": "Outputs",
@@ -162,10 +165,10 @@
"alpha": "Alpha",
"selected": "Selected",
"tab": "Tab",
"viewing": "Viewing",
"viewingDesc": "Review images in a large gallery view",
"editing": "Editing",
"editingDesc": "Edit on the Control Layers canvas",
"view": "View",
"viewDesc": "Review images in a large gallery view",
"edit": "Edit",
"editDesc": "Edit on the Canvas",
"comparing": "Comparing",
"comparingDesc": "Comparing two images",
"enabled": "Enabled",
@@ -325,6 +328,14 @@
"canceled": "Canceled",
"completedIn": "Completed in",
"batch": "Batch",
"origin": "Origin",
"destination": "Destination",
"upscaling": "Upscaling",
"canvas": "Canvas",
"generation": "Generation",
"workflows": "Workflows",
"other": "Other",
"gallery": "Gallery",
"batchFieldValues": "Batch Field Values",
"item": "Item",
"session": "Session",
@@ -1004,6 +1015,8 @@
"noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.",
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
"noModelSelected": "No model selected",
"canvasManagerNotLoaded": "Canvas Manager not loaded",
"canvasBusy": "Canvas is busy",
"noPrompts": "No prompts generated",
"noNodesInGraph": "No nodes in graph",
"systemDisconnected": "System disconnected",
@@ -1039,8 +1052,7 @@
"seamlessYAxis": "Seamless Tiling Y Axis",
"seed": "Seed",
"imageActions": "Image Actions",
"sendToImg2Img": "Send to Image to Image",
"sendToUnifiedCanvas": "Send To Unified Canvas",
"sendToCanvas": "Send To Canvas",
"sendToUpscale": "Send To Upscale",
"showOptionsPanel": "Show Side Panel (O or T)",
"shuffle": "Shuffle Seed",
@@ -1100,7 +1112,6 @@
"confirmOnDelete": "Confirm On Delete",
"developer": "Developer",
"displayInProgress": "Display Progress Images",
"enableImageDebugging": "Enable Image Debugging",
"enableInformationalPopovers": "Enable Informational Popovers",
"informationalPopoversDisabled": "Informational Popovers Disabled",
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
@@ -1182,8 +1193,8 @@
"problemSavingMaskDesc": "Unable to export mask",
"prunedQueue": "Pruned Queue",
"resetInitialImage": "Reset Initial Image",
"sentToImageToImage": "Sent To Image To Image",
"sentToUnifiedCanvas": "Sent to Unified Canvas",
"sentToCanvas": "Sent to Canvas",
"sentToUpscale": "Sent to Upscale",
"serverError": "Server Error",
"sessionRef": "Session: {{sessionId}}",
"setAsCanvasInitialImage": "Set as canvas initial image",
@@ -1567,7 +1578,7 @@
"copyToClipboard": "Copy to Clipboard",
"cursorPosition": "Cursor Position",
"darkenOutsideSelection": "Darken Outside Selection",
"discardAll": "Discard All",
"discardAll": "Discard All & Cancel Pending Generations",
"discardCurrent": "Discard Current",
"downloadAsImage": "Download As Image",
"enableMask": "Enable Mask",
@@ -1645,39 +1656,153 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
"deleteAll": "Delete All",
"bookmark": "Bookmark for Quick Switch",
"fitBboxToLayers": "Fit Bbox To Layers",
"removeBookmark": "Remove Bookmark",
"saveCanvasToGallery": "Save Canvas To Gallery",
"saveBboxToGallery": "Save Bbox To Gallery",
"savedToGalleryOk": "Saved to Gallery",
"savedToGalleryError": "Error saving to gallery",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
"clearHistory": "Clear History",
"generateMode": "Generate",
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",
"composeMode": "Compose",
"composeModeDesc": "Compose your work iterative. Generated images are added back to the canvas.",
"autoSave": "Auto-save to Gallery",
"resetCanvas": "Reset Canvas",
"resetAll": "Reset All",
"clearCaches": "Clear Caches",
"recalculateRects": "Recalculate Rects",
"clipToBbox": "Clip Strokes to Bbox",
"compositeMaskedRegions": "Composite Masked Regions",
"addLayer": "Add Layer",
"duplicate": "Duplicate",
"moveToFront": "Move to Front",
"moveToBack": "Move to Back",
"moveForward": "Move Forward",
"moveBackward": "Move Backward",
"brushSize": "Brush Size",
"width": "Width",
"zoom": "Zoom",
"resetView": "Reset View",
"controlLayers": "Control Layers",
"globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative",
"enableAutoNegative": "Enable Auto Negative",
"disableAutoNegative": "Disable Auto Negative",
"deletePrompt": "Delete Prompt",
"resetRegion": "Reset Region",
"debugLayers": "Debug Layers",
"showHUD": "Show HUD",
"rectangle": "Rectangle",
"maskPreviewColor": "Mask Preview Color",
"maskFill": "Mask Fill",
"addPositivePrompt": "Add $t(common.positivePrompt)",
"addNegativePrompt": "Add $t(common.negativePrompt)",
"addIPAdapter": "Add $t(common.ipAdapter)",
"regionalGuidance": "Regional Guidance",
"addRasterLayer": "Add $t(controlLayers.rasterLayer)",
"addControlLayer": "Add $t(controlLayers.controlLayer)",
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
"addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
"raster": "Raster",
"rasterLayer": "Raster Layer",
"controlLayer": "Control Layer",
"inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance",
"ipAdapter": "IP Adapter",
"sendToGallery": "Send To Gallery",
"sendToGalleryDesc": "Generations will be sent to the gallery.",
"sendToCanvas": "Send To Canvas",
"sendToCanvasDesc": "Generations will be staged onto the canvas.",
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)",
"rasterLayer_withCount_other": "Raster Layers",
"controlLayer_withCount_other": "Control Layers",
"inpaintMask_withCount_other": "Inpaint Masks",
"regionalGuidance_withCount_other": "Regional Guidance",
"ipAdapter_withCount_other": "IP Adapters",
"opacity": "Opacity",
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
"globalIPAdapters_withCount_hidden": "Global IP Adapters ({{count}} hidden)",
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
"controlLayers_withCount_visible": "Control Layers ({{count}})",
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
"globalIPAdapters_withCount_visible": "Global IP Adapters ({{count}})",
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"globalInitialImage": "Global Initial Image",
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
"layer": "Layer",
"opacityFilter": "Opacity Filter",
"clearProcessor": "Clear Processor",
"resetProcessor": "Reset Processor to Defaults",
"noLayersAdded": "No Layers Added",
"layers_one": "Layer",
"layers_other": "Layers"
"layer_one": "Layer",
"layer_other": "Layers",
"objects_zero": "empty",
"objects_one": "{{count}} object",
"objects_other": "{{count}} objects",
"convertToControlLayer": "Convert to Control Layer",
"convertToRasterLayer": "Convert to Raster Layer",
"transparency": "Transparency",
"enableTransparencyEffect": "Enable Transparency Effect",
"disableTransparencyEffect": "Disable Transparency Effect",
"hidingType": "Hiding {{type}}",
"showingType": "Showing {{type}}",
"dynamicGrid": "Dynamic Grid",
"logDebugInfo": "Log Debug Info",
"locked": "Locked",
"unlocked": "Unlocked",
"deleteSelected": "Delete Selected",
"deleteAll": "Delete All",
"flipHorizontal": "Flip Horizontal",
"flipVertical": "Flip Vertical",
"fill": {
"fillColor": "Fill Color",
"fillStyle": "Fill Style",
"solid": "Solid",
"grid": "Grid",
"crosshatch": "Crosshatch",
"vertical": "Vertical",
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"tool": {
"brush": "Brush",
"eraser": "Eraser",
"rectangle": "Rectangle",
"bbox": "Bbox",
"move": "Move",
"view": "View",
"colorPicker": "Color Picker"
},
"filter": {
"filter": "Filter",
"filters": "Filters",
"filterType": "Filter Type",
"preview": "Preview",
"apply": "Apply",
"cancel": "Cancel"
},
"transform": {
"transform": "Transform",
"fitToBbox": "Fit to Bbox",
"reset": "Reset",
"apply": "Apply",
"cancel": "Cancel"
}
},
"upscaling": {
"upscale": "Upscale",
@@ -1765,5 +1890,30 @@
"upscaling": "Upscaling",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"system": {
"enableLogging": "Enable Logging",
"logLevel": {
"logLevel": "Log Level",
"trace": "Trace",
"debug": "Debug",
"info": "Info",
"warn": "Warn",
"error": "Error",
"fatal": "Fatal"
},
"logNamespaces": {
"logNamespaces": "Log Namespaces",
"gallery": "Gallery",
"models": "Models",
"config": "Config",
"canvas": "Canvas",
"generation": "Generation",
"workflows": "Workflows",
"system": "System",
"events": "Events",
"queue": "Queue",
"metadata": "Metadata"
}
}
}

View File

@@ -38,7 +38,7 @@ async function generateTypes(schema) {
process.stdout.write(`\nOK!\r\n`);
}
async function main() {
function main() {
const encoding = 'utf-8';
if (process.stdin.isTTY) {

View File

@@ -6,6 +6,7 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { PartialAppConfig } from 'app/types/invokeai';
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
@@ -13,13 +14,16 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import SettingsModal from 'features/system/components/SettingsModal/SettingsModal';
import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { selectLanguage } from 'features/system/store/systemSelectors';
import { AppContent } from 'features/ui/components/AppContent';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
import { AnimatePresence } from 'framer-motion';
import i18n from 'i18n';
@@ -41,7 +45,7 @@ interface Props {
};
selectedWorkflowId?: string;
selectedStylePresetId?: string;
destination?: InvokeTabName | undefined;
destination?: TabName;
}
const App = ({
@@ -51,7 +55,7 @@ const App = ({
selectedStylePresetId,
destination,
}: Props) => {
const language = useAppSelector(languageSelector);
const language = useAppSelector(selectLanguage);
const logger = useLogger('system');
const dispatch = useAppDispatch();
const clearStorage = useClearStorage();
@@ -107,6 +111,7 @@ const App = ({
useStarterModelsToast();
useSyncQueueStatus();
useScopeFocusWatcher();
return (
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
@@ -119,7 +124,7 @@ const App = ({
{...dropzone.getRootProps()}
>
<input {...dropzone.getInputProps()} />
<InvokeTabs />
<AppContent />
<AnimatePresence>
{dropzone.isDragActive && isHandlingUpload && (
<ImageUploadOverlay dropzone={dropzone} setIsHandlingUpload={setIsHandlingUpload} />
@@ -130,7 +135,10 @@ const App = ({
<ChangeBoardModal />
<DynamicPromptsModal />
<StylePresetModal />
<ClearQueueConfirmationsAlertDialog />
<PreselectedImage selectedImage={selectedImage} />
<SettingsModal />
<RefreshAfterResetModal />
</ErrorBoundary>
);
};

View File

@@ -1,5 +1,7 @@
import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { toast } from 'features/toast/toast';
import newGithubIssueUrl from 'new-github-issue-url';
import InvokeLogoYellow from 'public/assets/images/invoke-symbol-ylw-lrg.svg';
@@ -13,9 +15,11 @@ type Props = {
resetErrorBoundary: () => void;
};
const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal);
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
const { t } = useTranslation();
const isLocal = useAppSelector((s) => s.config.isLocal);
const isLocal = useAppSelector(selectIsLocal);
const handleCopy = useCallback(() => {
const text = JSON.stringify(serializeError(error), null, 2);

View File

@@ -19,7 +19,7 @@ import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import AppDndContext from 'features/dnd/components/AppDndContext';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { TabName } from 'features/ui/store/uiTypes';
import type { PropsWithChildren, ReactNode } from 'react';
import React, { lazy, memo, useEffect, useMemo } from 'react';
import { Provider } from 'react-redux';
@@ -46,7 +46,7 @@ interface Props extends PropsWithChildren {
};
selectedWorkflowId?: string;
selectedStylePresetId?: string;
destination?: InvokeTabName;
destination?: TabName;
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
isDebugging?: boolean;

View File

@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { $authToken } from 'app/store/nanostores/authToken';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppStore } from 'app/store/nanostores/store';
import type { MapStore } from 'nanostores';
import { atom, map } from 'nanostores';
import { useEffect, useMemo } from 'react';
@@ -18,14 +18,19 @@ declare global {
}
}
export type AppSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
export const $socket = atom<AppSocket | null>(null);
export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
const $isSocketInitialized = atom<boolean>(false);
export const $isConnected = atom<boolean>(false);
/**
* Initializes the socket.io connection and sets up event listeners.
*/
export const useSocketIO = () => {
const dispatch = useAppDispatch();
const { dispatch, getState } = useAppStore();
const baseUrl = useStore($baseUrl);
const authToken = useStore($authToken);
const addlSocketOptions = useStore($socketOptions);
@@ -61,8 +66,9 @@ export const useSocketIO = () => {
return;
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(socketUrl, socketOptions);
setEventListeners({ dispatch, socket });
const socket: AppSocket = io(socketUrl, socketOptions);
$socket.set(socket);
setEventListeners({ socket, dispatch, getState, setIsConnected: $isConnected.set });
socket.connect();
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
@@ -84,5 +90,5 @@ export const useSocketIO = () => {
socket.disconnect();
$isSocketInitialized.set(false);
};
}, [dispatch, socketOptions, socketUrl]);
}, [dispatch, getState, socketOptions, socketUrl]);
};

View File

@@ -15,21 +15,21 @@ export const BASE_CONTEXT = {};
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
export type LoggerNamespace =
| 'images'
| 'models'
| 'config'
| 'canvas'
| 'generation'
| 'nodes'
| 'system'
| 'socketio'
| 'session'
| 'queue'
| 'dnd'
| 'controlLayers';
export const zLogNamespace = z.enum([
'canvas',
'config',
'events',
'gallery',
'generation',
'metadata',
'models',
'system',
'queue',
'workflows',
]);
export type LogNamespace = z.infer<typeof zLogNamespace>;
export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
export const logger = (namespace: LogNamespace) => $logger.get().child({ namespace });
export const zLogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
export type LogLevel = z.infer<typeof zLogLevel>;

View File

@@ -1,29 +1,41 @@
import { createLogWriter } from '@roarr/browser-log-writer';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectSystemLogIsEnabled,
selectSystemLogLevel,
selectSystemLogNamespaces,
} from 'features/system/store/systemSlice';
import { useEffect, useMemo } from 'react';
import { ROARR, Roarr } from 'roarr';
import type { LoggerNamespace } from './logger';
import type { LogNamespace } from './logger';
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger';
export const useLogger = (namespace: LoggerNamespace) => {
const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel);
const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
export const useLogger = (namespace: LogNamespace) => {
const logLevel = useAppSelector(selectSystemLogLevel);
const logNamespaces = useAppSelector(selectSystemLogNamespaces);
const logIsEnabled = useAppSelector(selectSystemLogIsEnabled);
// The provided Roarr browser log writer uses localStorage to config logging to console
useEffect(() => {
if (shouldLogToConsole) {
if (logIsEnabled) {
// Enable console log output
localStorage.setItem('ROARR_LOG', 'true');
// Use a filter to show only logs of the given level
localStorage.setItem('ROARR_FILTER', `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`);
let filter = `context.logLevel:>=${LOG_LEVEL_MAP[logLevel]}`;
if (logNamespaces.length > 0) {
filter += ` AND (${logNamespaces.map((ns) => `context.namespace:${ns}`).join(' OR ')})`;
} else {
filter += ' AND context.namespace:undefined';
}
localStorage.setItem('ROARR_FILTER', filter);
} else {
// Disable console log output
localStorage.setItem('ROARR_LOG', 'false');
}
ROARR.write = createLogWriter();
}, [consoleLogLevel, shouldLogToConsole]);
}, [logLevel, logIsEnabled, logNamespaces]);
// Update the module-scoped logger context as needed
useEffect(() => {

View File

@@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { TabName } from 'features/ui/store/uiTypes';
export const enqueueRequested = createAction<{
tabName: InvokeTabName;
tabName: TabName;
prepend: boolean;
}>('app/enqueueRequested');

View File

@@ -1,2 +1,3 @@
export const STORAGE_PREFIX = '@@invokeai-';
export const EMPTY_ARRAY = [];
export const EMPTY_OBJECT = {};

View File

@@ -1,5 +1,6 @@
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
import type { RootState } from 'app/store/store';
import { isEqual } from 'lodash-es';
/**
@@ -19,3 +20,5 @@ export const getSelectorsOptions: GetSelectorsOptions = {
argsMemoize: lruMemoize,
}),
};
export const createMemoizedAppSelector = createMemoizedSelector.withTypes<RootState>();

View File

@@ -1,5 +1,4 @@
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { PersistError, RehydrateError } from 'redux-remember';
import { serializeError } from 'serialize-error';
@@ -41,6 +40,6 @@ export const errorHandler = (err: PersistError | RehydrateError) => {
} else if (err instanceof RehydrateError) {
log.error({ error: serializeError(err) }, 'Problem rehydrating state');
} else {
log.error({ error: parseify(err) }, 'Problem in persistence layer');
log.error({ error: serializeError(err) }, 'Problem in persistence layer');
}
};

View File

@@ -1,9 +1,7 @@
import type { UnknownAction } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
import { appInfoApi } from 'services/api/endpoints/appInfo';
import type { Graph } from 'services/api/types';
import { socketGeneratorProgress } from 'services/events/actions';
export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
if (isAnyGraphBuilt(action)) {
@@ -24,13 +22,5 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
};
}
if (socketGeneratorProgress.match(action)) {
const sanitized = deepClone(action);
if (sanitized.payload.data.progress_image) {
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
}
return sanitized;
}
return action;
};

View File

@@ -1,7 +1,7 @@
import type { TypedStartListening } from '@reduxjs/toolkit';
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
@@ -9,17 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addCanvasCopiedToClipboardListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard';
import { addCanvasDownloadedAsImageListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage';
import { addCanvasImageToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet';
import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery';
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor';
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
@@ -37,16 +26,7 @@ import { addModelSelectedListener } from 'app/store/middleware/listenerMiddlewar
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings';
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected';
import { addSocketDisconnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected';
import { addGeneratorProgressEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress';
import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete';
import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError';
import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted';
import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall';
import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad';
import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged';
import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved';
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected';
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
import type { AppDispatch, RootState } from 'app/store/store';
@@ -83,7 +63,6 @@ addGalleryImageClickedListener(startAppListening);
addGalleryOffsetChangedListener(startAppListening);
// User Invoked
addEnqueueRequestedCanvasListener(startAppListening);
addEnqueueRequestedNodes(startAppListening);
addEnqueueRequestedLinear(startAppListening);
addEnqueueRequestedUpscale(startAppListening);
@@ -91,31 +70,22 @@ addAnyEnqueuedListener(startAppListening);
addBatchEnqueuedListener(startAppListening);
// Canvas actions
addCanvasSavedToGalleryListener(startAppListening);
addCanvasMaskSavedToGalleryListener(startAppListening);
addCanvasImageToControlNetListener(startAppListening);
addCanvasMaskToControlNetListener(startAppListening);
addCanvasDownloadedAsImageListener(startAppListening);
addCanvasCopiedToClipboardListener(startAppListening);
addCanvasMergedListener(startAppListening);
addStagingAreaImageSavedListener(startAppListening);
addCommitStagingAreaImageListener(startAppListening);
// addCanvasSavedToGalleryListener(startAppListening);
// addCanvasMaskSavedToGalleryListener(startAppListening);
// addCanvasImageToControlNetListener(startAppListening);
// addCanvasMaskToControlNetListener(startAppListening);
// addCanvasDownloadedAsImageListener(startAppListening);
// addCanvasCopiedToClipboardListener(startAppListening);
// addCanvasMergedListener(startAppListening);
// addStagingAreaImageSavedListener(startAppListening);
// addCommitStagingAreaImageListener(startAppListening);
addStagingListeners(startAppListening);
// Socket.IO
addGeneratorProgressEventListener(startAppListening);
addInvocationCompleteEventListener(startAppListening);
addInvocationErrorEventListener(startAppListening);
addInvocationStartedEventListener(startAppListening);
addSocketConnectedEventListener(startAppListening);
addSocketDisconnectedEventListener(startAppListening);
addModelLoadEventListener(startAppListening);
addModelInstallEventListener(startAppListening);
addSocketQueueItemStatusChangedEventListener(startAppListening);
addBulkDownloadListeners(startAppListening);
// ControlNet
addControlNetImageProcessedListener(startAppListening);
addControlNetAutoProcessListener(startAppListening);
// Gallery bulk download
addBulkDownloadListeners(startAppListening);
// Boards
addImageAddedToBoardFulfilledListener(startAppListening);
@@ -148,4 +118,4 @@ addAdHocPostProcessingRequestedListener(startAppListening);
addDynamicPromptsListener(startAppListening);
addSetDefaultSettingsListener(startAppListening);
addControlAdapterPreprocessor(startAppListening);
// addControlAdapterPreprocessor(startAppListening);

View File

@@ -1,21 +1,21 @@
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import type { SerializableObject } from 'common/types';
import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO } from 'services/api/types';
const log = logger('queue');
export const adHocPostProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`upscaling/postProcessingRequested`);
export const addAdHocPostProcessingRequestedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: adHocPostProcessingRequested,
effect: async (action, { dispatch, getState }) => {
const log = logger('session');
const { imageDTO } = action.payload;
const state = getState();
@@ -39,9 +39,9 @@ export const addAdHocPostProcessingRequestedListener = (startAppListening: AppSt
const enqueueResult = await req.unwrap();
req.reset();
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
log.debug({ enqueueResult } as SerializableObject, t('queue.graphQueued'));
} catch (error) {
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
log.error({ enqueueBatchArg } as SerializableObject, t('queue.graphFailedToQueue'));
if (error instanceof Object && 'status' in error && error.status === 403) {
return;

View File

@@ -23,7 +23,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
*/
startAppListening({
matcher: matchAnyBoardDeleted,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const state = getState();
const deletedBoardId = action.meta.arg.originalArgs;
const { autoAddBoardId, selectedBoardId } = state.gallery;
@@ -44,7 +44,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
// If we archived a board, it may end up hidden. If it's selected or the auto-add board, we should reset those.
startAppListening({
matcher: boardsApi.endpoints.updateBoard.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const state = getState();
const { shouldShowArchivedBoards } = state.gallery;
@@ -61,7 +61,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
// When we hide archived boards, if the selected or the auto-add board is archived, we should reset those.
startAppListening({
actionCreator: shouldShowArchivedBoardsChanged,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const shouldShowArchivedBoards = action.payload;
// We only need to take action if we have just hidden archived boards.
@@ -100,7 +100,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
*/
startAppListening({
matcher: boardsApi.endpoints.listAllBoards.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const boards = action.payload;
const state = getState();
const { selectedBoardId, autoAddBoardId } = state.gallery;

View File

@@ -2,32 +2,39 @@ import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
canvasBatchIdsReset,
commitStagingAreaImage,
discardStagedImages,
resetCanvas,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
sessionStagingAreaImageAccepted,
sessionStagingAreaReset,
} from 'features/controlLayers/store/canvasSessionSlice';
import { canvasReset, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
import { $lastCanvasProgressEvent } from 'services/events/setEventListeners';
import { assert } from 'tsafe';
const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage);
const log = logger('canvas');
export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => {
const matchCanvasOrStagingAreaRest = isAnyOf(sessionStagingAreaReset, canvasReset);
export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
matcher,
effect: async (_, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { batchIds } = state.canvas;
matcher: matchCanvasOrStagingAreaRest,
effect: async (_, { dispatch }) => {
try {
const req = dispatch(
queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' })
queueApi.endpoints.cancelByBatchDestination.initiate(
{ destination: 'canvas' },
{ fixedCacheKey: 'cancelByBatchOrigin' }
)
);
const { canceled } = await req.unwrap();
req.reset();
$lastCanvasProgressEvent.set(null);
if (canceled > 0) {
log.debug(`Canceled ${canceled} canvas batches`);
toast({
@@ -36,7 +43,6 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
status: 'success',
});
}
dispatch(canvasBatchIdsReset());
} catch {
log.error('Failed to cancel canvas batches');
toast({
@@ -47,4 +53,26 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
}
},
});
startAppListening({
actionCreator: sessionStagingAreaImageAccepted,
effect: (action, api) => {
const { index } = action.payload;
const state = api.getState();
const stagingAreaImage = state.canvasSession.stagedImages[index];
assert(stagingAreaImage, 'No staged image found to accept');
const { x, y } = selectCanvasSlice(state).bbox.rect;
const { imageDTO, offsetX, offsetY } = stagingAreaImage;
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasRasterLayerState> = {
position: { x: x + offsetX, y: y + offsetY },
objects: [imageObject],
};
api.dispatch(rasterLayerAdded({ overrides, isSelected: false }));
api.dispatch(sessionStagingAreaReset());
},
});
};

View File

@@ -4,7 +4,7 @@ import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
export const addAnyEnqueuedListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
effect: async (_, { dispatch, getState }) => {
effect: (_, { dispatch, getState }) => {
const { data } = selectQueueStatus(getState());
if (!data || data.processor.is_started) {

View File

@@ -1,14 +1,14 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setInfillMethod } from 'features/parameters/store/generationSlice';
import { setInfillMethod } from 'features/controlLayers/store/paramsSlice';
import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice';
import { appInfoApi } from 'services/api/endpoints/appInfo';
export const addAppConfigReceivedListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
effect: (action, { getState, dispatch }) => {
const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload;
const infillMethod = getState().generation.infillMethod;
const infillMethod = getState().params.infillMethod;
if (!infill_methods.includes(infillMethod)) {
// if there is no infill method, set it to the first one

View File

@@ -6,7 +6,7 @@ export const appStarted = createAction('app/appStarted');
export const addAppStartedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: appStarted,
effect: async (action, { unsubscribe, cancelActiveListeners }) => {
effect: (action, { unsubscribe, cancelActiveListeners }) => {
// this should only run once
cancelActiveListeners();
unsubscribe();

View File

@@ -1,27 +1,30 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import type { SerializableObject } from 'common/types';
import { zPydanticValidationError } from 'features/system/store/zodSchemas';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { truncate, upperFirst } from 'lodash-es';
import { serializeError } from 'serialize-error';
import { queueApi } from 'services/api/endpoints/queue';
const log = logger('queue');
export const addBatchEnqueuedListener = (startAppListening: AppStartListening) => {
// success
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
effect: async (action) => {
const response = action.payload;
effect: (action) => {
const enqueueResult = action.payload;
const arg = action.meta.arg.originalArgs;
logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
log.debug({ enqueueResult } as SerializableObject, 'Batch enqueued');
toast({
id: 'QUEUE_BATCH_SUCCEEDED',
title: t('queue.batchQueued'),
status: 'success',
description: t('queue.batchQueuedDesc', {
count: response.enqueued,
count: enqueueResult.enqueued,
direction: arg.prepend ? t('queue.front') : t('queue.back'),
}),
});
@@ -31,9 +34,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
// error
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchRejected,
effect: async (action) => {
effect: (action) => {
const response = action.payload;
const arg = action.meta.arg.originalArgs;
const batchConfig = action.meta.arg.originalArgs;
if (!response) {
toast({
@@ -42,7 +45,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
status: 'error',
description: t('common.unknownError'),
});
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
log.error({ batchConfig } as SerializableObject, t('queue.batchFailedToQueue'));
return;
}
@@ -68,7 +71,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
description: t('common.unknownError'),
});
}
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
log.error({ batchConfig, error: serializeError(response) } as SerializableObject, t('queue.batchFailedToQueue'));
},
});
};

View File

@@ -1,47 +1,31 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { imagesApi } from 'services/api/endpoints/images';
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { deleted_images } = action.payload;
// Remove all deleted images from the UI
let wasCanvasReset = false;
let wasNodeEditorReset = false;
let wereControlAdaptersReset = false;
let wereControlLayersReset = false;
const { canvas, nodes, controlAdapters, controlLayers } = getState();
const state = getState();
const nodes = selectNodesSlice(state);
const canvas = selectCanvasSlice(state);
deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name);
if (imageUsage.isCanvasImage && !wasCanvasReset) {
dispatch(resetCanvas());
wasCanvasReset = true;
}
const imageUsage = getImageUsage(nodes, canvas, image_name);
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
dispatch(nodeEditorReset());
wasNodeEditorReset = true;
}
if (imageUsage.isControlImage && !wereControlAdaptersReset) {
dispatch(controlAdaptersReset());
wereControlAdaptersReset = true;
}
if (imageUsage.isControlLayerImage && !wereControlLayersReset) {
dispatch(allLayersDeleted());
wereControlLayersReset = true;
}
});
},
});

View File

@@ -1,21 +1,15 @@
import { ExternalLink } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
import {
socketBulkDownloadComplete,
socketBulkDownloadError,
socketBulkDownloadStarted,
} from 'services/events/actions';
const log = logger('images');
const log = logger('gallery');
export const addBulkDownloadListeners = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled,
effect: async (action) => {
effect: (action) => {
log.debug(action.payload, 'Bulk download requested');
// If we have an item name, we are processing the bulk download locally and should use it as the toast id to
@@ -33,7 +27,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
startAppListening({
matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected,
effect: async () => {
effect: () => {
log.debug('Bulk download request failed');
// There isn't any toast to update if we get this event.
@@ -44,55 +38,4 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
});
},
});
startAppListening({
actionCreator: socketBulkDownloadStarted,
effect: async (action) => {
// This should always happen immediately after the bulk download request, so we don't need to show a toast here.
log.debug(action.payload.data, 'Bulk download preparation started');
},
});
startAppListening({
actionCreator: socketBulkDownloadComplete,
effect: async (action) => {
log.debug(action.payload.data, 'Bulk download preparation completed');
const { bulk_download_item_name } = action.payload.data;
// TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first
const url = `/api/v1/images/download/${bulk_download_item_name}`;
toast({
id: bulk_download_item_name,
title: t('gallery.bulkDownloadReady', 'Download ready'),
status: 'success',
description: (
<ExternalLink
label={t('gallery.clickToDownload', 'Click here to download')}
href={url}
download={bulk_download_item_name}
/>
),
duration: null,
});
},
});
startAppListening({
actionCreator: socketBulkDownloadError,
effect: async (action) => {
log.debug(action.payload.data, 'Bulk download preparation failed');
const { bulk_download_item_name } = action.payload.data;
toast({
id: bulk_download_item_name,
title: t('gallery.bulkDownloadFailed'),
status: 'error',
description: action.payload.data.error,
duration: null,
});
},
});
};

View File

@@ -1,38 +0,0 @@
import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasCopiedToClipboard,
effect: async (action, { getState }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const state = getState();
try {
const blob = getBaseLayerBlob(state);
copyBlobToClipboard(blob);
} catch (err) {
moduleLog.error(String(err));
toast({
id: 'CANVAS_COPY_FAILED',
title: t('toast.problemCopyingCanvas'),
description: t('toast.problemCopyingCanvasDesc'),
status: 'error',
});
return;
}
toast({
id: 'CANVAS_COPY_SUCCEEDED',
title: t('toast.canvasCopiedClipboard'),
status: 'success',
});
},
});
};

View File

@@ -1,34 +0,0 @@
import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
import { downloadBlob } from 'features/canvas/util/downloadBlob';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasDownloadedAsImage,
effect: async (action, { getState }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' });
const state = getState();
let blob;
try {
blob = await getBaseLayerBlob(state);
} catch (err) {
moduleLog.error(String(err));
toast({
id: 'CANVAS_DOWNLOAD_FAILED',
title: t('toast.problemDownloadingCanvas'),
description: t('toast.problemDownloadingCanvasDesc'),
status: 'error',
});
return;
}
downloadBlob(blob, 'canvas.png');
toast({ id: 'CANVAS_DOWNLOAD_SUCCEEDED', title: t('toast.canvasDownloaded'), status: 'success' });
},
});
};

View File

@@ -1,60 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasImageToControlAdapter } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasImageToControlNetListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasImageToControlAdapter,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { id } = action.payload;
let blob: Blob;
try {
blob = await getBaseLayerBlob(state, true);
} catch (err) {
log.error(String(err));
toast({
id: 'PROBLEM_SAVING_CANVAS',
title: t('toast.problemSavingCanvas'),
description: t('toast.problemSavingCanvasDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'savedCanvas.png', {
type: 'image/png',
}),
image_category: 'control',
is_intermediate: true,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: false,
postUploadAction: {
type: 'TOAST',
title: t('toast.canvasSentControlnetAssets'),
},
})
).unwrap();
const { image_name } = imageDTO;
dispatch(
controlAdapterImageChanged({
id,
controlImage: image_name,
})
);
},
});
};

View File

@@ -1,60 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMaskSavedToGallery } from 'features/canvas/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasMaskSavedToGallery,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const canvasBlobsAndImageData = await getCanvasData(
state.canvas.layerState,
state.canvas.boundingBoxCoordinates,
state.canvas.boundingBoxDimensions,
state.canvas.isMaskEnabled,
state.canvas.shouldPreserveMaskedArea
);
if (!canvasBlobsAndImageData) {
return;
}
const { maskBlob } = canvasBlobsAndImageData;
if (!maskBlob) {
log.error('Problem getting mask layer blob');
toast({
id: 'PROBLEM_SAVING_MASK',
title: t('toast.problemSavingMask'),
description: t('toast.problemSavingMaskDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
image_category: 'mask',
is_intermediate: false,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: true,
postUploadAction: {
type: 'TOAST',
title: t('toast.maskSavedAssets'),
},
})
);
},
});
};

View File

@@ -1,70 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMaskToControlAdapter } from 'features/canvas/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasMaskToControlNetListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasMaskToControlAdapter,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { id } = action.payload;
const canvasBlobsAndImageData = await getCanvasData(
state.canvas.layerState,
state.canvas.boundingBoxCoordinates,
state.canvas.boundingBoxDimensions,
state.canvas.isMaskEnabled,
state.canvas.shouldPreserveMaskedArea
);
if (!canvasBlobsAndImageData) {
return;
}
const { maskBlob } = canvasBlobsAndImageData;
if (!maskBlob) {
log.error('Problem getting mask layer blob');
toast({
id: 'PROBLEM_IMPORTING_MASK',
title: t('toast.problemImportingMask'),
description: t('toast.problemImportingMaskDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
image_category: 'mask',
is_intermediate: true,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: false,
postUploadAction: {
type: 'TOAST',
title: t('toast.maskSentControlnetAssets'),
},
})
).unwrap();
const { image_name } = imageDTO;
dispatch(
controlAdapterImageChanged({
id,
controlImage: image_name,
})
);
},
});
};

View File

@@ -1,73 +0,0 @@
import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMerged } from 'features/canvas/store/actions';
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasMergedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasMerged,
effect: async (action, { dispatch }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const blob = await getFullBaseLayerBlob();
if (!blob) {
moduleLog.error('Problem getting base layer blob');
toast({
id: 'PROBLEM_MERGING_CANVAS',
title: t('toast.problemMergingCanvas'),
description: t('toast.problemMergingCanvasDesc'),
status: 'error',
});
return;
}
const canvasBaseLayer = $canvasBaseLayer.get();
if (!canvasBaseLayer) {
moduleLog.error('Problem getting canvas base layer');
toast({
id: 'PROBLEM_MERGING_CANVAS',
title: t('toast.problemMergingCanvas'),
description: t('toast.problemMergingCanvasDesc'),
status: 'error',
});
return;
}
const baseLayerRect = canvasBaseLayer.getClientRect({
relativeTo: canvasBaseLayer.getParent() ?? undefined,
});
const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'mergedCanvas.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: true,
postUploadAction: {
type: 'TOAST',
title: t('toast.canvasMerged'),
},
})
).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 } = imageDTO;
dispatch(
setMergedCanvas({
kind: 'image',
layer: 'base',
imageName: image_name,
...baseLayerRect,
})
);
},
});
};

View File

@@ -1,53 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasSavedToGallery,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
let blob;
try {
blob = await getBaseLayerBlob(state);
} catch (err) {
log.error(String(err));
toast({
id: 'CANVAS_SAVE_FAILED',
title: t('toast.problemSavingCanvas'),
description: t('toast.problemSavingCanvasDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'savedCanvas.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: false,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: true,
postUploadAction: {
type: 'TOAST',
title: t('toast.canvasSavedGallery'),
},
metadata: {
_canvas_objects: parseify(state.canvas.layerState.objects),
},
})
);
},
});
};

View File

@@ -1,194 +0,0 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch } from 'app/store/store';
import { parseify } from 'common/util/serialize';
import {
caLayerImageChanged,
caLayerModelChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caLayerProcessorPendingBatchIdChanged,
caLayerRecalled,
isControlAdapterLayer,
} from 'features/controlLayers/store/controlLayersSlice';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { isEqual } from 'lodash-es';
import { getImageDTO } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
import { assert } from 'tsafe';
const matcher = isAnyOf(
caLayerImageChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caLayerModelChanged,
caLayerRecalled
);
const DEBOUNCE_MS = 300;
const log = logger('session');
/**
* Simple helper to cancel a batch and reset the pending batch ID
*/
const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => {
const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] }));
log.trace({ batchId }, 'Cancelling existing preprocessor batch');
try {
await req.unwrap();
} catch {
// no-op
} finally {
req.reset();
// Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
}
};
export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => {
startAppListening({
matcher,
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => {
const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
const state = getState();
const originalState = getOriginalState();
// Cancel any in-progress instances of this listener
cancelActiveListeners();
log.trace('Control Layer CA auto-process triggered');
// Delay before starting actual work
await delay(DEBOUNCE_MS);
const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
if (!layer) {
return;
}
// We should only process if the processor settings or image have changed
const originalLayer = originalState.controlLayers.present.layers
.filter(isControlAdapterLayer)
.find((l) => l.id === layerId);
const originalImage = originalLayer?.controlAdapter.image;
const originalConfig = originalLayer?.controlAdapter.processorConfig;
const image = layer.controlAdapter.image;
const processedImage = layer.controlAdapter.processedImage;
const config = layer.controlAdapter.processorConfig;
if (isEqual(config, originalConfig) && isEqual(image, originalImage) && processedImage) {
// Neither config nor image have changed, we can bail
return;
}
if (!image || !config) {
// - If we have no image, we have nothing to process
// - If we have no processor config, we have nothing to process
// Clear the processed image and bail
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null }));
return;
}
// At this point, the user has stopped fiddling with the processor settings and there is a processor selected.
// If there is a pending processor batch, cancel it.
if (layer.controlAdapter.processorPendingBatchId) {
cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId);
}
// TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now
const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never);
const enqueueBatchArg: BatchConfig = {
prepend: true,
batch: {
graph: {
nodes: {
[processorNode.id]: {
...processorNode,
// Control images are always intermediate - do not save to gallery
is_intermediate: true,
},
},
edges: [],
},
runs: 1,
},
};
// Kick off the processor batch
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
fixedCacheKey: 'enqueueBatch',
})
);
try {
const enqueueResult = await req.unwrap();
// TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional
assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue');
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id }));
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
// Wait for the processor node to complete
const [invocationCompleteAction] = await take(
(action): action is ReturnType<typeof socketInvocationComplete> =>
socketInvocationComplete.match(action) &&
action.payload.data.batch_id === enqueueResult.batch.batch_id &&
action.payload.data.invocation_source_id === processorNode.id
);
// We still have to check the output type
assert(
invocationCompleteAction.payload.data.result.type === 'image_output',
`Processor did not return an image output, got: ${invocationCompleteAction.payload.data.result}`
);
const { image_name } = invocationCompleteAction.payload.data.result.image;
const imageDTO = await getImageDTO(image_name);
assert(imageDTO, "Failed to fetch processor output's image DTO");
// Whew! We made it. Update the layer with the processed image
log.debug({ layerId, imageDTO }, 'ControlNet image processed');
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO }));
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
} catch (error) {
if (signal.aborted) {
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
const pendingBatchId = getState()
.controlLayers.present.layers.filter(isControlAdapterLayer)
.find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId;
if (pendingBatchId) {
cancelProcessorBatch(dispatch, layerId, pendingBatchId);
}
log.trace('Control Adapter preprocessor cancelled');
} else {
// Some other error condition...
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
if (error instanceof Object) {
if ('data' in error && 'status' in error) {
if (error.status === 403) {
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
return;
}
}
}
toast({
id: 'GRAPH_QUEUE_FAILED',
title: t('queue.graphFailedToQueue'),
status: 'error',
});
}
} finally {
req.reset();
}
},
});
};

View File

@@ -1,85 +0,0 @@
import type { AnyListenerPredicate } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { RootState } from 'app/store/store';
import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions';
import {
controlAdapterAutoConfigToggled,
controlAdapterImageChanged,
controlAdapterModelChanged,
controlAdapterProcessorParamsChanged,
controlAdapterProcessortTypeChanged,
selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
type AnyControlAdapterParamChangeAction =
| ReturnType<typeof controlAdapterProcessorParamsChanged>
| ReturnType<typeof controlAdapterModelChanged>
| ReturnType<typeof controlAdapterImageChanged>
| ReturnType<typeof controlAdapterProcessortTypeChanged>
| ReturnType<typeof controlAdapterAutoConfigToggled>;
const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) => {
const isActionMatched =
controlAdapterProcessorParamsChanged.match(action) ||
controlAdapterModelChanged.match(action) ||
controlAdapterImageChanged.match(action) ||
controlAdapterProcessortTypeChanged.match(action) ||
controlAdapterAutoConfigToggled.match(action);
if (!isActionMatched) {
return false;
}
const { id } = action.payload;
const prevCA = selectControlAdapterById(prevState.controlAdapters, id);
const ca = selectControlAdapterById(state.controlAdapters, id);
if (!prevCA || !isControlNetOrT2IAdapter(prevCA) || !ca || !isControlNetOrT2IAdapter(ca)) {
return false;
}
if (controlAdapterAutoConfigToggled.match(action)) {
// do not process if the user just disabled auto-config
if (prevCA.shouldAutoConfig === true) {
return false;
}
}
const { controlImage, processorType, shouldAutoConfig } = ca;
if (controlAdapterModelChanged.match(action) && !shouldAutoConfig) {
// do not process if the action is a model change but the processor settings are dirty
return false;
}
const isProcessorSelected = processorType !== 'none';
const hasControlImage = Boolean(controlImage);
return isProcessorSelected && hasControlImage;
};
const DEBOUNCE_MS = 300;
/**
* Listener that automatically processes a ControlNet image when its processor parameters are changed.
*
* The network request is debounced.
*/
export const addControlNetAutoProcessListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate,
effect: async (action, { dispatch, cancelActiveListeners, delay }) => {
const log = logger('session');
const { id } = (action as AnyControlAdapterParamChangeAction).payload;
// Cancel any in-progress instances of this listener
cancelActiveListeners();
log.trace('ControlNet auto-process triggered');
// Delay before starting actual work
await delay(DEBOUNCE_MS);
dispatch(controlAdapterImageProcessed({ id }));
},
});
};

View File

@@ -1,118 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions';
import {
controlAdapterImageChanged,
controlAdapterProcessedImageChanged,
pendingControlImagesCleared,
selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
export const addControlNetImageProcessedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: controlAdapterImageProcessed,
effect: async (action, { dispatch, getState, take }) => {
const log = logger('session');
const { id } = action.payload;
const ca = selectControlAdapterById(getState().controlAdapters, id);
if (!ca?.controlImage || !isControlNetOrT2IAdapter(ca)) {
log.error('Unable to process ControlNet image');
return;
}
if (ca.processorType === 'none' || ca.processorNode.type === 'none') {
return;
}
// ControlNet one-off procressing graph is just the processor node, no edges.
// Also we need to grab the image.
const nodeId = ca.processorNode.id;
const enqueueBatchArg: BatchConfig = {
prepend: true,
batch: {
graph: {
nodes: {
[ca.processorNode.id]: {
...ca.processorNode,
is_intermediate: true,
use_cache: false,
image: { image_name: ca.controlImage },
},
},
edges: [],
},
runs: 1,
},
};
try {
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
fixedCacheKey: 'enqueueBatch',
})
);
const enqueueResult = await req.unwrap();
req.reset();
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
const [invocationCompleteAction] = await take(
(action): action is ReturnType<typeof socketInvocationComplete> =>
socketInvocationComplete.match(action) &&
action.payload.data.batch_id === enqueueResult.batch.batch_id &&
action.payload.data.invocation_source_id === nodeId
);
// We still have to check the output type
if (invocationCompleteAction.payload.data.result.type === 'image_output') {
const { image_name } = invocationCompleteAction.payload.data.result.image;
// Wait for the ImageDTO to be received
const [{ payload }] = await take(
(action) =>
imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name
);
const processedControlImage = payload as ImageDTO;
log.debug({ controlNetId: action.payload, processedControlImage }, 'ControlNet image processed');
// Update the processed image in the store
dispatch(
controlAdapterProcessedImageChanged({
id,
processedControlImage: processedControlImage.image_name,
})
);
}
} catch (error) {
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
if (error instanceof Object) {
if ('data' in error && 'status' in error) {
if (error.status === 403) {
dispatch(pendingControlImagesCleared());
dispatch(controlAdapterImageChanged({ id, controlImage: null }));
return;
}
}
}
toast({
id: 'GRAPH_QUEUE_FAILED',
title: t('queue.graphFailedToQueue'),
status: 'error',
});
}
},
});
};

View File

@@ -1,144 +0,0 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { parseify } from 'common/util/serialize';
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
import { canvasGraphBuilt } from 'features/nodes/store/actions';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
import { imagesApi } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { ImageDTO } from 'services/api/types';
/**
* This listener is responsible invoking the canvas. This involves a number of steps:
*
* 1. Generate image blobs from the canvas layers
* 2. Determine the generation mode from the layers (txt2img, img2img, inpaint)
* 3. Build the canvas graph
* 4. Create the session with the graph
* 5. Upload the init image if necessary
* 6. Upload the mask image if necessary
* 7. Update the init and mask images with the session ID
* 8. Initialize the staging area if not yet initialized
* 9. Dispatch the sessionReadyToInvoke action to invoke the session
*/
export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && action.payload.tabName === 'canvas',
effect: async (action, { getState, dispatch }) => {
const log = logger('queue');
const { prepend } = action.payload;
const state = getState();
const { layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea } =
state.canvas;
// Build canvas blobs
const canvasBlobsAndImageData = await getCanvasData(
layerState,
boundingBoxCoordinates,
boundingBoxDimensions,
isMaskEnabled,
shouldPreserveMaskedArea
);
if (!canvasBlobsAndImageData) {
log.error('Unable to create canvas data');
return;
}
const { baseBlob, baseImageData, maskBlob, maskImageData } = canvasBlobsAndImageData;
// Determine the generation mode
const generationMode = getCanvasGenerationMode(baseImageData, maskImageData);
if (state.system.enableImageDebugging) {
const baseDataURL = await blobToDataURL(baseBlob);
const maskDataURL = await blobToDataURL(maskBlob);
openBase64ImageInTab([
{ base64: maskDataURL, caption: 'mask b64' },
{ base64: baseDataURL, caption: 'image b64' },
]);
}
log.debug(`Generation mode: ${generationMode}`);
// Temp placeholders for the init and mask images
let canvasInitImage: ImageDTO | undefined;
let canvasMaskImage: ImageDTO | undefined;
// 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
canvasInitImage = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([baseBlob], 'canvasInitImage.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: true,
})
).unwrap();
}
// For inpaint/outpaint, we also need to upload the mask layer
if (['inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id
canvasMaskImage = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
image_category: 'mask',
is_intermediate: true,
})
).unwrap();
}
const graph = await buildCanvasGraph(state, generationMode, canvasInitImage, canvasMaskImage);
log.debug({ graph: parseify(graph) }, `Canvas graph built`);
// currently this action is just listened to for logging
dispatch(canvasGraphBuilt(graph));
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
try {
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
const enqueueResult = await req.unwrap();
req.reset();
const batchId = enqueueResult.batch.batch_id as string; // we know the is a string, backend provides it
// Prep the canvas staging area if it is not yet initialized
if (!state.canvas.layerState.stagingArea.boundingBox) {
dispatch(
stagingAreaInitialized({
boundingBox: {
...state.canvas.boundingBoxCoordinates,
...state.canvas.boundingBoxDimensions,
},
})
);
}
// Associate the session with the canvas session ID
dispatch(canvasBatchIdAdded(batchId));
} catch {
// no-op
}
},
});
};

View File

@@ -1,10 +1,21 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import type { SerializableObject } from 'common/types';
import type { Result } from 'common/util/result';
import { isErr, withResult, withResultAsync } from 'common/util/result';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { serializeError } from 'serialize-error';
import { queueApi } from 'services/api/endpoints/queue';
import type { Invocation } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('generation');
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
startAppListening({
@@ -12,33 +23,81 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
enqueueRequested.match(action) && action.payload.tabName === 'generation',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const { shouldShowProgressInViewer } = state.ui;
const model = state.generation.model;
const model = state.params.model;
const { prepend } = action.payload;
let graph;
const manager = $canvasManager.get();
assert(manager, 'No model found in state');
if (model?.base === 'sdxl') {
graph = await buildGenerationTabSDXLGraph(state);
} else {
graph = await buildGenerationTabGraph(state);
let didStartStaging = false;
if (!state.canvasSession.isStaging && state.canvasSettings.sendToCanvas) {
dispatch(sessionStartedStaging());
didStartStaging = true;
}
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
const abortStaging = () => {
if (didStartStaging && getState().canvasSession.isStaging) {
dispatch(sessionStagingAreaReset());
}
};
let buildGraphResult: Result<
{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel' | 'sdxl_compel_prompt'> },
Error
>;
assert(model, 'No model found in state');
const base = model.base;
switch (base) {
case 'sdxl':
buildGraphResult = await withResultAsync(() => buildSDXLGraph(state, manager));
break;
case 'sd-1':
case `sd-2`:
buildGraphResult = await withResultAsync(() => buildSD1Graph(state, manager));
break;
default:
assert(false, `No graph builders for base ${base}`);
}
if (isErr(buildGraphResult)) {
log.error({ error: serializeError(buildGraphResult.error) }, 'Failed to build graph');
abortStaging();
return;
}
const { g, noise, posCond } = buildGraphResult.value;
const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
const prepareBatchResult = withResult(() =>
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
);
if (isErr(prepareBatchResult)) {
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
abortStaging();
return;
}
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, {
fixedCacheKey: 'enqueueBatch',
})
);
try {
await req.unwrap();
if (shouldShowProgressInViewer) {
dispatch(isImageViewerOpenChanged(true));
}
} finally {
req.reset();
req.reset();
const enqueueResult = await withResultAsync(() => req.unwrap());
if (isErr(enqueueResult)) {
log.error({ error: serializeError(enqueueResult.error) }, 'Failed to enqueue batch');
abortStaging();
return;
}
log.debug({ batchConfig: prepareBatchResult.value } as SerializableObject, 'Enqueued batch');
},
});
};

View File

@@ -1,5 +1,6 @@
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow';
import { queueApi } from 'services/api/endpoints/queue';
@@ -11,12 +12,12 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
enqueueRequested.match(action) && action.payload.tabName === 'workflows',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const { nodes, edges } = state.nodes.present;
const nodes = selectNodesSlice(state);
const workflow = state.workflow;
const graph = buildNodesGraph(state.nodes.present);
const graph = buildNodesGraph(nodes);
const builtWorkflow = buildWorkflowWithValidation({
nodes,
edges,
nodes: nodes.nodes,
edges: nodes.edges,
workflow,
});
@@ -29,7 +30,9 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
batch: {
graph,
workflow: builtWorkflow,
runs: state.generation.iterations,
runs: state.params.iterations,
origin: 'workflows',
destination: 'gallery',
},
prepend: action.payload.prepend,
};

View File

@@ -14,9 +14,9 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
const { shouldShowProgressInViewer } = state.ui;
const { prepend } = action.payload;
const graph = await buildMultidiffusionUpscaleGraph(state);
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {

View File

@@ -27,7 +27,7 @@ export const galleryImageClicked = createAction<{
export const addGalleryImageClickedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: galleryImageClicked,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);

View File

@@ -1,24 +1,27 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { SerializableObject } from 'common/types';
import { parseify } from 'common/util/serialize';
import { $templates } from 'features/nodes/store/nodesSlice';
import { parseSchema } from 'features/nodes/util/schema/parseSchema';
import { size } from 'lodash-es';
import { serializeError } from 'serialize-error';
import { appInfoApi } from 'services/api/endpoints/appInfo';
const log = logger('system');
export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
effect: (action, { getState }) => {
const log = logger('system');
const schemaJSON = action.payload;
log.debug({ schemaJSON: parseify(schemaJSON) }, 'Received OpenAPI schema');
log.debug({ schemaJSON: parseify(schemaJSON) } as SerializableObject, 'Received OpenAPI schema');
const { nodesAllowlist, nodesDenylist } = getState().config;
const nodeTemplates = parseSchema(schemaJSON, nodesAllowlist, nodesDenylist);
log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`);
log.debug({ nodeTemplates } as SerializableObject, `Built ${size(nodeTemplates)} node templates`);
$templates.set(nodeTemplates);
},
@@ -30,8 +33,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening
// If action.meta.condition === true, the request was canceled/skipped because another request was in flight or
// the value was already in the cache. We don't want to log these errors.
if (!action.meta.condition) {
const log = logger('system');
log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
log.error({ error: serializeError(action.error) }, 'Problem retrieving OpenAPI Schema');
}
},
});

View File

@@ -2,15 +2,13 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { imagesApi } from 'services/api/endpoints/images';
const log = logger('gallery');
export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
effect: (action) => {
const log = logger('images');
const { board_id, imageDTO } = action.meta.arg.originalArgs;
// TODO: update listImages cache for this board
log.debug({ board_id, imageDTO }, 'Image added to board');
},
});
@@ -18,9 +16,7 @@ export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStar
startAppListening({
matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
effect: (action) => {
const log = logger('images');
const { board_id, imageDTO } = action.meta.arg.originalArgs;
log.debug({ board_id, imageDTO }, 'Problem adding image to board');
},
});

View File

@@ -1,20 +1,9 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterProcessedImageChanged,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
isIPAdapterLayer,
isRegionalGuidanceLayer,
layerDeleted,
} from 'features/controlLayers/store/controlLayersSlice';
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@@ -26,6 +15,10 @@ import { forEach, intersectionBy } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
const log = logger('gallery');
//TODO(psyche): handle image deletion (canvas sessions?)
// Some utils to delete images from different parts of the app
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.nodes.present.nodes.forEach((node) => {
@@ -47,52 +40,37 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
});
};
const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
forEach(selectControlAdapterAll(state.controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
// const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
// state.canvas.present.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => {
// if (
// imageObject?.image.image_name === imageDTO.image_name ||
// processedImageObject?.image.image_name === imageDTO.image_name
// ) {
// dispatch(caImageChanged({ id, imageDTO: null }));
// dispatch(caProcessedImageChanged({ id, imageDTO: null }));
// }
// });
// };
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
selectCanvasSlice(state).ipAdapters.entities.forEach((entity) => {
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
}
});
};
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.controlLayers.present.layers.forEach((l) => {
if (isRegionalGuidanceLayer(l)) {
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
dispatch(layerDeleted(l.id));
const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
let shouldDelete = false;
for (const obj of objects) {
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
shouldDelete = true;
break;
}
}
if (isControlAdapterLayer(l)) {
if (
l.controlAdapter.image?.name === imageDTO.image_name ||
l.controlAdapter.processedImage?.name === imageDTO.image_name
) {
dispatch(layerDeleted(l.id));
}
}
if (isIPAdapterLayer(l)) {
if (l.ipAdapter.image?.name === imageDTO.image_name) {
dispatch(layerDeleted(l.id));
}
}
if (isInitialImageLayer(l)) {
if (l.image?.name === imageDTO.image_name) {
dispatch(layerDeleted(l.id));
}
if (shouldDelete) {
dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
}
});
};
@@ -145,14 +123,10 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
}
}
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
if (imageUsage.isCanvasImage) {
dispatch(resetCanvas());
}
deleteControlAdapterImages(state, dispatch, imageDTO);
deleteNodesImages(state, dispatch, imageDTO);
deleteControlLayerImages(state, dispatch, imageDTO);
// deleteControlAdapterImages(state, dispatch, imageDTO);
deleteIPAdapterImages(state, dispatch, imageDTO);
deleteLayerImages(state, dispatch, imageDTO);
} catch {
// no-op
} finally {
@@ -189,14 +163,11 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
if (imagesUsage.some((i) => i.isCanvasImage)) {
dispatch(resetCanvas());
}
imageDTOs.forEach((imageDTO) => {
deleteControlAdapterImages(state, dispatch, imageDTO);
deleteNodesImages(state, dispatch, imageDTO);
deleteControlLayerImages(state, dispatch, imageDTO);
// deleteControlAdapterImages(state, dispatch, imageDTO);
deleteIPAdapterImages(state, dispatch, imageDTO);
deleteLayerImages(state, dispatch, imageDTO);
});
} catch {
// no-op
@@ -220,7 +191,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
effect: (action) => {
const log = logger('images');
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
},
});
@@ -228,7 +198,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchRejected,
effect: (action) => {
const log = logger('images');
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
},
});

View File

@@ -1,28 +1,19 @@
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
controlLayerAdded,
ipaImageChanged,
rasterLayerAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import {
imageSelected,
imageToCompareChanged,
isImageViewerOpenChanged,
selectionChanged,
} from 'features/gallery/store/gallerySlice';
import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -31,11 +22,12 @@ export const dndDropped = createAction<{
activeData: TypesafeDraggableData;
}>('dnd/dndDropped');
const log = logger('system');
export const addImageDroppedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: dndDropped,
effect: async (action, { dispatch, getState }) => {
const log = logger('dnd');
effect: (action, { dispatch, getState }) => {
const { activeData, overData } = action.payload;
if (!isValidDrop(overData, activeData)) {
return;
@@ -46,80 +38,22 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
log.debug({ activeData, overData }, `Images (${getState().gallery.selection.length}) dropped`);
} else if (activeData.payloadType === 'NODE_FIELD') {
log.debug({ activeData: parseify(activeData), overData: parseify(overData) }, 'Node field dropped');
log.debug({ activeData, overData }, 'Node field dropped');
} else {
log.debug({ activeData, overData }, `Unknown payload dropped`);
}
/**
* Image dropped on current image
*/
if (
overData.actionType === 'SET_CURRENT_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(imageSelected(activeData.payload.imageDTO));
dispatch(isImageViewerOpenChanged(true));
return;
}
/**
* Image dropped on ControlNet
*/
if (
overData.actionType === 'SET_CONTROL_ADAPTER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { id } = overData.context;
dispatch(
controlAdapterImageChanged({
id,
controlImage: activeData.payload.imageDTO.image_name,
})
);
dispatch(
controlAdapterIsEnabledChanged({
id,
isEnabled: true,
})
);
return;
}
/**
* Image dropped on Control Adapter Layer
*/
if (
overData.actionType === 'SET_CA_LAYER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
dispatch(
caLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
);
return;
}
/**
* Image dropped on IP Adapter Layer
*/
if (
overData.actionType === 'SET_IPA_LAYER_IMAGE' &&
overData.actionType === 'SET_IPA_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
const { id } = overData.context;
dispatch(
ipaLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO: activeData.payload.imageDTO })
);
return;
}
@@ -128,14 +62,14 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
* Image dropped on RG Layer IP Adapter
*/
if (
overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' &&
overData.actionType === 'SET_RG_IP_ADAPTER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId, ipAdapterId } = overData.context;
const { id, ipAdapterId } = overData.context;
dispatch(
rgLayerIPAdapterImageChanged({
layerId,
rgIPAdapterImageChanged({
entityIdentifier: { id, type: 'regional_guidance' },
ipAdapterId,
imageDTO: activeData.payload.imageDTO,
})
@@ -144,32 +78,38 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
}
/**
* Image dropped on II Layer Image
* Image dropped on Raster layer
*/
if (
overData.actionType === 'SET_II_LAYER_IMAGE' &&
overData.actionType === 'ADD_RASTER_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
dispatch(
iiLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
);
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasRasterLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on Canvas
* Image dropped on Raster layer
*/
if (
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
overData.actionType === 'ADD_CONTROL_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(setInitialCanvasImage(activeData.payload.imageDTO, selectOptimalDimension(getState())));
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasControlLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
return;
}

View File

@@ -2,13 +2,13 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { imagesApi } from 'services/api/endpoints/images';
const log = logger('gallery');
export const addImageRemovedFromBoardFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
effect: (action) => {
const log = logger('images');
const imageDTO = action.meta.arg.originalArgs;
log.debug({ imageDTO }, 'Image removed from board');
},
});
@@ -16,9 +16,7 @@ export const addImageRemovedFromBoardFulfilledListener = (startAppListening: App
startAppListening({
matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
effect: (action) => {
const log = logger('images');
const imageDTO = action.meta.arg.originalArgs;
log.debug({ imageDTO }, 'Problem removing image from board');
},
});

View File

@@ -6,16 +6,17 @@ import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImage
export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: imagesToDeleteSelected,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const imageDTOs = action.payload;
const state = getState();
const { shouldConfirmOnDelete } = state.system;
const imagesUsage = selectImageUsage(getState());
const isImageInUse =
imagesUsage.some((i) => i.isCanvasImage) ||
imagesUsage.some((i) => i.isControlImage) ||
imagesUsage.some((i) => i.isNodesImage);
imagesUsage.some((i) => i.isLayerImage) ||
imagesUsage.some((i) => i.isControlAdapterImage) ||
imagesUsage.some((i) => i.isIPAdapterImage) ||
imagesUsage.some((i) => i.isLayerImage);
if (shouldConfirmOnDelete || isImageInUse) {
dispatch(isModalOpenChanged(true));

View File

@@ -1,19 +1,8 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
@@ -21,11 +10,12 @@ import { omit } from 'lodash-es';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
const log = logger('gallery');
export const addImageUploadedFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
const log = logger('images');
const imageDTO = action.payload;
const state = getState();
const { autoAddBoardId } = state.gallery;
@@ -81,15 +71,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state)));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setAsCanvasInitialImage'),
});
return;
}
if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') {
dispatch(upscaleInitialImageChanged(imageDTO));
toast({
@@ -99,70 +80,33 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') {
// if (postUploadAction?.type === 'SET_CA_IMAGE') {
// const { id } = postUploadAction;
// dispatch(caImageChanged({ id, imageDTO }));
// toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
// return;
// }
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
const { id } = postUploadAction;
dispatch(
controlAdapterIsEnabledChanged({
id,
isEnabled: true,
})
);
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO.image_name,
})
);
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
dispatch(ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO }));
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(caLayerImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
const { layerId, ipAdapterId } = postUploadAction;
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
const { id, ipAdapterId } = postUploadAction;
dispatch(
rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, ipAdapterId, imageDTO })
);
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
const { nodeId, fieldName } = postUploadAction;
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: `${t('toast.setNodeField')} ${fieldName}`,
});
toast({ ...DEFAULT_UPLOADED_TOAST, description: `${t('toast.setNodeField')} ${fieldName}` });
return;
}
},
@@ -171,7 +115,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchRejected,
effect: (action) => {
const log = logger('images');
const sanitizedData = {
arg: {
...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']),

View File

@@ -6,7 +6,7 @@ import type { ImageDTO } from 'services/api/types';
export const addImagesStarredListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.starImages.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { updated_image_names: starredImages } = action.payload;
const state = getState();

View File

@@ -6,7 +6,7 @@ import type { ImageDTO } from 'services/api/types';
export const addImagesUnstarredListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.unstarImages.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { updated_image_names: unstarredImages } = action.payload;
const state = getState();

View File

@@ -1,23 +1,18 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
controlAdapterIsEnabledChanged,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { modelSelected } from 'features/parameters/store/actions';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { forEach } from 'lodash-es';
const log = logger('models');
export const addModelSelectedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: modelSelected,
effect: (action, { getState, dispatch }) => {
const log = logger('models');
const state = getState();
const result = zParameterModel.safeParse(action.payload);
@@ -29,34 +24,36 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
const newModel = result.data;
const newBaseModel = newModel.base;
const didBaseModelChange = state.generation.model?.base !== newBaseModel;
const didBaseModelChange = state.params.model?.base !== newBaseModel;
if (didBaseModelChange) {
// we may need to reset some incompatible submodels
let modelsCleared = 0;
// handle incompatible loras
forEach(state.lora.loras, (lora, id) => {
state.loras.loras.forEach((lora) => {
if (lora.model.base !== newBaseModel) {
dispatch(loraRemoved(id));
dispatch(loraDeleted({ id: lora.id }));
modelsCleared += 1;
}
});
// handle incompatible vae
const { vae } = state.generation;
const { vae } = state.params;
if (vae && vae.base !== newBaseModel) {
dispatch(vaeSelected(null));
modelsCleared += 1;
}
// handle incompatible controlnets
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
if (ca.model?.base !== newBaseModel) {
dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
modelsCleared += 1;
}
});
// state.canvas.present.controlAdapters.entities.forEach((ca) => {
// if (ca.model?.base !== newBaseModel) {
// modelsCleared += 1;
// if (ca.isEnabled) {
// dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
// }
// }
// });
if (modelsCleared > 0) {
toast({
@@ -70,7 +67,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
}
}
dispatch(modelChanged(newModel, state.generation.model));
dispatch(modelChanged({ model: newModel, previousModel: state.params.model }));
},
});
};

View File

@@ -1,36 +1,42 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import type { JSONObject } from 'common/types';
import type { SerializableObject } from 'common/types';
import {
controlAdapterModelCleared,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
bboxHeightChanged,
bboxWidthChanged,
controlLayerModelChanged,
ipaModelChanged,
rgIPAdapterModelChanged,
} from 'features/controlLayers/store/canvasSlice';
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
import { forEach } from 'lodash-es';
import type { Logger } from 'roarr';
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';
import {
isControlNetOrT2IAdapterModelConfig,
isIPAdapterModelConfig,
isLoRAModelConfig,
isNonRefinerMainModelConfig,
isRefinerMainModelModelConfig,
isSpandrelImageToImageModelConfig,
isVAEModelConfig,
} from 'services/api/types';
const log = logger('models');
export const addModelsLoadedListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
effect: (action, { getState, dispatch }) => {
// models loaded, we need to ensure the selected model is available and if not, select the first one
const log = logger('models');
log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`);
const state = getState();
@@ -43,6 +49,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) =>
handleLoRAModels(models, state, dispatch, log);
handleControlAdapterModels(models, state, dispatch, log);
handleSpandrelImageToImageModels(models, state, dispatch, log);
handleIPAdapterModels(models, state, dispatch, log);
},
});
};
@@ -51,15 +58,15 @@ type ModelHandler = (
models: AnyModelConfig[],
state: RootState,
dispatch: AppDispatch,
log: Logger<JSONObject>
log: Logger<SerializableObject>
) => undefined;
const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
const currentModel = state.generation.model;
const currentModel = state.params.model;
const mainModels = models.filter(isNonRefinerMainModelConfig);
if (mainModels.length === 0) {
// No models loaded at all
dispatch(modelChanged(null));
dispatch(modelChanged({ model: null }));
return;
}
@@ -74,25 +81,16 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
if (defaultModelInList) {
const result = zParameterModel.safeParse(defaultModelInList);
if (result.success) {
dispatch(modelChanged(defaultModelInList, currentModel));
dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel }));
const { bbox } = selectCanvasSlice(state);
const optimalDimension = getOptimalDimension(defaultModelInList);
if (
getIsSizeOptimal(
state.controlLayers.present.size.width,
state.controlLayers.present.size.height,
optimalDimension
)
) {
if (getIsSizeOptimal(bbox.rect.width, bbox.rect.height, optimalDimension)) {
return;
}
const { width, height } = calculateNewSize(
state.controlLayers.present.size.aspectRatio.value,
optimalDimension * optimalDimension
);
const { width, height } = calculateNewSize(bbox.aspectRatio.value, optimalDimension * optimalDimension);
dispatch(widthChanged({ width }));
dispatch(heightChanged({ height }));
dispatch(bboxWidthChanged({ width }));
dispatch(bboxHeightChanged({ height }));
return;
}
}
@@ -104,11 +102,11 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
return;
}
dispatch(modelChanged(result.data, currentModel));
dispatch(modelChanged({ model: result.data, previousModel: currentModel }));
};
const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {
const currentRefinerModel = state.sdxl.refinerModel;
const currentRefinerModel = state.params.refinerModel;
const refinerModels = models.filter(isRefinerMainModelModelConfig);
if (models.length === 0) {
// No models loaded at all
@@ -127,7 +125,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {
};
const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
const currentVae = state.generation.vae;
const currentVae = state.params.vae;
if (currentVae === null) {
// null is a valid VAE! it means "use the default with the main model"
@@ -160,28 +158,47 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
};
const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => {
const loras = state.lora.loras;
forEach(loras, (lora, id) => {
const isLoRAAvailable = models.some((m) => m.key === lora.model.key);
const loraModels = models.filter(isLoRAModelConfig);
state.loras.loras.forEach((lora) => {
const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key);
if (isLoRAAvailable) {
return;
}
dispatch(loraRemoved(id));
dispatch(loraDeleted({ id: lora.id }));
});
};
const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
const isModelAvailable = models.some((m) => m.key === ca.model?.key);
const caModels = models.filter(isControlNetOrT2IAdapterModelConfig);
selectCanvasSlice(state).controlLayers.entities.forEach((entity) => {
const isModelAvailable = caModels.some((m) => m.key === entity.controlAdapter.model?.key);
if (isModelAvailable) {
return;
}
dispatch(controlLayerModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
};
dispatch(controlAdapterModelCleared({ id: ca.id }));
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
const ipaModels = models.filter(isIPAdapterModelConfig);
selectCanvasSlice(state).ipAdapters.entities.forEach((entity) => {
const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key);
if (isModelAvailable) {
return;
}
dispatch(ipaModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
selectCanvasSlice(state).regions.entities.forEach((entity) => {
entity.ipAdapters.forEach(({ id: ipAdapterId, model }) => {
const isModelAvailable = ipaModels.some((m) => m.key === model?.key);
if (isModelAvailable) {
return;
}
dispatch(
rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), ipAdapterId, modelConfig: null })
);
});
});
};

View File

@@ -1,6 +1,6 @@
import { isAnyOf } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { positivePromptChanged } from 'features/controlLayers/store/paramsSlice';
import {
combinatorialToggled,
isErrorChanged,
@@ -15,7 +15,7 @@ import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilder
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
import { utilitiesApi } from 'services/api/endpoints/utilities';
import { socketConnected } from 'services/events/actions';
import { socketConnected } from 'services/events/setEventListeners';
const matcher = isAnyOf(
positivePromptChanged,
@@ -24,8 +24,6 @@ const matcher = isAnyOf(
maxPromptsReset,
socketConnected,
activeStylePresetIdChanged,
stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled,
stylePresetsApi.endpoints.updateStylePreset.matchFulfilled,
stylePresetsApi.endpoints.listStylePresets.matchFulfilled
);

View File

@@ -1,6 +1,5 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { setDefaultSettings } from 'features/parameters/store/actions';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
import {
setCfgRescaleMultiplier,
setCfgScale,
@@ -8,7 +7,8 @@ import {
setSteps,
vaePrecisionChanged,
vaeSelected,
} from 'features/parameters/store/generationSlice';
} from 'features/controlLayers/store/paramsSlice';
import { setDefaultSettings } from 'features/parameters/store/actions';
import {
isParameterCFGRescaleMultiplier,
isParameterCFGScale,
@@ -30,7 +30,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
effect: async (action, { dispatch, getState }) => {
const state = getState();
const currentModel = state.generation.model;
const currentModel = state.params.model;
if (!currentModel) {
return;
@@ -98,13 +98,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
const setSizeOptions = { updateAspectRatio: true, clamp: true };
if (width) {
if (isParameterWidth(width)) {
dispatch(widthChanged({ width, ...setSizeOptions }));
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
}
}
if (height) {
if (isParameterHeight(height)) {
dispatch(heightChanged({ height, ...setSizeOptions }));
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
}
}

View File

@@ -6,9 +6,9 @@ import { atom } from 'nanostores';
import { api } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
import { socketConnected } from 'services/events/actions';
import { socketConnected } from 'services/events/setEventListeners';
const log = logger('socketio');
const log = logger('events');
const $isFirstConnection = atom(true);

View File

@@ -1,14 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { socketDisconnected } from 'services/events/actions';
const log = logger('socketio');
export const addSocketDisconnectedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketDisconnected,
effect: () => {
log.debug('Disconnected');
},
});
};

View File

@@ -1,26 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { socketGeneratorProgress } from 'services/events/actions';
const log = logger('socketio');
export const addGeneratorProgressEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketGeneratorProgress,
effect: (action) => {
log.trace(parseify(action.payload), `Generator progress`);
const { invocation_source_id, step, total_steps, progress_image } = action.payload.data;
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.IN_PROGRESS;
nes.progress = (step + 1) / total_steps;
nes.progressImage = progress_image ?? null;
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,122 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import {
boardIdSelected,
galleryViewChanged,
imageSelected,
isImageViewerOpenChanged,
offsetChanged,
} from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
import { getCategories, getListImagesUrl } from 'services/api/util';
import { socketInvocationComplete } from 'services/events/actions';
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
const nodeTypeDenylist = ['load_image', 'image'];
const log = logger('socketio');
export const addInvocationCompleteEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketInvocationComplete,
effect: async (action, { dispatch, getState }) => {
const { data } = action.payload;
log.debug({ data: parseify(data) }, `Invocation complete (${data.invocation.type})`);
const { result, invocation_source_id } = data;
// This complete event has an associated image output
if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) {
const { image_name } = data.result.image;
const { canvas, gallery } = getState();
// This populates the `getImageDTO` cache
const imageDTORequest = dispatch(
imagesApi.endpoints.getImageDTO.initiate(image_name, {
forceRefetch: true,
})
);
const imageDTO = await imageDTORequest.unwrap();
imageDTORequest.unsubscribe();
// Add canvas images to the staging area
if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) {
dispatch(addImageToStagingArea(imageDTO));
}
if (!imageDTO.is_intermediate) {
// update the total images for the board
dispatch(
boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
draft.total += 1;
})
);
dispatch(
imagesApi.util.invalidateTags([
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
{
type: 'ImageList',
id: getListImagesUrl({
board_id: imageDTO.board_id ?? 'none',
categories: getCategories(imageDTO),
}),
},
])
);
const { shouldAutoSwitch } = gallery;
// If auto-switch is enabled, select the new image
if (shouldAutoSwitch) {
// if auto-add is enabled, switch the gallery view and board if needed as the image comes in
if (gallery.galleryView !== 'images') {
dispatch(galleryViewChanged('images'));
}
if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) {
dispatch(
boardIdSelected({
boardId: imageDTO.board_id,
selectedImageName: imageDTO.image_name,
})
);
}
dispatch(offsetChanged({ offset: 0 }));
if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') {
dispatch(
boardIdSelected({
boardId: 'none',
selectedImageName: imageDTO.image_name,
})
);
}
dispatch(imageSelected(imageDTO));
dispatch(isImageViewerOpenChanged(true));
}
}
}
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.COMPLETED;
if (nes.progress !== null) {
nes.progress = 1;
}
nes.outputs.push(result);
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,31 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { socketInvocationError } from 'services/events/actions';
const log = logger('socketio');
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketInvocationError,
effect: (action) => {
const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data;
log.error(parseify(action.payload), `Invocation error (${invocation.type})`);
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.FAILED;
nes.progress = null;
nes.progressImage = null;
nes.error = {
error_type,
error_message,
error_traceback,
};
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,24 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { socketInvocationStarted } from 'services/events/actions';
const log = logger('socketio');
export const addInvocationStartedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketInvocationStarted,
effect: (action) => {
log.debug(parseify(action.payload), `Invocation started (${action.payload.data.invocation.type})`);
const { invocation_source_id } = action.payload.data;
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.IN_PROGRESS;
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,196 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { api, LIST_TAG } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
import {
socketModelInstallCancelled,
socketModelInstallComplete,
socketModelInstallDownloadProgress,
socketModelInstallDownloadsComplete,
socketModelInstallDownloadStarted,
socketModelInstallError,
socketModelInstallStarted,
} from 'services/events/actions';
/**
* A model install has two main stages - downloading and installing. All these events are namespaced under `model_install_`
* which is a bit misleading. For example, a `model_install_started` event is actually fired _after_ the model has fully
* downloaded and is being "physically" installed.
*
* Note: the download events are only fired for remote model installs, not local.
*
* Here's the expected flow:
* - API receives install request, model manager preps the install
* - `model_install_download_started` fired when the download starts
* - `model_install_download_progress` fired continually until the download is complete
* - `model_install_download_complete` fired when the download is complete
* - `model_install_started` fired when the "physical" installation starts
* - `model_install_complete` fired when the installation is complete
* - `model_install_cancelled` fired if the installation is cancelled
* - `model_install_error` fired if the installation has an error
*/
const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select();
export const addModelInstallEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketModelInstallDownloadStarted,
effect: async (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'downloading';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallStarted,
effect: async (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'running';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallDownloadProgress,
effect: async (action, { dispatch, getState }) => {
const { bytes, total_bytes, id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.bytes = bytes;
modelImport.total_bytes = total_bytes;
modelImport.status = 'downloading';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallComplete,
effect: (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'completed';
}
return draft;
})
);
}
dispatch(api.util.invalidateTags([{ type: 'ModelConfig', id: LIST_TAG }]));
dispatch(api.util.invalidateTags([{ type: 'ModelScanFolderResults', id: LIST_TAG }]));
},
});
startAppListening({
actionCreator: socketModelInstallError,
effect: (action, { dispatch, getState }) => {
const { id, error, error_type } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'error';
modelImport.error_reason = error_type;
modelImport.error = error;
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallCancelled,
effect: (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'cancelled';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallDownloadsComplete,
effect: (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'downloads_done';
}
return draft;
})
);
}
},
});
};

View File

@@ -1,42 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { socketModelLoadComplete, socketModelLoadStarted } from 'services/events/actions';
const log = logger('socketio');
export const addModelLoadEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketModelLoadStarted,
effect: (action) => {
const { config, submodel_type } = action.payload.data;
const { name, base, type } = config;
const extras: string[] = [base, type];
if (submodel_type) {
extras.push(submodel_type);
}
const message = `Model load started: ${name} (${extras.join(', ')})`;
log.debug(action.payload, message);
},
});
startAppListening({
actionCreator: socketModelLoadComplete,
effect: (action) => {
const { config, submodel_type } = action.payload.data;
const { name, base, type } = config;
const extras: string[] = [base, type];
if (submodel_type) {
extras.push(submodel_type);
}
const message = `Model load complete: ${name} (${extras.join(', ')})`;
log.debug(action.payload, message);
},
});
};

View File

@@ -1,114 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
import { toast } from 'features/toast/toast';
import { forEach } from 'lodash-es';
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
import { socketQueueItemStatusChanged } from 'services/events/actions';
const log = logger('socketio');
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketQueueItemStatusChanged,
effect: async (action, { dispatch, getState }) => {
// we've got new status for the queue item, batch and queue
const {
item_id,
session_id,
status,
started_at,
updated_at,
completed_at,
batch_status,
queue_status,
error_type,
error_message,
error_traceback,
} = action.payload.data;
log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`);
// Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
dispatch(
queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
queueItemsAdapter.updateOne(draft, {
id: String(item_id),
changes: {
status,
started_at,
updated_at: updated_at ?? undefined,
completed_at: completed_at ?? undefined,
error_type,
error_message,
error_traceback,
},
});
})
);
// Update the queue status (we do not get the processor status here)
dispatch(
queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => {
if (!draft) {
return;
}
Object.assign(draft.queue, queue_status);
})
);
// Update the batch status
dispatch(
queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status)
);
// Invalidate caches for things we cannot update
// TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again
dispatch(
queueApi.util.invalidateTags([
'CurrentSessionQueueItem',
'NextSessionQueueItem',
'InvocationCacheStatus',
{ type: 'SessionQueueItem', id: item_id },
])
);
if (status === 'in_progress') {
forEach($nodeExecutionStates.get(), (nes) => {
if (!nes) {
return;
}
const clone = deepClone(nes);
clone.status = zNodeStatus.enum.PENDING;
clone.error = null;
clone.progress = null;
clone.progressImage = null;
clone.outputs = [];
$nodeExecutionStates.setKey(clone.nodeId, clone);
});
} else if (status === 'failed' && error_type) {
const isLocal = getState().config.isLocal ?? true;
const sessionId = session_id;
toast({
id: `INVOCATION_ERROR_${error_type}`,
title: getTitleFromErrorType(error_type),
status: 'error',
duration: null,
updateDescription: isLocal,
description: (
<ErrorToastDescription
errorType={error_type}
errorMessage={error_message}
sessionId={sessionId}
isLocal={isLocal}
/>
),
});
}
},
});
};

View File

@@ -1,43 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addStagingAreaImageSavedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: stagingAreaImageSaved,
effect: async (action, { dispatch, getState }) => {
const { imageDTO } = action.payload;
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 && autoAddBoardId !== 'none') {
await dispatch(
imagesApi.endpoints.addImageToBoard.initiate({
imageDTO: newImageDTO,
board_id: autoAddBoardId,
})
);
}
toast({ id: 'IMAGE_SAVED', title: t('toast.imageSaved'), status: 'success' });
} catch (error) {
toast({
id: 'IMAGE_SAVE_FAILED',
title: t('toast.imageSavingFailed'),
description: (error as Error)?.message,
status: 'error',
});
}
},
});
};

View File

@@ -2,18 +2,20 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice';
import { selectNodes } from 'features/nodes/store/selectors';
import { NodeUpdateError } from 'features/nodes/types/error';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
const log = logger('workflows');
export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: updateAllNodesRequested,
effect: (action, { dispatch, getState }) => {
const log = logger('nodes');
const { nodes } = getState().nodes.present;
const nodes = selectNodes(getState());
const templates = $templates.get();
let unableToUpdateCount = 0;

View File

@@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
import { $templates } from 'features/nodes/store/nodesSlice';
import { $needsFit } from 'features/nodes/store/reactFlowInstance';
@@ -10,11 +10,14 @@ import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { serializeError } from 'serialize-error';
import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks';
import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types';
import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
const log = logger('workflows');
const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => {
if (data.workflow) {
// Prefer to load the workflow if it's available - it has more information
@@ -34,7 +37,6 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
startAppListening({
actionCreator: workflowLoadRequested,
effect: async (action, { dispatch }) => {
const log = logger('nodes');
const { data, asCopy } = action.payload;
const nodeTemplates = $templates.get();
@@ -46,6 +48,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
delete workflow.id;
}
$nodeExecutionStates.set({});
dispatch(workflowLoaded(workflow));
if (!warnings.length) {
toast({
@@ -69,7 +72,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
} catch (e) {
if (e instanceof WorkflowVersionError) {
// The workflow version was not recognized in the valid list of versions
log.error({ error: parseify(e) }, e.message);
log.error({ error: serializeError(e) }, e.message);
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),
@@ -78,7 +81,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
});
} else if (e instanceof WorkflowMigrationError) {
// There was a problem migrating the workflow to the latest version
log.error({ error: parseify(e) }, e.message);
log.error({ error: serializeError(e) }, e.message);
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),
@@ -90,7 +93,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
const { message } = fromZodError(e, {
prefix: t('nodes.workflowValidation'),
});
log.error({ error: parseify(e) }, message);
log.error({ error: serializeError(e) }, message);
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),
@@ -99,7 +102,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
});
} else {
// Some other error occurred
log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow'));
log.error({ error: serializeError(e) }, t('nodes.unknownErrorValidatingWorkflow'));
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),

View File

@@ -1,4 +1,5 @@
import type { createStore } from 'app/store/store';
import { useStore } from '@nanostores/react';
import type { AppStore } from 'app/store/store';
import { atom } from 'nanostores';
// Inject socket options and url into window for debugging
@@ -22,7 +23,7 @@ class ReduxStoreNotInitialized extends Error {
}
}
export const $store = atom<Readonly<ReturnType<typeof createStore>> | undefined>();
export const $store = atom<Readonly<AppStore | undefined>>();
export const getStore = () => {
const store = $store.get();
@@ -31,3 +32,11 @@ export const getStore = () => {
}
return store;
};
export const useAppStore = () => {
const store = useStore($store);
if (!store) {
throw new ReduxStoreNotInitialized();
}
return store;
};

View File

@@ -3,37 +3,30 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { JSONObject } from 'common/types';
import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice';
import type { SerializableObject } from 'common/types';
import { deepClone } from 'common/util/deepClone';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import {
controlAdaptersPersistConfig,
controlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
controlLayersPersistConfig,
controlLayersSlice,
controlLayersUndoableConfig,
} from 'features/controlLayers/store/controlLayersSlice';
import { canvasSessionPersistConfig, canvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice';
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
import { queueSlice } from 'features/queue/store/queueSlice';
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { configSlice } from 'features/system/store/configSlice';
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
import { diff } from 'jsondiffpatch';
import { defaultsDeep, keys, omit, pick } from 'lodash-es';
import { keys, mergeWith, omit, pick } from 'lodash-es';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
@@ -48,29 +41,30 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware';
const log = logger('system');
const allReducers = {
[canvasSlice.name]: canvasSlice.reducer,
[api.reducerPath]: api.reducer,
[gallerySlice.name]: gallerySlice.reducer,
[generationSlice.name]: generationSlice.reducer,
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),
[systemSlice.name]: systemSlice.reducer,
[configSlice.name]: configSlice.reducer,
[uiSlice.name]: uiSlice.reducer,
[controlAdaptersSlice.name]: controlAdaptersSlice.reducer,
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
[loraSlice.name]: loraSlice.reducer,
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
[sdxlSlice.name]: sdxlSlice.reducer,
[queueSlice.name]: queueSlice.reducer,
[workflowSlice.name]: workflowSlice.reducer,
[hrfSlice.name]: hrfSlice.reducer,
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
[canvasSlice.name]: undoable(canvasSlice.reducer, canvasUndoableConfig),
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer,
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
[paramsSlice.name]: paramsSlice.reducer,
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@@ -100,27 +94,25 @@ export type PersistConfig<T = any> = {
};
const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasPersistConfig.name]: canvasPersistConfig,
[galleryPersistConfig.name]: galleryPersistConfig,
[generationPersistConfig.name]: generationPersistConfig,
[nodesPersistConfig.name]: nodesPersistConfig,
[systemPersistConfig.name]: systemPersistConfig,
[workflowPersistConfig.name]: workflowPersistConfig,
[uiPersistConfig.name]: uiPersistConfig,
[controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig,
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
[sdxlPersistConfig.name]: sdxlPersistConfig,
[loraPersistConfig.name]: loraPersistConfig,
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
[hrfPersistConfig.name]: hrfPersistConfig,
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
[canvasPersistConfig.name]: canvasPersistConfig,
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[paramsPersistConfig.name]: paramsPersistConfig,
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
[canvasSessionPersistConfig.name]: canvasSessionPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {
const log = logger('system');
const persistConfig = persistConfigs[key as keyof typeof persistConfigs];
if (!persistConfig) {
throw new Error(`No persist config for slice "${key}"`);
@@ -130,17 +122,21 @@ const unserialize: UnserializeFunction = (data, key) => {
const parsed = JSON.parse(data);
// strip out old keys
const stripped = pick(parsed, keys(initialState));
const stripped = pick(deepClone(parsed), keys(initialState));
// run (additive) migrations
const migrated = migrate(stripped);
// merge in initial state as default values, covering any missing keys
const transformed = defaultsDeep(migrated, initialState);
/*
* Merge in initial state as default values, covering any missing keys. You might be tempted to use _.defaultsDeep,
* but that merges arrays by index and partial objects by key. Using an identity function as the customizer results
* in behaviour like defaultsDeep, but doesn't overwrite any values that are not undefined in the migrated state.
*/
const transformed = mergeWith(migrated, initialState, (objVal) => objVal);
log.debug(
{
persistedData: parsed,
rehydratedData: transformed,
diff: diff(parsed, transformed) as JSONObject, // this is always serializable
diff: diff(parsed, transformed) as SerializableObject, // this is always serializable
},
`Rehydrated slice "${key}"`
);
@@ -202,7 +198,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
},
});
export type RootState = ReturnType<ReturnType<typeof createStore>['getState']>;
export type AppStore = ReturnType<typeof createStore>;
export type RootState = ReturnType<AppStore['getState']>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;
export type AppDispatch = ReturnType<typeof createStore>['dispatch'];

View File

@@ -1,6 +1,6 @@
import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
import type { FilterType } from 'features/controlLayers/store/types';
import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { TabName } from 'features/ui/store/uiTypes';
import type { O } from 'ts-toolbelt';
/**
@@ -72,7 +72,7 @@ export type AppConfig = {
maxUpscaleDimension?: number;
allowPrivateBoards: boolean;
allowPrivateStylePresets: boolean;
disabledTabs: InvokeTabName[];
disabledTabs: TabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
nodesAllowlist: string[] | undefined;
@@ -83,7 +83,7 @@ export type AppConfig = {
sd: {
defaultModel?: string;
disabledControlNetModels: string[];
disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[];
disabledControlNetProcessors: FilterType[];
// Core parameters
iterations: NumericalParameterConfig;
width: NumericalParameterConfig; // initial value comes from model

View File

@@ -33,28 +33,23 @@ type IAINoImageFallbackProps = FlexProps & {
};
export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
const { icon = PiImageBold, boxSize = 16, sx, ...rest } = props;
const styles = useMemo(
() => ({
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
flexDir: 'column',
gap: 2,
userSelect: 'none',
opacity: 0.7,
color: 'base.500',
fontSize: 'md',
...sx,
}),
[sx]
);
const { icon = PiImageBold, boxSize = 16, ...rest } = props;
return (
<Flex sx={styles} {...rest}>
<Flex
w="full"
h="full"
alignItems="center"
justifyContent="center"
borderRadius="base"
flexDir="column"
gap={2}
userSelect="none"
opacity={0.7}
color="base.500"
fontSize="md"
{...rest}
>
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
{props.label && <Text textAlign="center">{props.label}</Text>}
</Flex>

View File

@@ -0,0 +1,104 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library';
import type { ReactElement, ReactNode } from 'react';
import { memo, useCallback, useMemo } from 'react';
type IconSwitchProps = {
isChecked: boolean;
onChange: (checked: boolean) => void;
iconChecked: ReactElement;
tooltipChecked?: ReactNode;
iconUnchecked: ReactElement;
tooltipUnchecked?: ReactNode;
ariaLabel: string;
};
const getSx = (padding: string | number): SystemStyleObject => ({
transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
'&[data-checked="true"]': {
left: `calc(100% - ${padding})`,
transform: 'translateX(-100%)',
},
'&[data-checked="false"]': {
left: padding,
transform: 'translateX(0)',
},
});
export const IconSwitch = memo(
({
isChecked,
onChange,
iconChecked,
tooltipChecked,
iconUnchecked,
tooltipUnchecked,
ariaLabel,
}: IconSwitchProps) => {
const onUncheck = useCallback(() => {
onChange(false);
}, [onChange]);
const onCheck = useCallback(() => {
onChange(true);
}, [onChange]);
const gap = useToken('space', 1.5);
const sx = useMemo(() => getSx(gap), [gap]);
return (
<Flex
position="relative"
bg="base.800"
borderRadius="base"
alignItems="center"
justifyContent="center"
h="full"
p={gap}
gap={gap}
>
<Box
position="absolute"
borderRadius="base"
bg="invokeBlue.400"
w={12}
top={gap}
bottom={gap}
data-checked={isChecked}
sx={sx}
/>
<Tooltip hasArrow label={tooltipUnchecked}>
<IconButton
size="sm"
fontSize={16}
icon={iconUnchecked}
onClick={onUncheck}
variant={!isChecked ? 'solid' : 'ghost'}
colorScheme={!isChecked ? 'invokeBlue' : 'base'}
aria-label={ariaLabel}
data-checked={!isChecked}
w={12}
alignSelf="stretch"
h="auto"
/>
</Tooltip>
<Tooltip hasArrow label={tooltipChecked}>
<IconButton
size="sm"
fontSize={16}
icon={iconChecked}
onClick={onCheck}
variant={isChecked ? 'solid' : 'ghost'}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
aria-label={ariaLabel}
data-checked={isChecked}
w={12}
alignSelf="stretch"
h="auto"
/>
</Tooltip>
</Flex>
);
}
);
IconSwitch.displayName = 'IconSwitch';

View File

@@ -13,8 +13,9 @@ import {
Spacer,
Text,
} from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice';
import { selectSystemSlice, setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice';
import { toast } from 'features/toast/toast';
import { merge, omit } from 'lodash-es';
import type { ReactElement } from 'react';
@@ -31,8 +32,13 @@ type Props = {
children: ReactElement;
};
const selectShouldEnableInformationalPopovers = createSelector(
selectSystemSlice,
(system) => system.shouldEnableInformationalPopovers
);
export const InformationalPopover = memo(({ feature, children, inPortal = true, ...rest }: Props) => {
const shouldEnableInformationalPopovers = useAppSelector((s) => s.system.shouldEnableInformationalPopovers);
const shouldEnableInformationalPopovers = useAppSelector(selectShouldEnableInformationalPopovers);
const data = useMemo(() => POPOVER_DATA[feature], [feature]);

View File

@@ -0,0 +1,158 @@
import { logger } from 'app/logging/logger';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { objectKeys } from 'common/util/objectKeys';
import { isEqual } from 'lodash-es';
import type { Atom } from 'nanostores';
import { atom, computed } from 'nanostores';
import type { RefObject } from 'react';
import { useEffect, useMemo } from 'react';
const log = logger('system');
const _INTERACTION_SCOPES = ['gallery', 'canvas', 'stagingArea', 'workflows', 'imageViewer'] as const;
type InteractionScope = (typeof _INTERACTION_SCOPES)[number];
export const $activeScopes = atom<Set<InteractionScope>>(new Set());
type InteractionScopeData = {
targets: Set<HTMLElement>;
$isActive: Atom<boolean>;
};
export const INTERACTION_SCOPES: Record<InteractionScope, InteractionScopeData> = _INTERACTION_SCOPES.reduce(
(acc, region) => {
acc[region] = {
targets: new Set(),
$isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)),
};
return acc;
},
{} as Record<InteractionScope, InteractionScopeData>
);
const formatScopes = (interactionScopes: Set<InteractionScope>) => {
if (interactionScopes.size === 0) {
return 'none';
}
return Array.from(interactionScopes).join(', ');
};
export const addScope = (scope: InteractionScope) => {
const currentScopes = $activeScopes.get();
if (currentScopes.has(scope)) {
return;
}
const newScopes = new Set(currentScopes);
newScopes.add(scope);
$activeScopes.set(newScopes);
log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`);
};
export const removeScope = (scope: InteractionScope) => {
const currentScopes = $activeScopes.get();
if (!currentScopes.has(scope)) {
return;
}
const newScopes = new Set(currentScopes);
newScopes.delete(scope);
$activeScopes.set(newScopes);
log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`);
};
export const setScopes = (scopes: InteractionScope[]) => {
const newScopes = new Set(scopes);
$activeScopes.set(newScopes);
log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`);
};
export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject<HTMLElement>) => {
useEffect(() => {
const element = ref.current;
if (!element) {
return;
}
INTERACTION_SCOPES[scope].targets.add(element);
return () => {
INTERACTION_SCOPES[scope].targets.delete(element);
};
}, [ref, scope]);
};
type UseScopeOnMountOptions = {
mount?: boolean;
unmount?: boolean;
};
const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = {
mount: true,
unmount: true,
};
export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => {
useEffect(() => {
const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options };
if (mount) {
addScope(scope);
}
return () => {
if (unmount) {
removeScope(scope);
}
};
}, [options, scope]);
};
export const useScopeImperativeApi = (scope: InteractionScope) => {
const api = useMemo(() => {
return {
add: () => {
addScope(scope);
},
remove: () => {
removeScope(scope);
},
};
}, [scope]);
return api;
};
const handleFocusEvent = (_event: FocusEvent) => {
const activeElement = document.activeElement;
if (!(activeElement instanceof HTMLElement)) {
return;
}
const newActiveScopes = new Set<InteractionScope>();
for (const scope of objectKeys(INTERACTION_SCOPES)) {
for (const element of INTERACTION_SCOPES[scope].targets) {
if (element.contains(activeElement)) {
newActiveScopes.add(scope);
}
}
}
const oldActiveScopes = $activeScopes.get();
if (!isEqual(oldActiveScopes, newActiveScopes)) {
$activeScopes.set(newActiveScopes);
log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`);
}
};
export const useScopeFocusWatcher = () => {
useAssertSingleton('useScopeFocusWatcher');
useEffect(() => {
window.addEventListener('focus', handleFocusEvent, true);
return () => {
window.removeEventListener('focus', handleFocusEvent, true);
};
}, []);
};

View File

@@ -1,21 +1,74 @@
import { useCallback, useMemo, useState } from 'react';
import { useStore } from '@nanostores/react';
import type { WritableAtom } from 'nanostores';
import { atom } from 'nanostores';
import { useCallback, useState } from 'react';
export const useBoolean = (initialValue: boolean) => {
const [isTrue, set] = useState(initialValue);
const setTrue = useCallback(() => set(true), []);
const setFalse = useCallback(() => set(false), []);
const toggle = useCallback(() => set((v) => !v), []);
type UseBoolean = {
isTrue: boolean;
setTrue: () => void;
setFalse: () => void;
set: (value: boolean) => void;
toggle: () => void;
};
const api = useMemo(
() => ({
/**
* Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom.
* Returns a tuple containing the hook and the atom. Use this for global boolean state.
* @param initialValue Initial value of the boolean
*/
export const buildUseBoolean = (initialValue: boolean): [() => UseBoolean, WritableAtom<boolean>] => {
const $boolean = atom(initialValue);
const setTrue = () => {
$boolean.set(true);
};
const setFalse = () => {
$boolean.set(false);
};
const set = (value: boolean) => {
$boolean.set(value);
};
const toggle = () => {
$boolean.set(!$boolean.get());
};
const useBoolean = () => {
const isTrue = useStore($boolean);
return {
isTrue,
set,
setTrue,
setFalse,
set,
toggle,
}),
[isTrue, set, setTrue, setFalse, toggle]
);
};
};
return api;
return [useBoolean, $boolean] as const;
};
/**
* Hook to manage a boolean state. Use this for a local boolean state.
* @param initialValue Initial value of the boolean
*/
export const useBoolean = (initialValue: boolean) => {
const [isTrue, set] = useState(initialValue);
const setTrue = useCallback(() => {
set(true);
}, [set]);
const setFalse = useCallback(() => {
set(false);
}, [set]);
const toggle = useCallback(() => {
set((val) => !val);
}, [set]);
return {
isTrue,
setTrue,
setFalse,
set,
toggle,
};
};

View File

@@ -1,7 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useState } from 'react';
import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
@@ -14,13 +15,9 @@ const accept: Accept = {
'image/jpeg': ['.jpg', '.jpeg', '.png'],
};
const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => {
const selectPostUploadAction = createMemoizedSelector(selectActiveTab, (activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'canvas') {
postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
}
if (activeTabName === 'upscaling') {
postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' };
}
@@ -30,10 +27,9 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac
export const useFullscreenDropzone = () => {
const { t } = useTranslation();
const postUploadAction = useAppSelector(selectPostUploadAction);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const postUploadAction = useAppSelector(selectPostUploadAction);
const [uploadImage] = useUploadImageMutation();
const fileRejectionCallback = useCallback(
@@ -51,7 +47,7 @@ export const useFullscreenDropzone = () => {
);
const fileAcceptedCallback = useCallback(
async (file: File) => {
(file: File) => {
uploadImage({
file,
image_category: 'user',
@@ -101,7 +97,7 @@ export const useFullscreenDropzone = () => {
useEffect(() => {
// This is a hack to allow pasting images into the uploader
const handlePaste = async (e: ClipboardEvent) => {
const handlePaste = (e: ClipboardEvent) => {
if (!dropzone.inputRef.current) {
return;
}

View File

@@ -1,6 +1,7 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -16,7 +17,7 @@ export const useGlobalHotkeys = () => {
['ctrl+enter', 'meta+enter'],
queueBack,
{
enabled: () => !isDisabledQueueBack && !isLoadingQueueBack,
enabled: !isDisabledQueueBack && !isLoadingQueueBack,
preventDefault: true,
enableOnFormTags: ['input', 'textarea', 'select'],
},
@@ -29,7 +30,7 @@ export const useGlobalHotkeys = () => {
['ctrl+shift+enter', 'meta+shift+enter'],
queueFront,
{
enabled: () => !isDisabledQueueFront && !isLoadingQueueFront,
enabled: !isDisabledQueueFront && !isLoadingQueueFront,
preventDefault: true,
enableOnFormTags: ['input', 'textarea', 'select'],
},
@@ -46,7 +47,7 @@ export const useGlobalHotkeys = () => {
['shift+x'],
cancelQueueItem,
{
enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
preventDefault: true,
},
[cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem]
@@ -58,7 +59,7 @@ export const useGlobalHotkeys = () => {
['ctrl+shift+x', 'meta+shift+x'],
clearQueue,
{
enabled: () => !isDisabledClearQueue && !isLoadingClearQueue,
enabled: !isDisabledClearQueue && !isLoadingClearQueue,
preventDefault: true,
},
[clearQueue, isDisabledClearQueue, isLoadingClearQueue]
@@ -68,6 +69,8 @@ export const useGlobalHotkeys = () => {
'1',
() => {
dispatch(setActiveTab('generation'));
addScope('canvas');
removeScope('workflows');
},
[dispatch]
);
@@ -75,7 +78,9 @@ export const useGlobalHotkeys = () => {
useHotkeys(
'2',
() => {
dispatch(setActiveTab('canvas'));
dispatch(setActiveTab('upscaling'));
removeScope('canvas');
removeScope('workflows');
},
[dispatch]
);
@@ -84,6 +89,8 @@ export const useGlobalHotkeys = () => {
'3',
() => {
dispatch(setActiveTab('workflows'));
removeScope('canvas');
addScope('workflows');
},
[dispatch]
);
@@ -93,6 +100,7 @@ export const useGlobalHotkeys = () => {
() => {
if (isModelManagerEnabled) {
dispatch(setActiveTab('models'));
setScopes([]);
}
},
[dispatch, isModelManagerEnabled]
@@ -102,6 +110,7 @@ export const useGlobalHotkeys = () => {
isModelManagerEnabled ? '5' : '4',
() => {
dispatch(setActiveTab('queue'));
setScopes([]);
},
[dispatch, isModelManagerEnabled]
);

View File

@@ -1,6 +1,8 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import type { GroupBase } from 'chakra-react-select';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type { ModelIdentifierField } from 'features/nodes/types/common';
import { groupBy, reduce } from 'lodash-es';
import { useCallback, useMemo } from 'react';
@@ -28,11 +30,13 @@ const groupByBaseFunc = <T extends AnyModelConfig>(model: T) => model.base.toUpp
const groupByBaseAndTypeFunc = <T extends AnyModelConfig>(model: T) =>
`${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`;
const selectBaseWithSDXLFallback = createSelector(selectParamsSlice, (params) => params.model?.base ?? 'sdxl');
export const useGroupedModelCombobox = <T extends AnyModelConfig>(
arg: UseGroupedModelComboboxArg<T>
): UseGroupedModelComboboxReturn => {
const { t } = useTranslation();
const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl');
const base = useAppSelector(selectBaseWithSDXLFallback);
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
if (!modelConfigs) {
@@ -54,9 +58,9 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
},
[] as GroupBase<ComboboxOption>[]
);
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1));
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1));
return _options;
}, [modelConfigs, groupByType, getIsDisabled, base_model]);
}, [modelConfigs, groupByType, getIsDisabled, base]);
const value = useMemo(
() =>

View File

@@ -1,4 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useUploadImageMutation } from 'services/api/endpoints/images';
@@ -29,7 +30,7 @@ type UseImageUploadButtonArgs = {
* <input {...getUploadInputProps()} /> // hidden, handles native upload functionality
*/
export const useImageUploadButton = ({ postUploadAction, isDisabled }: UseImageUploadButtonArgs) => {
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const [uploadImage] = useUploadImageMutation();
const onDropAccepted = useCallback(
(files: File[]) => {

View File

@@ -1,67 +1,51 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectControlAdapterAll,
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { Templates } from 'features/nodes/store/types';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { selectUpscalelice } from 'features/parameters/store/upscaleSlice';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es';
import { atom } from 'nanostores';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
const LAYER_TYPE_TO_TKEY: Record<Layer['type'], string> = {
initial_image_layer: 'controlLayers.globalInitialImage',
control_adapter_layer: 'controlLayers.globalControlAdapter',
ip_adapter_layer: 'controlLayers.globalIPAdapter',
regional_guidance_layer: 'controlLayers.regionalGuidance',
};
const LAYER_TYPE_TO_TKEY = {
ip_adapter: 'controlLayers.ipAdapter',
inpaint_mask: 'controlLayers.inpaintMask',
regional_guidance: 'controlLayers.regionalGuidance',
raster_layer: 'controlLayers.raster',
control_layer: 'controlLayers.globalControlAdapter',
} as const;
const createSelector = (templates: Templates) =>
const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy: boolean) =>
createMemoizedSelector(
[
selectControlAdaptersSlice,
selectGenerationSlice,
selectSystemSlice,
selectNodesSlice,
selectWorkflowSettingsSlice,
selectDynamicPromptsSlice,
selectControlLayersSlice,
activeTabNameSelector,
selectCanvasSlice,
selectParamsSlice,
selectUpscalelice,
selectConfigSlice,
selectActiveTab,
],
(
controlAdapters,
generation,
system,
nodes,
workflowSettings,
dynamicPrompts,
controlLayers,
activeTabName,
upscale,
config
) => {
const { model } = generation;
const { size } = controlLayers.present;
const { positivePrompt } = controlLayers.present;
const { isConnected } = system;
(system, nodes, workflowSettings, dynamicPrompts, canvas, params, upscale, config, activeTabName) => {
const { bbox } = canvas;
const { model, positivePrompt } = params;
const reasons: { prefix?: string; content: string }[] = [];
@@ -114,7 +98,31 @@ const createSelector = (templates: Templates) =>
});
});
}
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
}
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
if (canvasIsBusy) {
reasons.push({ content: i18n.t('parameters.invoke.canvasBusy') });
}
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
}
@@ -123,149 +131,132 @@ const createSelector = (templates: Templates) =>
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
if (activeTabName === 'generation') {
// Handling for generation tab
controlLayers.present.layers
.filter((l) => l.isEnabled)
.forEach((l, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
if (l.type === 'control_adapter_layer') {
// Must have model
if (!l.controlAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
// Model base must match
if (l.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// Must have a control image OR, if it has a processor, it must have a processed image
if (!l.controlAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
} else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (l.controlAdapter.type === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (size.width % multiple !== 0 || size.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
}
}
}
if (l.type === 'ip_adapter_layer') {
// Must have model
if (!l.ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (l.ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!l.ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
}
if (l.type === 'initial_image_layer') {
// Must have an image
if (!l.image) {
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
}
}
if (l.type === 'regional_guidance_layer') {
// Must have a region
if (l.maskObjects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
l.ipAdapters.forEach((ipAdapter) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
});
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
canvas.controlLayers.entities
.filter((controlLayer) => controlLayer.isEnabled)
.forEach((controlLayer, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!controlLayer.controlAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
// Handling for all other tabs
selectControlAdapterAll(controlAdapters)
.filter((ca) => ca.isEnabled)
.forEach((ca, i) => {
if (!ca.isEnabled) {
return;
// Model base must match
if (controlLayer.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (controlLayer.controlAdapter.type === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
}
}
if (!ca.model) {
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
} else if (ca.model.base !== model?.base) {
// This should never happen, just a sanity check
reasons.push({
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
});
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvas.ipAdapters.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!entity.ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (entity.ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!entity.ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvas.regions.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have a region
if (entity.objects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
entity.ipAdapters.forEach((ipAdapter) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
if (
!ca.controlImage ||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
) {
reasons.push({
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }),
});
// Model base must match
if (ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
});
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvas.rasterLayers.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
}
return { isReady: !reasons.length, reasons };
}
);
const dummyAtom = atom(true);
export const useIsReadyToEnqueue = () => {
const templates = useStore($templates);
const selector = useMemo(() => createSelector(templates), [templates]);
const isConnected = useStore($isConnected);
const canvasManager = useCanvasManagerSafe();
const canvasIsBusy = useStore(canvasManager?.$isBusy ?? dummyAtom);
const selector = useMemo(
() => createSelector(templates, isConnected, canvasIsBusy),
[templates, isConnected, canvasIsBusy]
);
const value = useAppSelector(selector);
return value;
};

View File

@@ -1,35 +0,0 @@
// https://stackoverflow.com/a/73731908
import { useCallback, useEffect, useState } from 'react';
type UseSingleAndDoubleClickOptions = {
onSingleClick: () => void;
onDoubleClick: () => void;
latency?: number;
};
export function useSingleAndDoubleClick({
onSingleClick,
onDoubleClick,
latency = 250,
}: UseSingleAndDoubleClickOptions): () => void {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
if (click === 1) {
onSingleClick();
}
setClick(0);
}, latency);
if (click === 2) {
onDoubleClick();
}
return () => clearTimeout(timer);
}, [click, onDoubleClick, latency, onSingleClick]);
const onClick = useCallback(() => setClick((prev) => prev + 1), []);
return onClick;
}

View File

@@ -1,5 +1,4 @@
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
export interface JSONObject {
[k: string]: JSONValue;
}
type SerializableValue = string | number | boolean | null | undefined | SerializableValue[] | SerializableObject;
export type SerializableObject = {
[k: string | number]: SerializableValue;
};

View File

@@ -0,0 +1,115 @@
import { describe, expect, it, vi } from 'vitest';
import { SyncableMap } from './SyncableMap';
describe('SyncableMap', () => {
it('should initialize with entries', () => {
const initialEntries = [
['key1', 'value1'],
['key2', 'value2'],
] as const;
const map = new SyncableMap(initialEntries);
expect(map.size).toBe(2);
expect(map.get('key1')).toBe('value1');
expect(map.get('key2')).toBe('value2');
});
it('should notify subscribers when a key is set', () => {
const map = new SyncableMap<string, string>();
const subscriber = vi.fn();
map.subscribe(subscriber);
map.set('key1', 'value1');
expect(subscriber).toHaveBeenCalledTimes(1);
expect(map.get('key1')).toBe('value1');
});
it('should notify subscribers when a key is deleted', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const subscriber = vi.fn();
map.subscribe(subscriber);
map.delete('key1');
expect(subscriber).toHaveBeenCalledTimes(1);
expect(map.get('key1')).toBeUndefined();
});
it('should notify subscribers when the map is cleared', () => {
const map = new SyncableMap<string, string>([
['key1', 'value1'],
['key2', 'value2'],
]);
const subscriber = vi.fn();
map.subscribe(subscriber);
map.clear();
expect(subscriber).toHaveBeenCalledTimes(1);
expect(map.size).toBe(0);
});
it('should not notify unsubscribed callbacks', () => {
const map = new SyncableMap<string, string>();
const subscriber = vi.fn();
const unsubscribe = map.subscribe(subscriber);
unsubscribe();
map.set('key1', 'value1');
expect(subscriber).not.toHaveBeenCalled();
});
it('should return a snapshot of the current state', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const snapshot = map.getSnapshot();
expect(snapshot.size).toBe(1);
expect(snapshot.get('key1')).toBe('value1');
});
it('should return the same snapshot if there were no changes', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const firstSnapshot = map.getSnapshot();
const secondSnapshot = map.getSnapshot();
expect(firstSnapshot).toBe(secondSnapshot);
});
it('should return a new snapshot if changes were made', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const firstSnapshot = map.getSnapshot();
map.set('key2', 'value2');
const secondSnapshot = map.getSnapshot();
expect(firstSnapshot).not.toBe(secondSnapshot);
expect(secondSnapshot.size).toBe(2);
});
it('should consider different snapshots unequal', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const firstSnapshot = map.getSnapshot();
map.set('key2', 'value2');
const secondSnapshot = map.getSnapshot();
expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(false);
});
it('should consider identical snapshots equal', () => {
const map = new SyncableMap<string, string>([
['key1', 'value1'],
['key2', 'value2'],
]);
const firstSnapshot = map.getSnapshot();
const secondSnapshot = map.getSnapshot();
expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(true);
});
});

View File

@@ -0,0 +1,86 @@
/**
* A Map that allows for subscribing to changes and getting a snapshot of the current state.
*
* It can be used with the `useSyncExternalStore` hook to sync the state of the map with a React component.
*
* Reactivity is shallow, so changes to nested objects will not trigger a re-render.
*/
export class SyncableMap<K, V> extends Map<K, V> {
private subscriptions = new Set<() => void>();
private lastSnapshot: Map<K, V> | null = null;
constructor(entries?: readonly (readonly [K, V])[] | null) {
super(entries);
}
set = (key: K, value: V): this => {
super.set(key, value);
this.notifySubscribers();
return this;
};
delete = (key: K): boolean => {
const result = super.delete(key);
this.notifySubscribers();
return result;
};
clear = (): void => {
super.clear();
this.notifySubscribers();
};
/**
* Notify all subscribers that the map has changed.
*/
private notifySubscribers = () => {
for (const callback of this.subscriptions) {
callback();
}
};
/**
* Subscribe to changes to the map.
* @param callback A function to call when the map changes
* @returns A function to unsubscribe from changes
*/
subscribe = (callback: () => void): (() => void) => {
this.subscriptions.add(callback);
return () => {
this.subscriptions.delete(callback);
};
};
/**
* Get a snapshot of the current state of the map.
* @returns A snapshot of the current state of the map
*/
getSnapshot = (): Map<K, V> => {
const currentSnapshot = new Map(this);
if (!this.lastSnapshot || !this.areSnapshotsEqual(this.lastSnapshot, currentSnapshot)) {
this.lastSnapshot = currentSnapshot;
}
return this.lastSnapshot;
};
/**
* Compare two snapshots to determine if they are equal.
* @param snapshotA The first snapshot to compare
* @param snapshotB The second snapshot to compare
* @returns Whether the two snapshots are equal
*/
private areSnapshotsEqual = (snapshotA: Map<K, V>, snapshotB: Map<K, V>): boolean => {
if (snapshotA.size !== snapshotB.size) {
return false;
}
for (const [key, value] of snapshotA) {
if (!Object.is(value, snapshotB.get(key))) {
return false;
}
}
return true;
};
}

View File

@@ -1,28 +0,0 @@
export const getImageDataTransparency = (pixels: Uint8ClampedArray) => {
let isFullyTransparent = true;
let isPartiallyTransparent = false;
const len = pixels.length;
let i = 3;
for (i; i < len; i += 4) {
if (pixels[i] === 255) {
isFullyTransparent = false;
} else {
isPartiallyTransparent = true;
}
if (!isFullyTransparent && isPartiallyTransparent) {
return { isFullyTransparent, isPartiallyTransparent };
}
}
return { isFullyTransparent, isPartiallyTransparent };
};
export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => {
const len = pixels.length;
let i = 0;
for (i; i < len; ) {
if (pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 255) {
return true;
}
}
return false;
};

View File

@@ -1,85 +1,170 @@
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { describe, expect, it } from 'vitest';
describe('Array Manipulation Functions', () => {
const originalArray = ['a', 'b', 'c', 'd'];
describe('moveForwardOne', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
describe('moveOneToEnd', () => {
describe('with callback', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveToFront', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
describe('moveToStart', () => {
describe('with callback', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToStart(array, 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToStart(array, 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToStart(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveBackwardsOne', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
describe('moveOneToStart', () => {
describe('with callback', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveToBack', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
describe('moveToEnd', () => {
describe('with callback', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToEnd(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
});

View File

@@ -1,37 +1,45 @@
export const moveForward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
if (index >= 0 && index < array.length - 1) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index + 1]] = [array[index + 1], array[index]];
}
return array;
};
export const moveToFront = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveToStart<T>(array: T[], item: T): T[];
export function moveToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index > 0) {
const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check
array.unshift(item);
}
return array;
};
}
export const moveBackward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveOneToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveOneToStart<T>(array: T[], item: T): T[];
export function moveOneToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index > 0) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index - 1]] = [array[index - 1], array[index]];
}
return array;
};
}
export const moveToBack = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveToEnd<T>(array: T[], item: T): T[];
export function moveToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index >= 0 && index < array.length - 1) {
const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check
array.push(item);
}
return array;
};
}
export function moveOneToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveOneToEnd<T>(array: T[], item: T): T[];
export function moveOneToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index >= 0 && index < array.length - 1) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index + 1]] = [array[index + 1], array[index]];
}
return array;
}

View File

@@ -1,4 +1,4 @@
import type { RgbaColor } from 'react-colorful';
import type { RgbaColor, RgbColor } from 'react-colorful';
export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string {
const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1);
@@ -15,3 +15,13 @@ export function hexToRGBA(hex: string, alpha: number) {
const b = parseInt(hex.substring(4, 6), 16);
return { r, g, b, a: alpha };
}
export const rgbaColorToString = (color: RgbaColor): string => {
const { r, g, b, a } = color;
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
export const rgbColorToString = (color: RgbColor): string => {
const { r, g, b } = color;
return `rgba(${r}, ${g}, ${b})`;
};

View File

@@ -7,7 +7,7 @@ import { $authToken } from 'app/store/nanostores/authToken';
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
*/
export const convertImageUrlToBlob = async (url: string) =>
export const convertImageUrlToBlob = (url: string) =>
new Promise<Blob | null>((resolve, reject) => {
const img = new Image();
img.onload = () => {

View File

@@ -1,7 +0,0 @@
export const isInputElement = (el: HTMLElement) => {
return (
el.tagName.toLowerCase() === 'input' ||
el.tagName.toLowerCase() === 'textarea' ||
el.tagName.toLowerCase() === 'select'
);
};

Some files were not shown because too many files have changed in this diff Show More