mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
33 Commits
bug-instal
...
feat/ui/re
Author | SHA1 | Date | |
---|---|---|---|
54e46faa54 | |||
183945077d | |||
0fea08df7d | |||
5fe40d228e | |||
1c507d088a | |||
87893d29e9 | |||
5667465029 | |||
9021841a49 | |||
15703c8045 | |||
eb2bbf1609 | |||
aca953044e | |||
f5a0050a00 | |||
9e38558633 | |||
93e589a738 | |||
4f3dd6dbca | |||
643ef964ac | |||
d8349ed42f | |||
53f2008893 | |||
60acd4e02f | |||
82c471ec2a | |||
47d76f8033 | |||
b02e11d2b5 | |||
24d67c77e1 | |||
b704941119 | |||
dd3b955b8a | |||
7613ef3d30 | |||
fba7f36038 | |||
2bfd4407ad | |||
265ccab15f | |||
ebf1f1bf6b | |||
07ce7685b1 | |||
521d91ea58 | |||
7521dff206 |
@ -4,7 +4,23 @@ The app makes heavy use of Redux Toolkit, its Query library, and `nanostores`.
|
||||
|
||||
## Redux
|
||||
|
||||
TODO
|
||||
We use [Redux Toolkit] + RTK Query extensively.
|
||||
|
||||
### Persistence
|
||||
|
||||
The usual persistence layer for redux is [redux-persist]. Unfortunately, it is abandoned, and not possible to fork. Past releases of it depend on a malicious package that was removed from npm, so it's very difficult (impossible?) to build. The current state of the repo is also non-functional, as it was abandoned mid-rewrite.
|
||||
|
||||
We had a need to debounce our persistence, and patched redux-persist's build directly to do so. This didn't feel great. We've since moved to [redux-remember], a well-designed, minimal, and actively maintained library.
|
||||
|
||||
#### Slice migration
|
||||
|
||||
When rehydrating state, we sometimes need to migrate data. This is handled by the `unserialize` function in [store.ts], which is used by redux-remember to rehydrate persisted state. This function uses some lodash utils to strip out unknown keys, and merge any new keys into the rehydrated state.
|
||||
|
||||
Sometimes the shape of state changes, but it keeps the same property name in the slice. In that case, we need to _transform_ incoming data.
|
||||
|
||||
To handle this, each persisted slice must have a `SliceConfig`, which includes its latest initial value, and a migrate function. The migrate function, defined in the slice, does does any transformations and updates the version.
|
||||
|
||||
The version of the slice is currently only incremented when we need to run _transform_ migrations. If keys are added or removed from state, the version is not bumped.
|
||||
|
||||
## `nanostores`
|
||||
|
||||
@ -36,3 +52,7 @@ const myStringOption = useStore($myStringOption);
|
||||
- For performance-critical code and in callbacks, redux selectors can be problematic due to the declarative reactivity system. Consider refactoring to use `nanostores` if there's a **measurable** performance issue.
|
||||
|
||||
[nanostores]: https://github.com/nanostores/nanostores/
|
||||
[Redux Toolkit]: https://redux-toolkit.js.org/
|
||||
[redux-persist]: https://github.com/rt2zz/redux-persist
|
||||
[redux-remember]: https://github.com/zewish/redux-remember
|
||||
[store.ts]: invokeai/frontend/web/src/app/store/store.ts
|
||||
|
@ -54,7 +54,7 @@
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource-variable/inter": "^5.0.16",
|
||||
"@invoke-ai/ui-library": "^0.0.18",
|
||||
"@invoke-ai/ui-library": "^0.0.20",
|
||||
"@mantine/form": "6.0.21",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@reduxjs/toolkit": "2.0.1",
|
||||
|
41
invokeai/frontend/web/pnpm-lock.yaml
generated
41
invokeai/frontend/web/pnpm-lock.yaml
generated
@ -29,8 +29,8 @@ dependencies:
|
||||
specifier: ^5.0.16
|
||||
version: 5.0.16
|
||||
'@invoke-ai/ui-library':
|
||||
specifier: ^0.0.18
|
||||
version: 0.0.18(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.1)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: ^0.0.20
|
||||
version: 0.0.20(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.1)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mantine/form':
|
||||
specifier: 6.0.21
|
||||
version: 6.0.21(react@18.2.0)
|
||||
@ -1700,6 +1700,13 @@ packages:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
/@babel/runtime@7.23.9:
|
||||
resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
dev: false
|
||||
|
||||
/@babel/template@7.22.15:
|
||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1960,7 +1967,7 @@ packages:
|
||||
dependencies:
|
||||
'@chakra-ui/dom-utils': 2.1.0
|
||||
react: 18.2.0
|
||||
react-focus-lock: 2.9.6(@types/react@18.2.48)(react@18.2.0)
|
||||
react-focus-lock: 2.9.7(@types/react@18.2.48)(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
@ -2992,7 +2999,7 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
'@babel/runtime': 7.23.9
|
||||
'@emotion/babel-plugin': 11.11.0
|
||||
'@emotion/is-prop-valid': 1.2.1
|
||||
'@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0)
|
||||
@ -3587,8 +3594,8 @@ packages:
|
||||
prettier: 3.2.4
|
||||
dev: true
|
||||
|
||||
/@invoke-ai/ui-library@0.0.18(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.1)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Yme+2+pzYy3TPb7ZT0hYmBwahH29ZRSVIxLKSexh3BsbJXbTzGssRQU78QvK6Ymxemgbso3P8Rs+IW0zNhQKjQ==}
|
||||
/@invoke-ai/ui-library@0.0.20(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.1)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-8BDL9LWbmpAZHTJB0B+zMJ0kBq7vQF0tem6q9WB03CPqdE307FiDmZ++NZF7BP8Rp4Sivdi6OagaXL7WV6e0Pw==}
|
||||
peerDependencies:
|
||||
'@fontsource-variable/inter': ^5.0.16
|
||||
react: ^18.2.0
|
||||
@ -3610,8 +3617,8 @@ packages:
|
||||
framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0)
|
||||
lodash-es: 4.17.21
|
||||
nanostores: 0.9.5
|
||||
overlayscrollbars: 2.4.7
|
||||
overlayscrollbars-react: 0.5.4(overlayscrollbars@2.4.7)(react@18.2.0)
|
||||
overlayscrollbars: 2.5.0
|
||||
overlayscrollbars-react: 0.5.4(overlayscrollbars@2.5.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-i18next: 14.0.1(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -11333,13 +11340,13 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/overlayscrollbars-react@0.5.4(overlayscrollbars@2.4.7)(react@18.2.0):
|
||||
/overlayscrollbars-react@0.5.4(overlayscrollbars@2.5.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-FPKx9XnXovTnI4+2JXig5uEaTLSEJ6svOwPzIfBBXTHBRNsz2+WhYUmfM0K/BNYxjgDEwuPm+NQhEoOA0RoG1g==}
|
||||
peerDependencies:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
overlayscrollbars: 2.4.7
|
||||
overlayscrollbars: 2.5.0
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
@ -11347,8 +11354,8 @@ packages:
|
||||
resolution: {integrity: sha512-C7tmhetwMv9frEvIT/RfkAVEgbjRNz/Gh2zE8BVmN+jl35GRaAnz73rlGQCMRoC2arpACAXyMNnJkzHb7GBrcA==}
|
||||
dev: false
|
||||
|
||||
/overlayscrollbars@2.4.7:
|
||||
resolution: {integrity: sha512-02X2/nHno35dzebCx+EO2tRDaKAOltZqUKdUqvq3Pt8htCuhJbYi+mjr0CYerVeGRRoZ2Uo6/8XrNg//DJJ+GA==}
|
||||
/overlayscrollbars@2.5.0:
|
||||
resolution: {integrity: sha512-CWVC2dwS07XZfLHDm5GmZN1iYggiJ8Vufnvzwt0gwR9Yz1hVckKeTxg7VILZeYVGhDYJHZ1Xc8Xfys5dWZ1qiA==}
|
||||
dev: false
|
||||
|
||||
/p-limit@2.3.0:
|
||||
@ -11836,7 +11843,7 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
'@babel/runtime': 7.23.9
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
@ -11922,8 +11929,8 @@ packages:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
dev: false
|
||||
|
||||
/react-focus-lock@2.9.6(@types/react@18.2.48)(react@18.2.0):
|
||||
resolution: {integrity: sha512-B7gYnCjHNrNYwY2juS71dHbf0+UpXXojt02svxybj8N5bxceAkzPChKEncHuratjUHkIFNCn06k2qj1DRlzTug==}
|
||||
/react-focus-lock@2.9.7(@types/react@18.2.48)(react@18.2.0):
|
||||
resolution: {integrity: sha512-EfhX040SELLqnQ9JftqsmQCG49iByg8F5X5m19Er+n371OaETZ35dlNPZrLOOTlnnwD4c2Zv0KDgabDTc7dPHw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
@ -11931,7 +11938,7 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
'@babel/runtime': 7.23.9
|
||||
'@types/react': 18.2.48
|
||||
focus-lock: 1.0.0
|
||||
prop-types: 15.8.1
|
||||
@ -11993,7 +12000,7 @@ packages:
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
'@babel/runtime': 7.23.9
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 23.7.16
|
||||
react: 18.2.0
|
||||
|
@ -1609,6 +1609,13 @@
|
||||
"showProgressImages": "Show Progress Images",
|
||||
"swapSizes": "Swap Sizes"
|
||||
},
|
||||
"viewer": {
|
||||
"viewerModeImage": "Image",
|
||||
"viewerModeInfo": "Info",
|
||||
"viewerModeProgress": "Progress",
|
||||
"dropLabel": "View Image",
|
||||
"noProgress": "Nothing in Progress"
|
||||
},
|
||||
"unifiedCanvas": {
|
||||
"accept": "Accept",
|
||||
"activeLayer": "Active Layer",
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { canvasBatchIdsReset, commitStagingAreaImage, discardStagedImages } from 'features/canvas/store/canvasSlice';
|
||||
import { matchAnyStagingAreaDismissed } from 'features/canvas/store/canvasSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { t } from 'i18next';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages);
|
||||
|
||||
export const addCommitStagingAreaImageListener = () => {
|
||||
startAppListening({
|
||||
matcher,
|
||||
matcher: matchAnyStagingAreaDismissed,
|
||||
effect: async (_, { dispatch, getState }) => {
|
||||
const log = logger('canvas');
|
||||
const state = getState();
|
||||
const { batchIds } = state.canvas;
|
||||
const { canvasBatchIds } = state.progress;
|
||||
|
||||
try {
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' })
|
||||
queueApi.endpoints.cancelByBatchIds.initiate(
|
||||
{ batch_ids: canvasBatchIds },
|
||||
{ fixedCacheKey: 'cancelByBatchIds' }
|
||||
)
|
||||
);
|
||||
const { canceled } = await req.unwrap();
|
||||
req.reset();
|
||||
@ -32,7 +32,6 @@ export const addCommitStagingAreaImageListener = () => {
|
||||
})
|
||||
);
|
||||
}
|
||||
dispatch(canvasBatchIdsReset());
|
||||
} catch {
|
||||
log.error('Failed to cancel canvas batches');
|
||||
dispatch(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageCache } from 'services/api/types';
|
||||
@ -28,7 +28,7 @@ export const addFirstListImagesListener = () => {
|
||||
if (data.ids.length > 0) {
|
||||
// Select the first image
|
||||
const firstImage = imagesSelectors.selectAll(data)[0];
|
||||
dispatch(imageSelected(firstImage ?? null));
|
||||
dispatch(imageSelectionChanged(firstImage ?? null));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
@ -37,17 +37,17 @@ export const addBoardIdSelectedListener = () => {
|
||||
|
||||
if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) {
|
||||
const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName);
|
||||
dispatch(imageSelected(selectedImage || null));
|
||||
dispatch(imageSelectionChanged(selectedImage ?? null));
|
||||
} else if (boardImagesData) {
|
||||
const firstImage = imagesSelectors.selectAll(boardImagesData)[0];
|
||||
dispatch(imageSelected(firstImage || null));
|
||||
dispatch(imageSelectionChanged(firstImage ?? null));
|
||||
} else {
|
||||
// board has no images - deselect
|
||||
dispatch(imageSelected(null));
|
||||
dispatch(imageSelectionChanged(null));
|
||||
}
|
||||
} else {
|
||||
// fallback - deselect
|
||||
dispatch(imageSelected(null));
|
||||
dispatch(imageSelectionChanged(null));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -2,13 +2,14 @@ import { logger } from 'app/logging/logger';
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
|
||||
import { 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 { buildCanvasGraph } from 'features/nodes/util/graph/buildCanvasGraph';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { canvasBatchEnqueued } from 'features/progress/store/progressSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@ -121,8 +122,6 @@ export const addEnqueueRequestedCanvasListener = () => {
|
||||
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(
|
||||
@ -135,8 +134,9 @@ export const addEnqueueRequestedCanvasListener = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Associate the session with the canvas session ID
|
||||
dispatch(canvasBatchIdAdded(batchId));
|
||||
if (enqueueResult.batch.batch_id) {
|
||||
dispatch(canvasBatchEnqueued(enqueueResult.batch.batch_id));
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
@ -1,19 +1,10 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { galleryImageClicked, imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
import { startAppListening } from '..';
|
||||
|
||||
export const galleryImageClicked = createAction<{
|
||||
imageDTO: ImageDTO;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
}>('gallery/imageClicked');
|
||||
|
||||
/**
|
||||
* This listener handles the logic for selecting images in the gallery.
|
||||
*
|
||||
@ -52,16 +43,16 @@ export const addGalleryImageClickedListener = () => {
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
const imagesToSelect = imageDTOs.slice(start, end + 1);
|
||||
dispatch(selectionChanged(selection.concat(imagesToSelect)));
|
||||
dispatch(imageSelectionChanged(selection.concat(imagesToSelect)));
|
||||
}
|
||||
} else if (ctrlKey || metaKey) {
|
||||
if (selection.some((i) => i.image_name === imageDTO.image_name) && selection.length > 1) {
|
||||
dispatch(selectionChanged(selection.filter((n) => n.image_name !== imageDTO.image_name)));
|
||||
dispatch(imageSelectionChanged(selection.filter((n) => n.image_name !== imageDTO.image_name)));
|
||||
} else {
|
||||
dispatch(selectionChanged(selection.concat(imageDTO)));
|
||||
dispatch(imageSelectionChanged(selection.concat(imageDTO)));
|
||||
}
|
||||
} else {
|
||||
dispatch(selectionChanged([imageDTO]));
|
||||
dispatch(imageSelectionChanged(imageDTO));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
@ -62,9 +62,9 @@ export const addRequestedSingleImageDeletionListener = () => {
|
||||
const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex];
|
||||
|
||||
if (newSelectedImageDTO) {
|
||||
dispatch(imageSelected(newSelectedImageDTO));
|
||||
dispatch(imageSelectionChanged(newSelectedImageDTO));
|
||||
} else {
|
||||
dispatch(imageSelected(null));
|
||||
dispatch(imageSelectionChanged(null));
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,9 +160,9 @@ export const addRequestedMultipleImageDeletionListener = () => {
|
||||
const newSelectedImageDTO = data ? imagesSelectors.selectAll(data)[0] : undefined;
|
||||
|
||||
if (newSelectedImageDTO) {
|
||||
dispatch(imageSelected(newSelectedImageDTO));
|
||||
dispatch(imageSelectionChanged(newSelectedImageDTO));
|
||||
} else {
|
||||
dispatch(imageSelected(null));
|
||||
dispatch(imageSelectionChanged(null));
|
||||
}
|
||||
|
||||
dispatch(isModalOpenChanged(false));
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
controlAdapterIsEnabledChanged,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@ -37,14 +37,14 @@ export const addImageDroppedListener = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on current image
|
||||
* Image dropped on viewer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
||||
overData.actionType === 'SET_VIEWER_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(imageSelected(activeData.payload.imageDTO));
|
||||
dispatch(imageSelectionChanged(activeData.payload.imageDTO));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@ -25,7 +25,7 @@ export const addImagesStarredListener = () => {
|
||||
updatedSelection.push(selectedImageDTO);
|
||||
}
|
||||
});
|
||||
dispatch(selectionChanged(updatedSelection));
|
||||
dispatch(imageSelectionChanged(updatedSelection));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@ -25,7 +25,7 @@ export const addImagesUnstarredListener = () => {
|
||||
updatedSelection.push(selectedImageDTO);
|
||||
}
|
||||
});
|
||||
dispatch(selectionChanged(updatedSelection));
|
||||
dispatch(imageSelectionChanged(updatedSelection));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { isImageOutput } from 'features/nodes/types/common';
|
||||
import { LINEAR_UI_OUTPUT, nodeIDDenyList } from 'features/nodes/util/graph/constants';
|
||||
import { imageInvocationComplete } from 'features/progress/store/progressSlice';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imagesAdapter } from 'services/api/util';
|
||||
@ -29,7 +30,7 @@ export const addInvocationCompleteEventListener = () => {
|
||||
// This complete event has an associated image output
|
||||
if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type) && !nodeIDDenyList.includes(source_node_id)) {
|
||||
const { image_name } = result.image;
|
||||
const { canvas, gallery } = getState();
|
||||
const { gallery, progress } = getState();
|
||||
|
||||
// This populates the `getImageDTO` cache
|
||||
const imageDTORequest = dispatch(
|
||||
@ -41,8 +42,10 @@ export const addInvocationCompleteEventListener = () => {
|
||||
const imageDTO = await imageDTORequest.unwrap();
|
||||
imageDTORequest.unsubscribe();
|
||||
|
||||
dispatch(imageInvocationComplete({ data, imageDTO }));
|
||||
|
||||
// Add canvas images to the staging area
|
||||
if (canvas.batchIds.includes(queue_batch_id) && [LINEAR_UI_OUTPUT].includes(data.source_node_id)) {
|
||||
if (progress.canvasBatchIds.includes(queue_batch_id) && [LINEAR_UI_OUTPUT].includes(data.source_node_id)) {
|
||||
dispatch(addImageToStagingArea(imageDTO));
|
||||
}
|
||||
|
||||
@ -102,7 +105,7 @@ export const addInvocationCompleteEventListener = () => {
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(imageSelected(imageDTO));
|
||||
dispatch(imageSelectionChanged(imageDTO));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import { nodesTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
|
||||
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
||||
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
||||
import { progressPersistConfig, progressSlice } from 'features/progress/store/progressSlice';
|
||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||
import { configSlice } from 'features/system/store/configSlice';
|
||||
@ -61,6 +62,7 @@ const allReducers = {
|
||||
[queueSlice.name]: queueSlice.reducer,
|
||||
[workflowSlice.name]: workflowSlice.reducer,
|
||||
[hrfSlice.name]: hrfSlice.reducer,
|
||||
[progressSlice.name]: progressSlice.reducer,
|
||||
[api.reducerPath]: api.reducer,
|
||||
};
|
||||
|
||||
@ -105,6 +107,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
||||
[loraPersistConfig.name]: loraPersistConfig,
|
||||
[modelManagerPersistConfig.name]: modelManagerPersistConfig,
|
||||
[hrfPersistConfig.name]: hrfPersistConfig,
|
||||
[progressPersistConfig.name]: progressPersistConfig,
|
||||
};
|
||||
|
||||
const unserialize: UnserializeFunction = (data, key) => {
|
||||
|
@ -37,17 +37,18 @@ type IAIDndImageProps = FlexProps & {
|
||||
isSelected?: boolean;
|
||||
thumbnail?: boolean;
|
||||
noContentFallback?: ReactElement;
|
||||
useThumbailFallback?: boolean;
|
||||
withHoverOverlay?: boolean;
|
||||
children?: JSX.Element;
|
||||
uploadElement?: ReactNode;
|
||||
dataTestId?: string;
|
||||
fallbackSrc?: string;
|
||||
};
|
||||
|
||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
const {
|
||||
imageDTO,
|
||||
onError,
|
||||
onLoad,
|
||||
onClick,
|
||||
withMetadataOverlay = false,
|
||||
isDropDisabled = false,
|
||||
@ -64,7 +65,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
thumbnail = false,
|
||||
noContentFallback = defaultNoContentFallback,
|
||||
uploadElement = defaultUploadElement,
|
||||
useThumbailFallback,
|
||||
fallbackSrc,
|
||||
withHoverOverlay = false,
|
||||
children,
|
||||
onMouseOver,
|
||||
@ -150,8 +151,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
<Image
|
||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
fallbackSrc={useThumbailFallback ? imageDTO.thumbnail_url : undefined}
|
||||
fallback={useThumbailFallback ? undefined : <IAILoadingImageFallback image={imageDTO} />}
|
||||
fallbackSrc={fallbackSrc ?? imageDTO.thumbnail_url}
|
||||
fallback={fallbackSrc ? undefined : <IAILoadingImageFallback image={imageDTO} />}
|
||||
onError={onError}
|
||||
draggable={false}
|
||||
w={imageDTO.width}
|
||||
@ -161,6 +162,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
borderRadius="base"
|
||||
sx={imageSx}
|
||||
data-testid={dataTestId}
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
|
||||
<SelectionOverlay isSelected={isSelected} isHovered={withHoverOverlay ? isHovered : false} />
|
||||
@ -175,11 +177,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
</>
|
||||
)}
|
||||
{!imageDTO && isUploadDisabled && noContentFallback}
|
||||
{imageDTO && !isDragDisabled && (
|
||||
{imageDTO && !isDragDisabled && draggableData && (
|
||||
<IAIDraggable data={draggableData} disabled={isDragDisabled || !imageDTO} onClick={onClick} />
|
||||
)}
|
||||
{children}
|
||||
{!isDropDisabled && <IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />}
|
||||
{!isDropDisabled && droppableData && (
|
||||
<IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</ImageContextMenu>
|
||||
|
@ -28,7 +28,6 @@ import { Layer, Stage } from 'react-konva';
|
||||
|
||||
import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay';
|
||||
import IAICanvasGrid from './IAICanvasGrid';
|
||||
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
|
||||
import IAICanvasMaskCompositer from './IAICanvasMaskCompositer';
|
||||
import IAICanvasMaskLines from './IAICanvasMaskLines';
|
||||
import IAICanvasObjectRenderer from './IAICanvasObjectRenderer';
|
||||
@ -55,7 +54,7 @@ const IAICanvas = () => {
|
||||
const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox);
|
||||
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
|
||||
const stageScale = useAppSelector((s) => s.canvas.stageScale);
|
||||
const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates);
|
||||
// const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates);
|
||||
const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias);
|
||||
const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox);
|
||||
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
|
||||
@ -184,7 +183,7 @@ const IAICanvas = () => {
|
||||
<Layer id="preview" imageSmoothingEnabled={shouldAntialias}>
|
||||
{!isStaging && <IAICanvasToolPreview visible={tool !== 'move'} listening={false} />}
|
||||
<IAICanvasStagingArea listening={false} visible={isStaging} />
|
||||
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
|
||||
{/* {shouldShowIntermediates && <IAICanvasIntermediateImage />} */}
|
||||
<IAICanvasBoundingBox visible={shouldShowBoundingBox && !isStaging} />
|
||||
</Layer>
|
||||
</ChakraStage>
|
||||
|
@ -1,20 +1,33 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { Image as KonvaImage } from 'react-konva';
|
||||
|
||||
const progressImageSelector = createMemoizedSelector([selectSystemSlice, selectCanvasSlice], (system, canvas) => {
|
||||
const { denoiseProgress } = system;
|
||||
const { batchIds } = canvas;
|
||||
export const progressImageSelector = createMemoizedSelector(
|
||||
[selectProgressSlice, selectCanvasSlice],
|
||||
(progress, canvas) => {
|
||||
const isLatestProgressFromCanvas =
|
||||
progress.latestDenoiseProgress && progress.canvasBatchIds.includes(progress.latestDenoiseProgress.queue_batch_id);
|
||||
|
||||
return {
|
||||
progressImage:
|
||||
denoiseProgress && batchIds.includes(denoiseProgress.batch_id) ? denoiseProgress.progress_image : undefined,
|
||||
boundingBox: canvas.layerState.stagingArea.boundingBox,
|
||||
};
|
||||
});
|
||||
const { selectedImageIndex, images } = canvas.layerState.stagingArea;
|
||||
const _currentStagingAreaImage =
|
||||
images.length > 0 && selectedImageIndex !== undefined ? images[selectedImageIndex] : undefined;
|
||||
|
||||
const isProgressImageIncomplete =
|
||||
progress.latestDenoiseProgress?.graph_execution_state_id !==
|
||||
progress.latestImageOutputEvent?.graph_execution_state_id;
|
||||
|
||||
return {
|
||||
progressImage:
|
||||
progress.latestDenoiseProgress && isLatestProgressFromCanvas && isProgressImageIncomplete
|
||||
? progress.latestDenoiseProgress.progress_image
|
||||
: undefined,
|
||||
boundingBox: canvas.layerState.stagingArea.boundingBox,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const IAICanvasIntermediateImage = () => {
|
||||
const { progressImage, boundingBox } = useAppSelector(progressImageSelector);
|
||||
|
@ -5,7 +5,7 @@ import type { GroupConfig } from 'konva/lib/Group';
|
||||
import { memo } from 'react';
|
||||
import { Group, Rect } from 'react-konva';
|
||||
|
||||
import IAICanvasImage from './IAICanvasImage';
|
||||
import { IAICanvasStagingAreaImage } from './IAICanvasStagingAreaImage';
|
||||
|
||||
const dash = [4, 4];
|
||||
|
||||
@ -37,12 +37,11 @@ const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
type Props = GroupConfig;
|
||||
|
||||
const IAICanvasStagingArea = (props: Props) => {
|
||||
const { currentStagingAreaImage, shouldShowStagingImage, shouldShowStagingOutline, x, y, width, height } =
|
||||
useAppSelector(selector);
|
||||
const { shouldShowStagingOutline, x, y, width, height } = useAppSelector(selector);
|
||||
|
||||
return (
|
||||
<Group {...props}>
|
||||
{shouldShowStagingImage && currentStagingAreaImage && <IAICanvasImage canvasImage={currentStagingAreaImage} />}
|
||||
<IAICanvasStagingAreaImage />
|
||||
{shouldShowStagingOutline && (
|
||||
<Group listening={false}>
|
||||
<Rect
|
||||
|
@ -0,0 +1,71 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAICanvasIntermediateImage, {
|
||||
progressImageSelector,
|
||||
} from 'features/canvas/components/IAICanvasIntermediateImage';
|
||||
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
|
||||
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||
import { memo } from 'react';
|
||||
import { Image } from 'react-konva';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import useImage from 'use-image';
|
||||
|
||||
import IAICanvasImageErrorFallback from './IAICanvasImageErrorFallback';
|
||||
|
||||
const selector = createMemoizedSelector([selectProgressSlice, selectCanvasSlice], (progress, canvas) => {
|
||||
const { selectedImageIndex, images } = canvas.layerState.stagingArea;
|
||||
|
||||
const currentStagingAreaImage =
|
||||
images.length > 0 && selectedImageIndex !== undefined ? images[selectedImageIndex] : undefined;
|
||||
|
||||
const progressImage =
|
||||
progress.latestDenoiseProgress && progress.canvasBatchIds.includes(progress.latestDenoiseProgress.queue_batch_id)
|
||||
? progress.latestDenoiseProgress.progress_image
|
||||
: undefined;
|
||||
|
||||
const boundingBox = canvas.layerState.stagingArea.boundingBox ?? {
|
||||
...canvas.boundingBoxCoordinates,
|
||||
...canvas.boundingBoxDimensions,
|
||||
};
|
||||
|
||||
return {
|
||||
currentStagingAreaImage,
|
||||
progressImage,
|
||||
boundingBox,
|
||||
};
|
||||
});
|
||||
|
||||
export const IAICanvasStagingAreaImage = memo(() => {
|
||||
const { currentStagingAreaImage, boundingBox } = useAppSelector(selector);
|
||||
const { progressImage } = useAppSelector(progressImageSelector);
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(currentStagingAreaImage?.imageName ?? skipToken);
|
||||
const [stagedImageEl, stagedImageElStatus] = useImage(
|
||||
imageDTO?.image_url ?? '',
|
||||
$authToken.get() ? 'use-credentials' : 'anonymous'
|
||||
);
|
||||
|
||||
if (currentStagingAreaImage && (isError || stagedImageElStatus === 'failed')) {
|
||||
return <IAICanvasImageErrorFallback canvasImage={currentStagingAreaImage} />;
|
||||
}
|
||||
|
||||
if (progressImage) {
|
||||
return <IAICanvasIntermediateImage />;
|
||||
}
|
||||
|
||||
if (stagedImageEl) {
|
||||
return (
|
||||
<Image
|
||||
x={boundingBox.x}
|
||||
y={boundingBox.y}
|
||||
width={boundingBox.width}
|
||||
height={boundingBox.height}
|
||||
image={stagedImageEl}
|
||||
listening={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
IAICanvasStagingAreaImage.displayName = 'IAICanvasStagingAreaImage';
|
@ -1,12 +1,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||
|
||||
import { selectCanvasSlice } from './canvasSlice';
|
||||
import { isCanvasBaseImage } from './canvasTypes';
|
||||
|
||||
export const isStagingSelector = createSelector(
|
||||
selectProgressSlice,
|
||||
selectCanvasSlice,
|
||||
(canvas) => canvas.batchIds.length > 0 || canvas.layerState.stagingArea.images.length > 0
|
||||
(progress, canvas) => progress.canvasBatchIds.length > 0 || canvas.layerState.stagingArea.images.length > 0
|
||||
);
|
||||
|
||||
export const initialCanvasImageSelector = createMemoizedSelector(selectCanvasSlice, (canvas) =>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import calculateCoordinates from 'features/canvas/util/calculateCoordinates';
|
||||
@ -15,9 +15,7 @@ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { clamp, cloneDeep } from 'lodash-es';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
||||
|
||||
import type {
|
||||
BoundingBoxScaleMethod,
|
||||
@ -79,7 +77,6 @@ export const initialCanvasState: CanvasState = {
|
||||
stageCoordinates: { x: 0, y: 0 },
|
||||
stageDimensions: { width: 0, height: 0 },
|
||||
stageScale: 1,
|
||||
batchIds: [],
|
||||
aspectRatio: {
|
||||
id: '1:1',
|
||||
value: 1,
|
||||
@ -180,7 +177,6 @@ export const canvasSlice = createSlice({
|
||||
],
|
||||
};
|
||||
state.futureLayerStates = [];
|
||||
state.batchIds = [];
|
||||
|
||||
const newScale = calculateScale(
|
||||
stageDimensions.width,
|
||||
@ -237,12 +233,6 @@ export const canvasSlice = createSlice({
|
||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowBoundingBox = action.payload;
|
||||
},
|
||||
canvasBatchIdAdded: (state, action: PayloadAction<string>) => {
|
||||
state.batchIds.push(action.payload);
|
||||
},
|
||||
canvasBatchIdsReset: (state) => {
|
||||
state.batchIds = [];
|
||||
},
|
||||
stagingAreaInitialized: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
@ -293,7 +283,6 @@ export const canvasSlice = createSlice({
|
||||
state.futureLayerStates = [];
|
||||
state.shouldShowStagingOutline = true;
|
||||
state.shouldShowStagingImage = true;
|
||||
state.batchIds = [];
|
||||
},
|
||||
addFillRect: (state) => {
|
||||
const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state;
|
||||
@ -426,7 +415,6 @@ export const canvasSlice = createSlice({
|
||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||
state.layerState = cloneDeep(initialLayerState);
|
||||
state.futureLayerStates = [];
|
||||
state.batchIds = [];
|
||||
state.boundingBoxCoordinates = {
|
||||
...initialCanvasState.boundingBoxCoordinates,
|
||||
};
|
||||
@ -536,7 +524,6 @@ export const canvasSlice = createSlice({
|
||||
state.futureLayerStates = [];
|
||||
state.shouldShowStagingOutline = true;
|
||||
state.shouldShowStagingImage = true;
|
||||
state.batchIds = [];
|
||||
},
|
||||
setBoundingBoxScaleMethod: {
|
||||
reducer: (state, action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>) => {
|
||||
@ -644,23 +631,6 @@ export const canvasSlice = createSlice({
|
||||
optimalDimension
|
||||
);
|
||||
});
|
||||
|
||||
builder.addCase(socketQueueItemStatusChanged, (state, action) => {
|
||||
const batch_status = action.payload.data.batch_status;
|
||||
if (!state.batchIds.includes(batch_status.batch_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (batch_status.in_progress === 0 && batch_status.pending === 0) {
|
||||
state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id);
|
||||
}
|
||||
});
|
||||
builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => {
|
||||
state.batchIds = [];
|
||||
});
|
||||
builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => {
|
||||
state.batchIds = state.batchIds.filter((id) => !action.meta.arg.originalArgs.batch_ids.includes(id));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -713,8 +683,6 @@ export const {
|
||||
stagingAreaInitialized,
|
||||
setShouldAntialias,
|
||||
canvasResized,
|
||||
canvasBatchIdAdded,
|
||||
canvasBatchIdsReset,
|
||||
aspectRatioChanged,
|
||||
scaledBoundingBoxDimensionsReset,
|
||||
} = canvasSlice.actions;
|
||||
@ -736,3 +704,5 @@ export const canvasPersistConfig: PersistConfig<CanvasState> = {
|
||||
migrate: migrateCanvasState,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const matchAnyStagingAreaDismissed = isAnyOf(commitStagingAreaImage, discardStagedImages);
|
||||
|
@ -142,7 +142,6 @@ export interface CanvasState {
|
||||
stageDimensions: Dimensions;
|
||||
stageScale: number;
|
||||
generationMode?: GenerationMode;
|
||||
batchIds: string[];
|
||||
aspectRatio: AspectRatioState;
|
||||
}
|
||||
|
||||
|
@ -12,14 +12,19 @@ import type {
|
||||
} from '@dnd-kit/core';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import type { FieldInputInstance, FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { ViewerMode } from 'features/ui/store/uiTypes';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
type BaseDropData = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type CurrentImageDropData = BaseDropData & {
|
||||
actionType: 'SET_CURRENT_IMAGE';
|
||||
export type ViewerImageDropData = BaseDropData & {
|
||||
actionType: 'SET_VIEWER_IMAGE';
|
||||
context: {
|
||||
viewerMode: ViewerMode;
|
||||
currentImageName?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type InitialImageDropData = BaseDropData & {
|
||||
@ -59,13 +64,13 @@ export type RemoveFromBoardDropData = BaseDropData & {
|
||||
};
|
||||
|
||||
export type TypesafeDroppableData =
|
||||
| CurrentImageDropData
|
||||
| InitialImageDropData
|
||||
| ControlAdapterDropData
|
||||
| CanvasInitialImageDropData
|
||||
| NodesImageDropData
|
||||
| AddToBoardDropData
|
||||
| RemoveFromBoardDropData;
|
||||
| RemoveFromBoardDropData
|
||||
| ViewerImageDropData;
|
||||
|
||||
type BaseDragData = {
|
||||
id: string;
|
||||
|
@ -13,8 +13,11 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
|
||||
}
|
||||
|
||||
switch (actionType) {
|
||||
case 'SET_CURRENT_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_VIEWER_IMAGE':
|
||||
return (
|
||||
payloadType === 'IMAGE_DTO' &&
|
||||
overData.context.currentImageName !== active.data.current.payload.imageDTO.image_name
|
||||
);
|
||||
case 'SET_INITIAL_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_CONTROL_ADAPTER_IMAGE':
|
||||
|
@ -1,295 +0,0 @@
|
||||
import { ButtonGroup, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { setShouldShowImageDetails, setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiDotsThreeOutlineFill,
|
||||
PiFlowArrowBold,
|
||||
PiHourglassHighBold,
|
||||
PiInfoBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
const selectShouldDisableToolbarButtons = createSelector(
|
||||
selectSystemSlice,
|
||||
selectGallerySlice,
|
||||
selectLastSelectedImage,
|
||||
(system, gallery, lastSelectedImage) => {
|
||||
const hasProgressImage = Boolean(system.denoiseProgress?.progress_image);
|
||||
return hasProgressImage || !lastSelectedImage;
|
||||
}
|
||||
);
|
||||
|
||||
const CurrentImageButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
|
||||
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { recallBothPrompts, recallSeed, recallWidthAndHeight, recallAllParameters } = useRecallParameters();
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(lastSelectedImage?.image_name);
|
||||
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!lastSelectedImage || !lastSelectedImage.has_workflow) {
|
||||
return;
|
||||
}
|
||||
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
|
||||
|
||||
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
|
||||
|
||||
const handleClickUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(metadata);
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('a', handleClickUseAllParameters, [metadata]);
|
||||
|
||||
const handleUseSeed = useCallback(() => {
|
||||
recallSeed(metadata?.seed);
|
||||
}, [metadata?.seed, recallSeed]);
|
||||
|
||||
useHotkeys('s', handleUseSeed, [metadata]);
|
||||
|
||||
const handleUsePrompt = useCallback(() => {
|
||||
recallBothPrompts(
|
||||
metadata?.positive_prompt,
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt
|
||||
);
|
||||
}, [
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt,
|
||||
recallBothPrompts,
|
||||
]);
|
||||
|
||||
useHotkeys('p', handleUsePrompt, [metadata]);
|
||||
|
||||
const handleRemixImage = useCallback(() => {
|
||||
// Recalls all metadata parameters except seed
|
||||
recallAllParameters({
|
||||
...metadata,
|
||||
seed: undefined,
|
||||
});
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('r', handleRemixImage, [metadata]);
|
||||
|
||||
const handleUseSize = useCallback(() => {
|
||||
recallWidthAndHeight(metadata?.width, metadata?.height);
|
||||
}, [metadata?.width, metadata?.height, recallWidthAndHeight]);
|
||||
|
||||
useHotkeys('d', handleUseSize, [metadata]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(initialImageSelected(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||
|
||||
const handleClickUpscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(upscaleRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys(
|
||||
'Shift+U',
|
||||
() => {
|
||||
handleClickUpscale();
|
||||
},
|
||||
{
|
||||
enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected),
|
||||
},
|
||||
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
|
||||
);
|
||||
|
||||
const handleClickShowImageDetails = useCallback(
|
||||
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
|
||||
[dispatch, shouldShowImageDetails]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'i',
|
||||
() => {
|
||||
if (imageDTO) {
|
||||
handleClickShowImageDetails();
|
||||
} else {
|
||||
toaster({
|
||||
title: t('toast.metadataLoadFailed'),
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[imageDTO, shouldShowImageDetails, toaster]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'delete',
|
||||
() => {
|
||||
handleDelete();
|
||||
},
|
||||
[dispatch, imageDTO]
|
||||
);
|
||||
|
||||
const handleClickProgressImagesToggle = useCallback(() => {
|
||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||
}, [dispatch, shouldShowProgressInViewer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
|
||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label={t('parameters.imageActions')}
|
||||
tooltip={t('parameters.imageActions')}
|
||||
isDisabled={!imageDTO}
|
||||
icon={<PiDotsThreeOutlineFill />}
|
||||
/>
|
||||
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
|
||||
</Menu>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<IconButton
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!imageDTO?.has_workflow}
|
||||
onClick={handleLoadWorkflow}
|
||||
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={!metadata?.positive_prompt}
|
||||
onClick={handleRemixImage}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={!metadata?.positive_prompt}
|
||||
onClick={handleUsePrompt}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={metadata?.seed === null || metadata?.seed === undefined}
|
||||
onClick={handleUseSeed}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
isDisabled={
|
||||
metadata?.height === null ||
|
||||
metadata?.height === undefined ||
|
||||
metadata?.width === null ||
|
||||
metadata?.width === undefined
|
||||
}
|
||||
onClick={handleUseSize}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={!metadata}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isUpscalingEnabled && (
|
||||
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
||||
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiInfoBold />}
|
||||
tooltip={`${t('parameters.info')} (I)`}
|
||||
aria-label={`${t('parameters.info')} (I)`}
|
||||
isChecked={shouldShowImageDetails}
|
||||
onClick={handleClickShowImageDetails}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
aria-label={t('settings.displayInProgress')}
|
||||
tooltip={t('settings.displayInProgress')}
|
||||
icon={<PiHourglassHighBold />}
|
||||
isChecked={shouldShowProgressInViewer}
|
||||
onClick={handleClickProgressImagesToggle}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup>
|
||||
<DeleteImageButton onClick={handleDelete} />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CurrentImageButtons);
|
@ -1,24 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
|
||||
const CurrentImageDisplay = () => {
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
width="100%"
|
||||
rowGap={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<CurrentImageButtons />
|
||||
<CurrentImagePreview />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CurrentImageDisplay);
|
@ -1,129 +0,0 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import ProgressImage from 'features/gallery/components/CurrentImage/ProgressImage';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const selectLastSelectedImageName = createSelector(
|
||||
selectLastSelectedImage,
|
||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||
);
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||
const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress));
|
||||
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
|
||||
|
||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||
if (imageDTO) {
|
||||
return {
|
||||
id: 'current-image',
|
||||
payloadType: 'IMAGE_DTO',
|
||||
payload: { imageDTO },
|
||||
};
|
||||
}
|
||||
}, [imageDTO]);
|
||||
|
||||
const droppableData = useMemo<TypesafeDroppableData | undefined>(
|
||||
() => ({
|
||||
id: 'current-image',
|
||||
actionType: 'SET_CURRENT_IMAGE',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// Show and hide the next/prev buttons on mouse move
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
|
||||
|
||||
const timeoutId = useRef(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setShouldShowNextPrevButtons(true);
|
||||
window.clearTimeout(timeoutId.current);
|
||||
}, []);
|
||||
|
||||
const handleMouseOut = useCallback(() => {
|
||||
timeoutId.current = window.setTimeout(() => {
|
||||
setShouldShowNextPrevButtons(false);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
width="full"
|
||||
height="full"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
>
|
||||
{hasDenoiseProgress && shouldShowProgressInViewer ? (
|
||||
<ProgressImage />
|
||||
) : (
|
||||
<IAIDndImage
|
||||
imageDTO={imageDTO}
|
||||
droppableData={droppableData}
|
||||
draggableData={draggableData}
|
||||
isUploadDisabled={true}
|
||||
fitContainer
|
||||
useThumbailFallback
|
||||
dropLabel={t('gallery.setCurrentImage')}
|
||||
noContentFallback={<IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />}
|
||||
dataTestId="image-preview"
|
||||
/>
|
||||
)}
|
||||
{shouldShowImageDetails && imageDTO && (
|
||||
<Box position="absolute" top="0" width="full" height="full" borderRadius="base">
|
||||
<ImageMetadataViewer image={imageDTO} />
|
||||
</Box>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && (
|
||||
<motion.div key="nextPrevButtons" initial={initial} animate={animate} exit={exit} style={motionStyles}>
|
||||
<NextPrevImageButtons />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CurrentImagePreview);
|
||||
|
||||
const initial: AnimationProps['initial'] = {
|
||||
opacity: 0,
|
||||
};
|
||||
const animate: AnimationProps['animate'] = {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.1 },
|
||||
};
|
||||
const exit: AnimationProps['exit'] = {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1 },
|
||||
};
|
||||
const motionStyles: CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Image } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const progress_image = useAppSelector((s) => s.system.denoiseProgress?.progress_image);
|
||||
const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage);
|
||||
|
||||
const sx = useMemo<SystemStyleObject>(
|
||||
() => ({
|
||||
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
|
||||
}),
|
||||
[shouldAntialiasProgressImage]
|
||||
);
|
||||
|
||||
if (!progress_image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={progress_image.dataURL}
|
||||
width={progress_image.width}
|
||||
height={progress_image.height}
|
||||
draggable={false}
|
||||
data-testid="progress-image"
|
||||
objectFit="contain"
|
||||
maxWidth="full"
|
||||
maxHeight="full"
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
sx={sx}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CurrentImagePreview);
|
@ -28,12 +28,11 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
padding={4}
|
||||
gap={1}
|
||||
gap={4}
|
||||
flexDirection="column"
|
||||
width="full"
|
||||
height="full"
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
overflow="hidden"
|
||||
>
|
||||
<ExternalLink href={image.image_url} label={image.image_name} />
|
||||
|
@ -5,7 +5,7 @@ import { imageListContainerTestId } from 'features/gallery/components/ImageGrid/
|
||||
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { getIsVisible } from 'features/gallery/util/getIsVisible';
|
||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||
import { clamp } from 'lodash-es';
|
||||
@ -144,7 +144,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
||||
if (!image || index === lastSelectedImageIndex) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageSelected(image));
|
||||
dispatch(imageSelectionChanged(image));
|
||||
scrollToImage(image.image_name, index);
|
||||
},
|
||||
[dispatch, lastSelectedImageIndex, data]
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { galleryImageClicked, imageSelectionChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@ -26,7 +25,7 @@ export const useMultiselect = (imageDTO?: ImageDTO) => {
|
||||
return;
|
||||
}
|
||||
if (!isMultiSelectEnabled) {
|
||||
dispatch(selectionChanged([imageDTO]));
|
||||
dispatch(imageSelectionChanged(imageDTO));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import { createAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
@ -26,11 +26,16 @@ export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState: initialGalleryState,
|
||||
reducers: {
|
||||
imageSelected: (state, action: PayloadAction<ImageDTO | null>) => {
|
||||
state.selection = action.payload ? [action.payload] : [];
|
||||
},
|
||||
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
||||
imageSelectionChanged: (state, action: PayloadAction<ImageDTO[] | ImageDTO | null>) => {
|
||||
if (!action.payload) {
|
||||
state.selection = [];
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(action.payload)) {
|
||||
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
||||
return;
|
||||
}
|
||||
state.selection = [action.payload];
|
||||
},
|
||||
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldAutoSwitch = action.payload;
|
||||
@ -97,14 +102,13 @@ export const gallerySlice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
imageSelected,
|
||||
shouldAutoSwitchChanged,
|
||||
autoAssignBoardOnClickChanged,
|
||||
setGalleryImageMinimumWidth,
|
||||
boardIdSelected,
|
||||
autoAddBoardIdChanged,
|
||||
galleryViewChanged,
|
||||
selectionChanged,
|
||||
imageSelectionChanged,
|
||||
boardSearchTextChanged,
|
||||
moreImagesLoaded,
|
||||
} = gallerySlice.actions;
|
||||
@ -130,3 +134,10 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
|
||||
migrate: migrateGalleryState,
|
||||
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit'],
|
||||
};
|
||||
|
||||
export const galleryImageClicked = createAction<{
|
||||
imageDTO: ImageDTO;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
}>(`${gallerySlice.name}/galleryImageClicked`);
|
||||
|
@ -38,7 +38,7 @@ const CurrentImageNode = (props: NodeProps) => {
|
||||
if (imageDTO) {
|
||||
return (
|
||||
<Wrapper nodeProps={props}>
|
||||
<IAIDndImage imageDTO={imageDTO} isDragDisabled useThumbailFallback />
|
||||
<IAIDndImage imageDTO={imageDTO} isDragDisabled fallbackSrc={imageDTO?.thumbnail_url} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
droppableData={droppableData}
|
||||
draggableData={draggableData}
|
||||
postUploadAction={postUploadAction}
|
||||
useThumbailFallback
|
||||
fallbackSrc={imageDTO?.thumbnail_url}
|
||||
uploadElement={<UploadElement />}
|
||||
dropLabel={<DropLabel />}
|
||||
minSize={8}
|
||||
|
13
invokeai/frontend/web/src/features/progress/README.md
Normal file
13
invokeai/frontend/web/src/features/progress/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Progress
|
||||
|
||||
We have 3 different places to display progress images:
|
||||
|
||||
- TextToImage & ImageToImage
|
||||
- Canvas
|
||||
- Workflow
|
||||
|
||||
The progress slice tracks the latest denoising progress events, latest image output, and active batch ids for each of the workspaces.
|
||||
|
||||
Each of these have different requirements for displaying progress images, but much of the logic around tracking progress is the same, so it is consolidated here.
|
||||
|
||||
It also holds the latest progress event separately, which is used for the progress bar.
|
@ -0,0 +1,29 @@
|
||||
import { Progress } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsProcessing } from 'features/queue/hooks/useIsProcessing';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ProgressBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||
const isProcessing = useIsProcessing();
|
||||
const hasSteps = useAppSelector((s) => isProcessing && Boolean(s.progress.currentDenoiseProgress));
|
||||
const value = useAppSelector((s) => (isProcessing ? (s.progress.currentDenoiseProgress?.percentage ?? 0) * 100 : 0));
|
||||
const isIndeterminate = useMemo(() => {
|
||||
return isConnected && isProcessing && !hasSteps;
|
||||
}, [hasSteps, isConnected, isProcessing]);
|
||||
|
||||
return (
|
||||
<Progress
|
||||
value={value}
|
||||
aria-label={t('accessibility.invokeProgressBar')}
|
||||
isIndeterminate={isIndeterminate}
|
||||
h={2}
|
||||
w="full"
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ProgressBar);
|
@ -0,0 +1,147 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import {
|
||||
commitStagingAreaImage,
|
||||
discardStagedImages,
|
||||
resetCanvas,
|
||||
setInitialCanvasImage,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import type { DenoiseProgress } from 'features/progress/store/types';
|
||||
import { calculateStepPercentage } from 'features/system/util/calculateStepPercentage';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import {
|
||||
socketConnected,
|
||||
socketDisconnected,
|
||||
socketGeneratorProgress,
|
||||
socketInvocationComplete,
|
||||
socketQueueItemStatusChanged,
|
||||
} from 'services/events/actions';
|
||||
import type { GeneratorProgressEvent, InvocationCompleteEvent } from 'services/events/types';
|
||||
|
||||
export type ProgressTab = 'linear' | 'canvas' | 'workflow';
|
||||
|
||||
export type LatestImageOutputEvent = InvocationCompleteEvent & {
|
||||
image_name: string;
|
||||
};
|
||||
|
||||
export type ProgressState = {
|
||||
_version: 1;
|
||||
/**
|
||||
* The current denoise progress. Only has a value if denoising is in progress.
|
||||
*/
|
||||
currentDenoiseProgress: DenoiseProgress | null;
|
||||
/**
|
||||
* The latest denoise progress, regardless of tab.
|
||||
*/
|
||||
latestDenoiseProgress: DenoiseProgress | null;
|
||||
/**
|
||||
* The latest image output event.
|
||||
*/
|
||||
latestImageOutputEvent: LatestImageOutputEvent | null;
|
||||
/**
|
||||
* Batch ids for batches initiated on the canvas tab.
|
||||
*/
|
||||
canvasBatchIds: string[];
|
||||
};
|
||||
|
||||
export const initialProgressState: ProgressState = {
|
||||
_version: 1,
|
||||
currentDenoiseProgress: null,
|
||||
latestDenoiseProgress: null,
|
||||
latestImageOutputEvent: null,
|
||||
canvasBatchIds: [],
|
||||
};
|
||||
|
||||
export const progressSlice = createSlice({
|
||||
name: 'progress',
|
||||
initialState: initialProgressState,
|
||||
reducers: {
|
||||
canvasBatchEnqueued: (state, action: PayloadAction<string>) => {
|
||||
state.canvasBatchIds.push(action.payload);
|
||||
},
|
||||
imageInvocationComplete: (state, action: PayloadAction<{ data: InvocationCompleteEvent; imageDTO: ImageDTO }>) => {
|
||||
const { data, imageDTO } = action.payload;
|
||||
state.latestImageOutputEvent = { ...data, image_name: imageDTO.image_name };
|
||||
},
|
||||
latestImageLoaded: () => {
|
||||
// state.latestDenoiseProgress = null;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(socketConnected, (state) => {
|
||||
state.latestDenoiseProgress = null;
|
||||
state.latestImageOutputEvent = null;
|
||||
});
|
||||
|
||||
builder.addCase(socketDisconnected, (state) => {
|
||||
state.latestDenoiseProgress = null;
|
||||
state.latestImageOutputEvent = null;
|
||||
});
|
||||
|
||||
builder.addCase(socketGeneratorProgress, (state, action) => {
|
||||
const denoiseProgress = buildDenoiseProgress(action.payload.data);
|
||||
state.latestDenoiseProgress = denoiseProgress;
|
||||
state.currentDenoiseProgress = denoiseProgress;
|
||||
});
|
||||
|
||||
builder.addCase(socketInvocationComplete, (state) => {
|
||||
state.currentDenoiseProgress = null;
|
||||
});
|
||||
|
||||
builder.addCase(socketQueueItemStatusChanged, (state, action) => {
|
||||
state.currentDenoiseProgress = null;
|
||||
const { status } = action.payload.data.queue_item;
|
||||
if (status === 'canceled' || status === 'failed') {
|
||||
state.latestDenoiseProgress = null;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addMatcher(
|
||||
isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage),
|
||||
(state) => {
|
||||
state.canvasBatchIds = [];
|
||||
}
|
||||
);
|
||||
|
||||
builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => {
|
||||
// When the queue is cleared, all progress is cleared
|
||||
state.latestDenoiseProgress = null;
|
||||
});
|
||||
|
||||
builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => {
|
||||
// When a batch is canceled, remove it from the list of batch ids and clear its progress if it is stored.
|
||||
|
||||
const canceled_batch_ids = action.meta.arg.originalArgs.batch_ids;
|
||||
|
||||
if (state.latestDenoiseProgress && canceled_batch_ids.includes(state.latestDenoiseProgress.queue_batch_id)) {
|
||||
state.latestDenoiseProgress = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { imageInvocationComplete, canvasBatchEnqueued, latestImageLoaded } = progressSlice.actions;
|
||||
|
||||
export const selectProgressSlice = (state: RootState) => state.progress;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateProgressState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const buildDenoiseProgress = (data: GeneratorProgressEvent): DenoiseProgress => ({
|
||||
...data,
|
||||
percentage: calculateStepPercentage(data.step, data.total_steps, data.order),
|
||||
});
|
||||
|
||||
export const progressPersistConfig: PersistConfig<ProgressState> = {
|
||||
name: progressSlice.name,
|
||||
initialState: initialProgressState,
|
||||
migrate: migrateProgressState,
|
||||
persistDenylist: ['currentDenoiseProgress', 'latestDenoiseProgress', 'latestImageOutputEvent'],
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import type { GeneratorProgressEvent } from 'services/events/types';
|
||||
|
||||
export type SystemStatus = 'CONNECTED' | 'DISCONNECTED' | 'PROCESSING' | 'ERROR' | 'LOADING_MODEL';
|
||||
|
||||
export type DenoiseProgress = GeneratorProgressEvent & {
|
||||
percentage: number;
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import ProgressBar from 'features/progress/components/ProgressBar';
|
||||
import ClearQueueIconButton from 'features/queue/components/ClearQueueIconButton';
|
||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
export const useIsProcessing = () => {
|
||||
const { currentData } = useGetQueueStatusQuery();
|
||||
const isProcessing = useMemo(() => {
|
||||
if ((currentData?.processor.is_processing && currentData?.queue.pending) || currentData?.queue.in_progress) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [currentData?.processor.is_processing, currentData?.queue.in_progress, currentData?.queue.pending]);
|
||||
return isProcessing;
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { Progress } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
const selectProgressValue = createSelector(
|
||||
selectSystemSlice,
|
||||
(system) => (system.denoiseProgress?.percentage ?? 0) * 100
|
||||
);
|
||||
|
||||
const ProgressBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||
const hasSteps = useAppSelector((s) => Boolean(s.system.denoiseProgress));
|
||||
const value = useAppSelector(selectProgressValue);
|
||||
|
||||
return (
|
||||
<Progress
|
||||
value={value}
|
||||
aria-label={t('accessibility.invokeProgressBar')}
|
||||
isIndeterminate={isConnected && Boolean(queueStatus?.queue.in_progress) && !hasSteps}
|
||||
h={2}
|
||||
w="full"
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ProgressBar);
|
@ -4,13 +4,13 @@ import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCu
|
||||
import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { ClearAllQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
||||
import { useIsProcessing } from 'features/queue/hooks/useIsProcessing';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCircleNotchBold, PiSlidersHorizontalBold } from 'react-icons/pi';
|
||||
import { RiSparklingFill } from 'react-icons/ri';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
const floatingButtonStyles: SystemStyleObject = {
|
||||
borderStartRadius: 0,
|
||||
@ -24,16 +24,16 @@ type Props = {
|
||||
const FloatingSidePanelButtons = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const isProcessing = useIsProcessing();
|
||||
|
||||
const queueButtonIcon = useMemo(
|
||||
() =>
|
||||
!isDisabled && queueStatus?.processor.is_processing ? (
|
||||
!isDisabled && isProcessing ? (
|
||||
<Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />
|
||||
) : (
|
||||
<RiSparklingFill size="16px" />
|
||||
),
|
||||
[isDisabled, queueStatus?.processor.is_processing]
|
||||
[isDisabled, isProcessing]
|
||||
);
|
||||
|
||||
const disclosure = useDisclosure();
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { Viewer } from 'features/viewer/components/Viewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
const TextToImageTab = () => {
|
||||
return (
|
||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||
<Flex w="full" h="full">
|
||||
<CurrentImageDisplay />
|
||||
</Flex>
|
||||
<Viewer />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,11 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { galleryImageClicked } from 'features/gallery/store/gallerySlice';
|
||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
|
||||
import type { InvokeTabName } from './tabMap';
|
||||
import type { UIState } from './uiTypes';
|
||||
import type { UIState, ViewerMode } from './uiTypes';
|
||||
|
||||
export const initialUIState: UIState = {
|
||||
_version: 1,
|
||||
@ -14,6 +15,7 @@ export const initialUIState: UIState = {
|
||||
panels: {},
|
||||
accordions: {},
|
||||
expanders: {},
|
||||
viewerMode: 'image',
|
||||
};
|
||||
|
||||
export const uiSlice = createSlice({
|
||||
@ -40,11 +42,22 @@ export const uiSlice = createSlice({
|
||||
const { id, isOpen } = action.payload;
|
||||
state.expanders[id] = isOpen;
|
||||
},
|
||||
viewerModeChanged: (state, action: PayloadAction<ViewerMode>) => {
|
||||
state.viewerMode = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(initialImageChanged, (state) => {
|
||||
state.activeTab = 'img2img';
|
||||
});
|
||||
|
||||
builder.addCase(galleryImageClicked, (state) => {
|
||||
// When a gallery image is clicked and we are in progress mode, switch to image mode.
|
||||
// This is not the same as the gallery _selection_ being changed.
|
||||
if (state.viewerMode === 'progress') {
|
||||
state.viewerMode = 'image';
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -55,6 +68,7 @@ export const {
|
||||
panelsChanged,
|
||||
accordionStateChanged,
|
||||
expanderStateChanged,
|
||||
viewerModeChanged,
|
||||
} = uiSlice.actions;
|
||||
|
||||
export const selectUiSlice = (state: RootState) => state.ui;
|
||||
|
@ -29,4 +29,10 @@ export interface UIState {
|
||||
* The state of expanders. The key is the id of the expander, and the value is a boolean representing the open state.
|
||||
*/
|
||||
expanders: Record<string, boolean>;
|
||||
/**
|
||||
* The currently-selected viewer mode.
|
||||
*/
|
||||
viewerMode: ViewerMode;
|
||||
}
|
||||
|
||||
export type ViewerMode = 'image' | 'info' | 'progress';
|
||||
|
9
invokeai/frontend/web/src/features/viewer/README.md
Normal file
9
invokeai/frontend/web/src/features/viewer/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Viewer
|
||||
|
||||
The viewer is the main panel in the `TextToImage` and `ImageToImage` tabs.
|
||||
|
||||
It has 3 modes:
|
||||
|
||||
- Image: Displays the currently selected image
|
||||
- Info: Displays info for the currently selected image
|
||||
- Progress: Displays generation progress, and eventually the image associated with that progress (if there was one)
|
@ -0,0 +1,26 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ViewerDroppable } from 'features/viewer/components/ViewerDroppable';
|
||||
import { ViewerImage } from 'features/viewer/components/ViewerImage';
|
||||
import { ViewerInfo } from 'features/viewer/components/ViewerInfo';
|
||||
import { ViewerProgress } from 'features/viewer/components/ViewerProgress';
|
||||
import { ViewerToolbar } from 'features/viewer/components/ViewerToolbar';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const Viewer = memo(() => {
|
||||
const viewerMode = useAppSelector((s) => s.ui.viewerMode);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDirection="column" height="full" width="full" gap={4}>
|
||||
<ViewerToolbar />
|
||||
<Flex height="full" width="full" alignItems="center" justifyContent="center">
|
||||
{viewerMode === 'image' && <ViewerImage />}
|
||||
{viewerMode === 'info' && <ViewerInfo />}
|
||||
{viewerMode === 'progress' && <ViewerProgress />}
|
||||
</Flex>
|
||||
<ViewerDroppable />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
Viewer.displayName = 'Viewer';
|
@ -0,0 +1,33 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type { ViewerImageDropData } from 'features/dnd/types';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectLastSelectedImageName = createSelector(
|
||||
selectLastSelectedImage,
|
||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||
);
|
||||
|
||||
export const ViewerDroppable = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const currentImageName = useAppSelector(selectLastSelectedImageName);
|
||||
const viewerMode = useAppSelector((s) => s.ui.viewerMode);
|
||||
const viewerDropData = useMemo<ViewerImageDropData>(
|
||||
() => ({
|
||||
id: 'viewer-image',
|
||||
actionType: 'SET_VIEWER_IMAGE',
|
||||
context: {
|
||||
currentImageName,
|
||||
viewerMode,
|
||||
},
|
||||
}),
|
||||
[currentImageName, viewerMode]
|
||||
);
|
||||
|
||||
return <IAIDroppable data={viewerDropData} dropLabel={t('viewer.dropLabel')} />;
|
||||
});
|
||||
|
||||
ViewerDroppable.displayName = 'ViewerDroppable';
|
@ -0,0 +1,48 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const selectLastSelectedImageName = createSelector(
|
||||
selectLastSelectedImage,
|
||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||
);
|
||||
|
||||
export const ViewerImage = memo(() => {
|
||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
|
||||
|
||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||
if (imageDTO) {
|
||||
return {
|
||||
id: 'current-image',
|
||||
payloadType: 'IMAGE_DTO',
|
||||
payload: { imageDTO },
|
||||
};
|
||||
}
|
||||
}, [imageDTO]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IAIDndImage
|
||||
imageDTO={imageDTO}
|
||||
draggableData={draggableData}
|
||||
isUploadDisabled={true}
|
||||
fitContainer
|
||||
fallbackSrc={imageDTO?.thumbnail_url}
|
||||
noContentFallback={<IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />}
|
||||
dataTestId="image-preview"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerImage.displayName = 'ViewerImage';
|
@ -0,0 +1,29 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const selectLastSelectedImageName = createSelector(
|
||||
selectLastSelectedImage,
|
||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||
);
|
||||
|
||||
export const ViewerInfo = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
|
||||
|
||||
if (!imageDTO) {
|
||||
return <IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />;
|
||||
}
|
||||
|
||||
return <ImageMetadataViewer image={imageDTO} />;
|
||||
});
|
||||
|
||||
ViewerInfo.displayName = 'ViewerInfo';
|
@ -0,0 +1,36 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { ViewerProgressLatestImage } from 'features/viewer/components/ViewerProgressLatestImage';
|
||||
import { ViewerProgressLinearDenoiseProgress } from 'features/viewer/components/ViewerProgressLinearDenoiseProgress';
|
||||
import { useLatestImageDTO } from 'features/viewer/hooks/useLatestImageDTO';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiHourglassBold } from 'react-icons/pi';
|
||||
|
||||
export const ViewerProgress = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const latestDenoiseProgress = useAppSelector((s) => {
|
||||
if (!s.progress.latestDenoiseProgress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (s.progress.canvasBatchIds.includes(s.progress.latestDenoiseProgress.queue_batch_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return s.progress.latestDenoiseProgress;
|
||||
});
|
||||
const latestImageDTO = useLatestImageDTO();
|
||||
|
||||
if (latestImageDTO) {
|
||||
return <ViewerProgressLatestImage imageDTO={latestImageDTO} />;
|
||||
}
|
||||
|
||||
if (latestDenoiseProgress?.progress_image) {
|
||||
return <ViewerProgressLinearDenoiseProgress progressImage={latestDenoiseProgress.progress_image} />;
|
||||
}
|
||||
|
||||
return <IAINoContentFallback icon={PiHourglassBold} label={t('viewer.noProgress')} />;
|
||||
});
|
||||
|
||||
ViewerProgress.displayName = 'ViewerProgress';
|
@ -0,0 +1,44 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import { latestImageLoaded } from 'features/progress/store/progressSlice';
|
||||
import { useProgressImageRenderingStyles } from 'features/viewer/hooks/useProgressImageRenderingStyles';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
|
||||
export const ViewerProgressLatestImage = memo(({ imageDTO }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const progressImageDataURL = useAppSelector((s) => s.progress.latestDenoiseProgress?.progress_image?.dataURL);
|
||||
const sx = useProgressImageRenderingStyles();
|
||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(
|
||||
() => ({
|
||||
id: 'current-image',
|
||||
payloadType: 'IMAGE_DTO',
|
||||
payload: { imageDTO },
|
||||
}),
|
||||
[imageDTO]
|
||||
);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
dispatch(latestImageLoaded());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<IAIDndImage
|
||||
imageDTO={imageDTO}
|
||||
draggableData={draggableData}
|
||||
isUploadDisabled={true}
|
||||
fitContainer
|
||||
fallbackSrc={progressImageDataURL}
|
||||
dataTestId="progress-latest-image"
|
||||
onLoad={onLoad}
|
||||
sx={sx}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerProgressLatestImage.displayName = 'ViewerProgressLatestImage';
|
@ -0,0 +1,30 @@
|
||||
import { Image } from '@invoke-ai/ui-library';
|
||||
import { useProgressImageRenderingStyles } from 'features/viewer/hooks/useProgressImageRenderingStyles';
|
||||
import { memo } from 'react';
|
||||
import type { ProgressImage } from 'services/events/types';
|
||||
|
||||
type Props = {
|
||||
progressImage: ProgressImage;
|
||||
};
|
||||
|
||||
export const ViewerProgressLinearDenoiseProgress = memo(({ progressImage }: Props) => {
|
||||
const sx = useProgressImageRenderingStyles();
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={progressImage.dataURL}
|
||||
width={progressImage.width}
|
||||
height={progressImage.height}
|
||||
draggable={false}
|
||||
data-testid="progress-image"
|
||||
objectFit="contain"
|
||||
maxWidth="full"
|
||||
maxHeight="full"
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
sx={sx}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerProgressLinearDenoiseProgress.displayName = 'ViewerProgressLinearDenoiseProgress';
|
@ -0,0 +1,23 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { ViewerToolbarImageButtons } from 'features/viewer/components/ViewerToolbarImageButtons';
|
||||
import { ViewerToolbarImageMenu } from 'features/viewer/components/ViewerToolbarImageMenu';
|
||||
import { ViewerToolbarModeButtons } from 'features/viewer/components/ViewerToolbarModeButtons';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
return (
|
||||
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2} w="full">
|
||||
<Flex flexGrow={1} flexWrap="wrap" justifyContent="flex-start" alignItems="center" gap={2}>
|
||||
<ViewerToolbarImageMenu />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
|
||||
<ViewerToolbarImageButtons />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexWrap="wrap" justifyContent="flex-end" alignItems="center" gap={2}>
|
||||
<ViewerToolbarModeButtons />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbar.displayName = 'ViewerToolbar';
|
@ -0,0 +1,204 @@
|
||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useCurrentImageDTO } from 'features/viewer/hooks/useCurrentImageDTO';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiFlowArrowBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
export const ViewerToolbarImageButtons = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useCurrentImageDTO();
|
||||
|
||||
const { recallBothPrompts, recallSeed, recallWidthAndHeight, recallAllParameters } = useRecallParameters();
|
||||
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(imageDTO?.image_name);
|
||||
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!imageDTO || !imageDTO.has_workflow) {
|
||||
return;
|
||||
}
|
||||
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, imageDTO]);
|
||||
|
||||
useHotkeys('w', handleLoadWorkflow, [imageDTO]);
|
||||
|
||||
const handleClickUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(metadata);
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('a', handleClickUseAllParameters, [metadata]);
|
||||
|
||||
const handleUseSeed = useCallback(() => {
|
||||
recallSeed(metadata?.seed);
|
||||
}, [metadata?.seed, recallSeed]);
|
||||
|
||||
useHotkeys('s', handleUseSeed, [metadata]);
|
||||
|
||||
const handleUsePrompt = useCallback(() => {
|
||||
recallBothPrompts(
|
||||
metadata?.positive_prompt,
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt
|
||||
);
|
||||
}, [
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt,
|
||||
recallBothPrompts,
|
||||
]);
|
||||
|
||||
useHotkeys('p', handleUsePrompt, [metadata]);
|
||||
|
||||
const handleRemixImage = useCallback(() => {
|
||||
// Recalls all metadata parameters except seed
|
||||
recallAllParameters({
|
||||
...metadata,
|
||||
seed: undefined,
|
||||
});
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('r', handleRemixImage, [metadata]);
|
||||
|
||||
const handleUseSize = useCallback(() => {
|
||||
recallWidthAndHeight(metadata?.width, metadata?.height);
|
||||
}, [metadata?.width, metadata?.height, recallWidthAndHeight]);
|
||||
|
||||
useHotkeys('d', handleUseSize, [metadata]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(initialImageSelected(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||
|
||||
const handleClickUpscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(upscaleRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys(
|
||||
'Shift+U',
|
||||
() => {
|
||||
handleClickUpscale();
|
||||
},
|
||||
{
|
||||
enabled: () => Boolean(isUpscalingEnabled && imageDTO && isConnected),
|
||||
},
|
||||
[isUpscalingEnabled, imageDTO, imageDTO, isConnected]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'delete',
|
||||
() => {
|
||||
handleDelete();
|
||||
},
|
||||
[dispatch, imageDTO]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!imageDTO || !imageDTO?.has_workflow}
|
||||
onClick={handleLoadWorkflow}
|
||||
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={!imageDTO || !metadata?.positive_prompt}
|
||||
onClick={handleRemixImage}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={!imageDTO || !metadata?.positive_prompt}
|
||||
onClick={handleUsePrompt}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={!imageDTO || metadata?.seed === null || metadata?.seed === undefined}
|
||||
onClick={handleUseSeed}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
isDisabled={
|
||||
!imageDTO ||
|
||||
metadata?.height === null ||
|
||||
metadata?.height === undefined ||
|
||||
metadata?.width === null ||
|
||||
metadata?.width === undefined
|
||||
}
|
||||
onClick={handleUseSize}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={!imageDTO || !metadata}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isUpscalingEnabled && (
|
||||
<ButtonGroup isDisabled={isQueueMutationInProgress || !imageDTO}>
|
||||
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
<DeleteImageButton onClick={handleDelete} isDisabled={!imageDTO} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbarImageButtons.displayName = 'ViewerToolbarImageButtons';
|
@ -0,0 +1,26 @@
|
||||
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { useCurrentImageDTO } from 'features/viewer/hooks/useCurrentImageDTO';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDotsThreeOutlineFill } from 'react-icons/pi';
|
||||
|
||||
export const ViewerToolbarImageMenu = memo(() => {
|
||||
const imageDTO = useCurrentImageDTO();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label={t('parameters.imageActions')}
|
||||
tooltip={t('parameters.imageActions')}
|
||||
isDisabled={!imageDTO}
|
||||
icon={<PiDotsThreeOutlineFill />}
|
||||
/>
|
||||
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbarImageMenu.displayName = 'ViewerToolbarImageMenu';
|
@ -0,0 +1,64 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, IconButton, spinAnimation } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsProcessing } from 'features/queue/hooks/useIsProcessing';
|
||||
import { viewerModeChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCircleNotchBold, PiEyeBold, PiHourglassMediumFill, PiInfoBold } from 'react-icons/pi';
|
||||
|
||||
const loadingStyles: SystemStyleObject = {
|
||||
svg: { animation: spinAnimation },
|
||||
};
|
||||
|
||||
export const ViewerToolbarModeButtons = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isProcessing = useIsProcessing();
|
||||
const viewerMode = useAppSelector((s) => s.ui.viewerMode);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelectViewerImage = useCallback(() => {
|
||||
dispatch(viewerModeChanged('image'));
|
||||
}, [dispatch]);
|
||||
// TODO: hotkey
|
||||
|
||||
const handleSelectViewerInfo = useCallback(() => {
|
||||
dispatch(viewerModeChanged('info'));
|
||||
}, [dispatch]);
|
||||
useHotkeys('i', handleSelectViewerInfo, [handleSelectViewerInfo]);
|
||||
|
||||
const handleSelectViewerProgress = useCallback(() => {
|
||||
dispatch(viewerModeChanged('progress'));
|
||||
}, [dispatch]);
|
||||
// TODO: hotkey
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiEyeBold />}
|
||||
tooltip={`${t('viewer.viewerModeImage')}`}
|
||||
aria-label={`${t('viewer.viewerModeImage')}`}
|
||||
isChecked={viewerMode === 'image'}
|
||||
onClick={handleSelectViewerImage}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiInfoBold />}
|
||||
tooltip={`${t('viewer.viewerModeInfo')} (I)`}
|
||||
aria-label={`${t('viewer.viewerModeInfo')} (I)`}
|
||||
isChecked={viewerMode === 'info'}
|
||||
onClick={handleSelectViewerInfo}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={`${t('viewer.viewerModeProgress')}`}
|
||||
tooltip={`${t('viewer.viewerModeProgress')}`}
|
||||
icon={isProcessing ? <PiCircleNotchBold /> : <PiHourglassMediumFill />}
|
||||
isChecked={viewerMode === 'progress'}
|
||||
onClick={handleSelectViewerProgress}
|
||||
sx={isProcessing ? loadingStyles : undefined}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbarModeButtons.displayName = 'ViewerToolbarModeButtons';
|
@ -0,0 +1,19 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectLatestImageName } from 'features/viewer/hooks/useLatestImageDTO';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
/**
|
||||
* The currently displayed image's DTO. If the viewer mode is 'progress', returns the latest image's DTO.
|
||||
*/
|
||||
export const useCurrentImageDTO = () => {
|
||||
const latestImageName = useAppSelector(selectLatestImageName);
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const viewerMode = useAppSelector((s) => s.ui.viewerMode);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(
|
||||
(viewerMode === 'progress' ? latestImageName : lastSelectedImage?.image_name) ?? skipToken
|
||||
);
|
||||
|
||||
return imageDTO;
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
|
||||
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const selectLatestCanvasImageName = createSelector(selectProgressSlice, selectCanvasSlice, (progress, _canvas) => {
|
||||
const { latestDenoiseProgress, latestImageOutputEvent } = progress;
|
||||
|
||||
if (!latestImageOutputEvent) {
|
||||
return null;
|
||||
}
|
||||
if (!progress.canvasBatchIds.includes(latestImageOutputEvent.queue_batch_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
latestDenoiseProgress &&
|
||||
latestDenoiseProgress.graph_execution_state_id === latestImageOutputEvent.graph_execution_state_id
|
||||
) {
|
||||
return latestImageOutputEvent.image_name;
|
||||
}
|
||||
|
||||
if (!latestDenoiseProgress?.progress_image) {
|
||||
return latestImageOutputEvent.image_name;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the latest image's DTO. This is not the currently selected image, just the last image received.
|
||||
*/
|
||||
export const useLatestCanvasImageDTO = () => {
|
||||
const latestImageName = useAppSelector(selectLatestCanvasImageName);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(latestImageName ?? skipToken);
|
||||
return imageDTO;
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
export const selectLatestImageName = createSelector(selectProgressSlice, (progress) => {
|
||||
const { latestDenoiseProgress, latestImageOutputEvent, canvasBatchIds } = progress;
|
||||
|
||||
if (!latestImageOutputEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canvasBatchIds.includes(latestImageOutputEvent.queue_batch_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
latestDenoiseProgress &&
|
||||
latestDenoiseProgress.graph_execution_state_id === latestImageOutputEvent.graph_execution_state_id
|
||||
) {
|
||||
return latestImageOutputEvent.image_name;
|
||||
}
|
||||
|
||||
if (!latestDenoiseProgress?.progress_image) {
|
||||
return latestImageOutputEvent.image_name;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the latest image's DTO. This is not the currently selected image, just the last image received.
|
||||
*/
|
||||
export const useLatestImageDTO = () => {
|
||||
const latestImageName = useAppSelector(selectLatestImageName);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(latestImageName ?? skipToken);
|
||||
return imageDTO;
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useProgressImageRenderingStyles = () => {
|
||||
const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage);
|
||||
|
||||
const styles = useMemo<SystemStyleObject>(
|
||||
() => ({
|
||||
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
|
||||
}),
|
||||
[shouldAntialiasProgressImage]
|
||||
);
|
||||
|
||||
return styles;
|
||||
};
|
Reference in New Issue
Block a user