mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
33 Commits
separate-g
...
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
|
## 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`
|
## `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.
|
- 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/
|
[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/core": "^6.1.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource-variable/inter": "^5.0.16",
|
"@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",
|
"@mantine/form": "6.0.21",
|
||||||
"@nanostores/react": "^0.7.1",
|
"@nanostores/react": "^0.7.1",
|
||||||
"@reduxjs/toolkit": "2.0.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
|
specifier: ^5.0.16
|
||||||
version: 5.0.16
|
version: 5.0.16
|
||||||
'@invoke-ai/ui-library':
|
'@invoke-ai/ui-library':
|
||||||
specifier: ^0.0.18
|
specifier: ^0.0.20
|
||||||
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)
|
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':
|
'@mantine/form':
|
||||||
specifier: 6.0.21
|
specifier: 6.0.21
|
||||||
version: 6.0.21(react@18.2.0)
|
version: 6.0.21(react@18.2.0)
|
||||||
@ -1700,6 +1700,13 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
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:
|
/@babel/template@7.22.15:
|
||||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -1960,7 +1967,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@chakra-ui/dom-utils': 2.1.0
|
'@chakra-ui/dom-utils': 2.1.0
|
||||||
react: 18.2.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:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
dev: false
|
dev: false
|
||||||
@ -2992,7 +2999,7 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.8
|
'@babel/runtime': 7.23.9
|
||||||
'@emotion/babel-plugin': 11.11.0
|
'@emotion/babel-plugin': 11.11.0
|
||||||
'@emotion/is-prop-valid': 1.2.1
|
'@emotion/is-prop-valid': 1.2.1
|
||||||
'@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0)
|
'@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0)
|
||||||
@ -3587,8 +3594,8 @@ packages:
|
|||||||
prettier: 3.2.4
|
prettier: 3.2.4
|
||||||
dev: true
|
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):
|
/@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-Yme+2+pzYy3TPb7ZT0hYmBwahH29ZRSVIxLKSexh3BsbJXbTzGssRQU78QvK6Ymxemgbso3P8Rs+IW0zNhQKjQ==}
|
resolution: {integrity: sha512-8BDL9LWbmpAZHTJB0B+zMJ0kBq7vQF0tem6q9WB03CPqdE307FiDmZ++NZF7BP8Rp4Sivdi6OagaXL7WV6e0Pw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@fontsource-variable/inter': ^5.0.16
|
'@fontsource-variable/inter': ^5.0.16
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
@ -3610,8 +3617,8 @@ packages:
|
|||||||
framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0)
|
framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
nanostores: 0.9.5
|
nanostores: 0.9.5
|
||||||
overlayscrollbars: 2.4.7
|
overlayscrollbars: 2.5.0
|
||||||
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)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 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)
|
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
|
react: 18.2.0
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-FPKx9XnXovTnI4+2JXig5uEaTLSEJ6svOwPzIfBBXTHBRNsz2+WhYUmfM0K/BNYxjgDEwuPm+NQhEoOA0RoG1g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
overlayscrollbars: ^2.0.0
|
overlayscrollbars: ^2.0.0
|
||||||
react: '>=16.8.0'
|
react: '>=16.8.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
overlayscrollbars: 2.4.7
|
overlayscrollbars: 2.5.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -11347,8 +11354,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-C7tmhetwMv9frEvIT/RfkAVEgbjRNz/Gh2zE8BVmN+jl35GRaAnz73rlGQCMRoC2arpACAXyMNnJkzHb7GBrcA==}
|
resolution: {integrity: sha512-C7tmhetwMv9frEvIT/RfkAVEgbjRNz/Gh2zE8BVmN+jl35GRaAnz73rlGQCMRoC2arpACAXyMNnJkzHb7GBrcA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/overlayscrollbars@2.4.7:
|
/overlayscrollbars@2.5.0:
|
||||||
resolution: {integrity: sha512-02X2/nHno35dzebCx+EO2tRDaKAOltZqUKdUqvq3Pt8htCuhJbYi+mjr0CYerVeGRRoZ2Uo6/8XrNg//DJJ+GA==}
|
resolution: {integrity: sha512-CWVC2dwS07XZfLHDm5GmZN1iYggiJ8Vufnvzwt0gwR9Yz1hVckKeTxg7VILZeYVGhDYJHZ1Xc8Xfys5dWZ1qiA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/p-limit@2.3.0:
|
/p-limit@2.3.0:
|
||||||
@ -11836,7 +11843,7 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.8
|
'@babel/runtime': 7.23.9
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -11922,8 +11929,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/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):
|
||||||
resolution: {integrity: sha512-B7gYnCjHNrNYwY2juS71dHbf0+UpXXojt02svxybj8N5bxceAkzPChKEncHuratjUHkIFNCn06k2qj1DRlzTug==}
|
resolution: {integrity: sha512-EfhX040SELLqnQ9JftqsmQCG49iByg8F5X5m19Er+n371OaETZ35dlNPZrLOOTlnnwD4c2Zv0KDgabDTc7dPHw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
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':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.8
|
'@babel/runtime': 7.23.9
|
||||||
'@types/react': 18.2.48
|
'@types/react': 18.2.48
|
||||||
focus-lock: 1.0.0
|
focus-lock: 1.0.0
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
@ -11993,7 +12000,7 @@ packages:
|
|||||||
react-native:
|
react-native:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.8
|
'@babel/runtime': 7.23.9
|
||||||
html-parse-stringify: 3.0.1
|
html-parse-stringify: 3.0.1
|
||||||
i18next: 23.7.16
|
i18next: 23.7.16
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
@ -1609,6 +1609,13 @@
|
|||||||
"showProgressImages": "Show Progress Images",
|
"showProgressImages": "Show Progress Images",
|
||||||
"swapSizes": "Swap Sizes"
|
"swapSizes": "Swap Sizes"
|
||||||
},
|
},
|
||||||
|
"viewer": {
|
||||||
|
"viewerModeImage": "Image",
|
||||||
|
"viewerModeInfo": "Info",
|
||||||
|
"viewerModeProgress": "Progress",
|
||||||
|
"dropLabel": "View Image",
|
||||||
|
"noProgress": "Nothing in Progress"
|
||||||
|
},
|
||||||
"unifiedCanvas": {
|
"unifiedCanvas": {
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
"activeLayer": "Active Layer",
|
"activeLayer": "Active Layer",
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import { isAnyOf } from '@reduxjs/toolkit';
|
|
||||||
import { logger } from 'app/logging/logger';
|
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 { addToast } from 'features/system/store/systemSlice';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
|
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages);
|
|
||||||
|
|
||||||
export const addCommitStagingAreaImageListener = () => {
|
export const addCommitStagingAreaImageListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher,
|
matcher: matchAnyStagingAreaDismissed,
|
||||||
effect: async (_, { dispatch, getState }) => {
|
effect: async (_, { dispatch, getState }) => {
|
||||||
const log = logger('canvas');
|
const log = logger('canvas');
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { batchIds } = state.canvas;
|
const { canvasBatchIds } = state.progress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const req = dispatch(
|
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();
|
const { canceled } = await req.unwrap();
|
||||||
req.reset();
|
req.reset();
|
||||||
@ -32,7 +32,6 @@ export const addCommitStagingAreaImageListener = () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
dispatch(canvasBatchIdsReset());
|
|
||||||
} catch {
|
} catch {
|
||||||
log.error('Failed to cancel canvas batches');
|
log.error('Failed to cancel canvas batches');
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
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 { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageCache } from 'services/api/types';
|
import type { ImageCache } from 'services/api/types';
|
||||||
@ -28,7 +28,7 @@ export const addFirstListImagesListener = () => {
|
|||||||
if (data.ids.length > 0) {
|
if (data.ids.length > 0) {
|
||||||
// Select the first image
|
// Select the first image
|
||||||
const firstImage = imagesSelectors.selectAll(data)[0];
|
const firstImage = imagesSelectors.selectAll(data)[0];
|
||||||
dispatch(imageSelected(firstImage ?? null));
|
dispatch(imageSelectionChanged(firstImage ?? null));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { isAnyOf } from '@reduxjs/toolkit';
|
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 { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { imagesSelectors } from 'services/api/util';
|
import { imagesSelectors } from 'services/api/util';
|
||||||
@ -37,17 +37,17 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
|
|
||||||
if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) {
|
if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) {
|
||||||
const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName);
|
const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName);
|
||||||
dispatch(imageSelected(selectedImage || null));
|
dispatch(imageSelectionChanged(selectedImage ?? null));
|
||||||
} else if (boardImagesData) {
|
} else if (boardImagesData) {
|
||||||
const firstImage = imagesSelectors.selectAll(boardImagesData)[0];
|
const firstImage = imagesSelectors.selectAll(boardImagesData)[0];
|
||||||
dispatch(imageSelected(firstImage || null));
|
dispatch(imageSelectionChanged(firstImage ?? null));
|
||||||
} else {
|
} else {
|
||||||
// board has no images - deselect
|
// board has no images - deselect
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelectionChanged(null));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// fallback - deselect
|
// 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 { enqueueRequested } from 'app/store/actions';
|
||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { parseify } from 'common/util/serialize';
|
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 { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||||
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||||
import { buildCanvasGraph } from 'features/nodes/util/graph/buildCanvasGraph';
|
import { buildCanvasGraph } from 'features/nodes/util/graph/buildCanvasGraph';
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
|
import { canvasBatchEnqueued } from 'features/progress/store/progressSlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
@ -121,8 +122,6 @@ export const addEnqueueRequestedCanvasListener = () => {
|
|||||||
const enqueueResult = await req.unwrap();
|
const enqueueResult = await req.unwrap();
|
||||||
req.reset();
|
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
|
// Prep the canvas staging area if it is not yet initialized
|
||||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -135,8 +134,9 @@ export const addEnqueueRequestedCanvasListener = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Associate the session with the canvas session ID
|
if (enqueueResult.batch.batch_id) {
|
||||||
dispatch(canvasBatchIdAdded(batchId));
|
dispatch(canvasBatchEnqueued(enqueueResult.batch.batch_id));
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
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 { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
|
||||||
import { imagesSelectors } from 'services/api/util';
|
import { imagesSelectors } from 'services/api/util';
|
||||||
|
|
||||||
import { startAppListening } from '..';
|
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.
|
* 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 start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||||
const imagesToSelect = imageDTOs.slice(start, end + 1);
|
const imagesToSelect = imageDTOs.slice(start, end + 1);
|
||||||
dispatch(selectionChanged(selection.concat(imagesToSelect)));
|
dispatch(imageSelectionChanged(selection.concat(imagesToSelect)));
|
||||||
}
|
}
|
||||||
} else if (ctrlKey || metaKey) {
|
} else if (ctrlKey || metaKey) {
|
||||||
if (selection.some((i) => i.image_name === imageDTO.image_name) && selection.length > 1) {
|
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 {
|
} else {
|
||||||
dispatch(selectionChanged(selection.concat(imageDTO)));
|
dispatch(imageSelectionChanged(selection.concat(imageDTO)));
|
||||||
}
|
}
|
||||||
} else {
|
} 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 { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
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 { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
@ -62,9 +62,9 @@ export const addRequestedSingleImageDeletionListener = () => {
|
|||||||
const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex];
|
const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex];
|
||||||
|
|
||||||
if (newSelectedImageDTO) {
|
if (newSelectedImageDTO) {
|
||||||
dispatch(imageSelected(newSelectedImageDTO));
|
dispatch(imageSelectionChanged(newSelectedImageDTO));
|
||||||
} else {
|
} else {
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelectionChanged(null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,9 +160,9 @@ export const addRequestedMultipleImageDeletionListener = () => {
|
|||||||
const newSelectedImageDTO = data ? imagesSelectors.selectAll(data)[0] : undefined;
|
const newSelectedImageDTO = data ? imagesSelectors.selectAll(data)[0] : undefined;
|
||||||
|
|
||||||
if (newSelectedImageDTO) {
|
if (newSelectedImageDTO) {
|
||||||
dispatch(imageSelected(newSelectedImageDTO));
|
dispatch(imageSelectionChanged(newSelectedImageDTO));
|
||||||
} else {
|
} else {
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelectionChanged(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(isModalOpenChanged(false));
|
dispatch(isModalOpenChanged(false));
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
controlAdapterIsEnabledChanged,
|
controlAdapterIsEnabledChanged,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
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 { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
@ -37,14 +37,14 @@ export const addImageDroppedListener = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image dropped on current image
|
* Image dropped on viewer
|
||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
overData.actionType === 'SET_VIEWER_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
dispatch(imageSelected(activeData.payload.imageDTO));
|
dispatch(imageSelectionChanged(activeData.payload.imageDTO));
|
||||||
return;
|
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 { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export const addImagesStarredListener = () => {
|
|||||||
updatedSelection.push(selectedImageDTO);
|
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 { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export const addImagesUnstarredListener = () => {
|
|||||||
updatedSelection.push(selectedImageDTO);
|
updatedSelection.push(selectedImageDTO);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dispatch(selectionChanged(updatedSelection));
|
dispatch(imageSelectionChanged(updatedSelection));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
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 { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import { isImageOutput } from 'features/nodes/types/common';
|
import { isImageOutput } from 'features/nodes/types/common';
|
||||||
import { LINEAR_UI_OUTPUT, nodeIDDenyList } from 'features/nodes/util/graph/constants';
|
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 { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { imagesAdapter } from 'services/api/util';
|
import { imagesAdapter } from 'services/api/util';
|
||||||
@ -29,7 +30,7 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
// This complete event has an associated image output
|
// This complete event has an associated image output
|
||||||
if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type) && !nodeIDDenyList.includes(source_node_id)) {
|
if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type) && !nodeIDDenyList.includes(source_node_id)) {
|
||||||
const { image_name } = result.image;
|
const { image_name } = result.image;
|
||||||
const { canvas, gallery } = getState();
|
const { gallery, progress } = getState();
|
||||||
|
|
||||||
// This populates the `getImageDTO` cache
|
// This populates the `getImageDTO` cache
|
||||||
const imageDTORequest = dispatch(
|
const imageDTORequest = dispatch(
|
||||||
@ -41,8 +42,10 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
const imageDTO = await imageDTORequest.unwrap();
|
const imageDTO = await imageDTORequest.unwrap();
|
||||||
imageDTORequest.unsubscribe();
|
imageDTORequest.unsubscribe();
|
||||||
|
|
||||||
|
dispatch(imageInvocationComplete({ data, imageDTO }));
|
||||||
|
|
||||||
// Add canvas images to the staging area
|
// 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));
|
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 { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
||||||
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
||||||
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
||||||
|
import { progressPersistConfig, progressSlice } from 'features/progress/store/progressSlice';
|
||||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||||
import { configSlice } from 'features/system/store/configSlice';
|
import { configSlice } from 'features/system/store/configSlice';
|
||||||
@ -61,6 +62,7 @@ const allReducers = {
|
|||||||
[queueSlice.name]: queueSlice.reducer,
|
[queueSlice.name]: queueSlice.reducer,
|
||||||
[workflowSlice.name]: workflowSlice.reducer,
|
[workflowSlice.name]: workflowSlice.reducer,
|
||||||
[hrfSlice.name]: hrfSlice.reducer,
|
[hrfSlice.name]: hrfSlice.reducer,
|
||||||
|
[progressSlice.name]: progressSlice.reducer,
|
||||||
[api.reducerPath]: api.reducer,
|
[api.reducerPath]: api.reducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,6 +107,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[loraPersistConfig.name]: loraPersistConfig,
|
[loraPersistConfig.name]: loraPersistConfig,
|
||||||
[modelManagerPersistConfig.name]: modelManagerPersistConfig,
|
[modelManagerPersistConfig.name]: modelManagerPersistConfig,
|
||||||
[hrfPersistConfig.name]: hrfPersistConfig,
|
[hrfPersistConfig.name]: hrfPersistConfig,
|
||||||
|
[progressPersistConfig.name]: progressPersistConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unserialize: UnserializeFunction = (data, key) => {
|
const unserialize: UnserializeFunction = (data, key) => {
|
||||||
|
@ -37,17 +37,18 @@ type IAIDndImageProps = FlexProps & {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
thumbnail?: boolean;
|
thumbnail?: boolean;
|
||||||
noContentFallback?: ReactElement;
|
noContentFallback?: ReactElement;
|
||||||
useThumbailFallback?: boolean;
|
|
||||||
withHoverOverlay?: boolean;
|
withHoverOverlay?: boolean;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
uploadElement?: ReactNode;
|
uploadElement?: ReactNode;
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
|
fallbackSrc?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||||
const {
|
const {
|
||||||
imageDTO,
|
imageDTO,
|
||||||
onError,
|
onError,
|
||||||
|
onLoad,
|
||||||
onClick,
|
onClick,
|
||||||
withMetadataOverlay = false,
|
withMetadataOverlay = false,
|
||||||
isDropDisabled = false,
|
isDropDisabled = false,
|
||||||
@ -64,7 +65,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
thumbnail = false,
|
thumbnail = false,
|
||||||
noContentFallback = defaultNoContentFallback,
|
noContentFallback = defaultNoContentFallback,
|
||||||
uploadElement = defaultUploadElement,
|
uploadElement = defaultUploadElement,
|
||||||
useThumbailFallback,
|
fallbackSrc,
|
||||||
withHoverOverlay = false,
|
withHoverOverlay = false,
|
||||||
children,
|
children,
|
||||||
onMouseOver,
|
onMouseOver,
|
||||||
@ -150,8 +151,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
<Image
|
<Image
|
||||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||||
fallbackStrategy="beforeLoadOrError"
|
fallbackStrategy="beforeLoadOrError"
|
||||||
fallbackSrc={useThumbailFallback ? imageDTO.thumbnail_url : undefined}
|
fallbackSrc={fallbackSrc ?? imageDTO.thumbnail_url}
|
||||||
fallback={useThumbailFallback ? undefined : <IAILoadingImageFallback image={imageDTO} />}
|
fallback={fallbackSrc ? undefined : <IAILoadingImageFallback image={imageDTO} />}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
w={imageDTO.width}
|
w={imageDTO.width}
|
||||||
@ -161,6 +162,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
sx={imageSx}
|
sx={imageSx}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
|
onLoad={onLoad}
|
||||||
/>
|
/>
|
||||||
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
|
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
|
||||||
<SelectionOverlay isSelected={isSelected} isHovered={withHoverOverlay ? isHovered : false} />
|
<SelectionOverlay isSelected={isSelected} isHovered={withHoverOverlay ? isHovered : false} />
|
||||||
@ -175,11 +177,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!imageDTO && isUploadDisabled && noContentFallback}
|
{!imageDTO && isUploadDisabled && noContentFallback}
|
||||||
{imageDTO && !isDragDisabled && (
|
{imageDTO && !isDragDisabled && draggableData && (
|
||||||
<IAIDraggable data={draggableData} disabled={isDragDisabled || !imageDTO} onClick={onClick} />
|
<IAIDraggable data={draggableData} disabled={isDragDisabled || !imageDTO} onClick={onClick} />
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
{!isDropDisabled && <IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />}
|
{!isDropDisabled && droppableData && (
|
||||||
|
<IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</ImageContextMenu>
|
</ImageContextMenu>
|
||||||
|
@ -28,7 +28,6 @@ import { Layer, Stage } from 'react-konva';
|
|||||||
|
|
||||||
import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay';
|
import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay';
|
||||||
import IAICanvasGrid from './IAICanvasGrid';
|
import IAICanvasGrid from './IAICanvasGrid';
|
||||||
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
|
|
||||||
import IAICanvasMaskCompositer from './IAICanvasMaskCompositer';
|
import IAICanvasMaskCompositer from './IAICanvasMaskCompositer';
|
||||||
import IAICanvasMaskLines from './IAICanvasMaskLines';
|
import IAICanvasMaskLines from './IAICanvasMaskLines';
|
||||||
import IAICanvasObjectRenderer from './IAICanvasObjectRenderer';
|
import IAICanvasObjectRenderer from './IAICanvasObjectRenderer';
|
||||||
@ -55,7 +54,7 @@ const IAICanvas = () => {
|
|||||||
const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox);
|
const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox);
|
||||||
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
|
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
|
||||||
const stageScale = useAppSelector((s) => s.canvas.stageScale);
|
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 shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias);
|
||||||
const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox);
|
const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox);
|
||||||
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
|
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
|
||||||
@ -184,7 +183,7 @@ const IAICanvas = () => {
|
|||||||
<Layer id="preview" imageSmoothingEnabled={shouldAntialias}>
|
<Layer id="preview" imageSmoothingEnabled={shouldAntialias}>
|
||||||
{!isStaging && <IAICanvasToolPreview visible={tool !== 'move'} listening={false} />}
|
{!isStaging && <IAICanvasToolPreview visible={tool !== 'move'} listening={false} />}
|
||||||
<IAICanvasStagingArea listening={false} visible={isStaging} />
|
<IAICanvasStagingArea listening={false} visible={isStaging} />
|
||||||
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
|
{/* {shouldShowIntermediates && <IAICanvasIntermediateImage />} */}
|
||||||
<IAICanvasBoundingBox visible={shouldShowBoundingBox && !isStaging} />
|
<IAICanvasBoundingBox visible={shouldShowBoundingBox && !isStaging} />
|
||||||
</Layer>
|
</Layer>
|
||||||
</ChakraStage>
|
</ChakraStage>
|
||||||
|
@ -1,20 +1,33 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
|
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 { memo, useEffect, useState } from 'react';
|
||||||
import { Image as KonvaImage } from 'react-konva';
|
import { Image as KonvaImage } from 'react-konva';
|
||||||
|
|
||||||
const progressImageSelector = createMemoizedSelector([selectSystemSlice, selectCanvasSlice], (system, canvas) => {
|
export const progressImageSelector = createMemoizedSelector(
|
||||||
const { denoiseProgress } = system;
|
[selectProgressSlice, selectCanvasSlice],
|
||||||
const { batchIds } = canvas;
|
(progress, canvas) => {
|
||||||
|
const isLatestProgressFromCanvas =
|
||||||
|
progress.latestDenoiseProgress && progress.canvasBatchIds.includes(progress.latestDenoiseProgress.queue_batch_id);
|
||||||
|
|
||||||
return {
|
const { selectedImageIndex, images } = canvas.layerState.stagingArea;
|
||||||
progressImage:
|
const _currentStagingAreaImage =
|
||||||
denoiseProgress && batchIds.includes(denoiseProgress.batch_id) ? denoiseProgress.progress_image : undefined,
|
images.length > 0 && selectedImageIndex !== undefined ? images[selectedImageIndex] : undefined;
|
||||||
boundingBox: canvas.layerState.stagingArea.boundingBox,
|
|
||||||
};
|
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 IAICanvasIntermediateImage = () => {
|
||||||
const { progressImage, boundingBox } = useAppSelector(progressImageSelector);
|
const { progressImage, boundingBox } = useAppSelector(progressImageSelector);
|
||||||
|
@ -5,7 +5,7 @@ import type { GroupConfig } from 'konva/lib/Group';
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Group, Rect } from 'react-konva';
|
import { Group, Rect } from 'react-konva';
|
||||||
|
|
||||||
import IAICanvasImage from './IAICanvasImage';
|
import { IAICanvasStagingAreaImage } from './IAICanvasStagingAreaImage';
|
||||||
|
|
||||||
const dash = [4, 4];
|
const dash = [4, 4];
|
||||||
|
|
||||||
@ -37,12 +37,11 @@ const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
|||||||
type Props = GroupConfig;
|
type Props = GroupConfig;
|
||||||
|
|
||||||
const IAICanvasStagingArea = (props: Props) => {
|
const IAICanvasStagingArea = (props: Props) => {
|
||||||
const { currentStagingAreaImage, shouldShowStagingImage, shouldShowStagingOutline, x, y, width, height } =
|
const { shouldShowStagingOutline, x, y, width, height } = useAppSelector(selector);
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group {...props}>
|
<Group {...props}>
|
||||||
{shouldShowStagingImage && currentStagingAreaImage && <IAICanvasImage canvasImage={currentStagingAreaImage} />}
|
<IAICanvasStagingAreaImage />
|
||||||
{shouldShowStagingOutline && (
|
{shouldShowStagingOutline && (
|
||||||
<Group listening={false}>
|
<Group listening={false}>
|
||||||
<Rect
|
<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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||||
|
|
||||||
import { selectCanvasSlice } from './canvasSlice';
|
import { selectCanvasSlice } from './canvasSlice';
|
||||||
import { isCanvasBaseImage } from './canvasTypes';
|
import { isCanvasBaseImage } from './canvasTypes';
|
||||||
|
|
||||||
export const isStagingSelector = createSelector(
|
export const isStagingSelector = createSelector(
|
||||||
|
selectProgressSlice,
|
||||||
selectCanvasSlice,
|
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) =>
|
export const initialCanvasImageSelector = createMemoizedSelector(selectCanvasSlice, (canvas) =>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import calculateCoordinates from 'features/canvas/util/calculateCoordinates';
|
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 type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { clamp, cloneDeep } from 'lodash-es';
|
import { clamp, cloneDeep } from 'lodash-es';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BoundingBoxScaleMethod,
|
BoundingBoxScaleMethod,
|
||||||
@ -79,7 +77,6 @@ export const initialCanvasState: CanvasState = {
|
|||||||
stageCoordinates: { x: 0, y: 0 },
|
stageCoordinates: { x: 0, y: 0 },
|
||||||
stageDimensions: { width: 0, height: 0 },
|
stageDimensions: { width: 0, height: 0 },
|
||||||
stageScale: 1,
|
stageScale: 1,
|
||||||
batchIds: [],
|
|
||||||
aspectRatio: {
|
aspectRatio: {
|
||||||
id: '1:1',
|
id: '1:1',
|
||||||
value: 1,
|
value: 1,
|
||||||
@ -180,7 +177,6 @@ export const canvasSlice = createSlice({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.batchIds = [];
|
|
||||||
|
|
||||||
const newScale = calculateScale(
|
const newScale = calculateScale(
|
||||||
stageDimensions.width,
|
stageDimensions.width,
|
||||||
@ -237,12 +233,6 @@ export const canvasSlice = createSlice({
|
|||||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowBoundingBox = action.payload;
|
state.shouldShowBoundingBox = action.payload;
|
||||||
},
|
},
|
||||||
canvasBatchIdAdded: (state, action: PayloadAction<string>) => {
|
|
||||||
state.batchIds.push(action.payload);
|
|
||||||
},
|
|
||||||
canvasBatchIdsReset: (state) => {
|
|
||||||
state.batchIds = [];
|
|
||||||
},
|
|
||||||
stagingAreaInitialized: (
|
stagingAreaInitialized: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@ -293,7 +283,6 @@ export const canvasSlice = createSlice({
|
|||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.shouldShowStagingOutline = true;
|
state.shouldShowStagingOutline = true;
|
||||||
state.shouldShowStagingImage = true;
|
state.shouldShowStagingImage = true;
|
||||||
state.batchIds = [];
|
|
||||||
},
|
},
|
||||||
addFillRect: (state) => {
|
addFillRect: (state) => {
|
||||||
const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state;
|
const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state;
|
||||||
@ -426,7 +415,6 @@ export const canvasSlice = createSlice({
|
|||||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||||
state.layerState = cloneDeep(initialLayerState);
|
state.layerState = cloneDeep(initialLayerState);
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.batchIds = [];
|
|
||||||
state.boundingBoxCoordinates = {
|
state.boundingBoxCoordinates = {
|
||||||
...initialCanvasState.boundingBoxCoordinates,
|
...initialCanvasState.boundingBoxCoordinates,
|
||||||
};
|
};
|
||||||
@ -536,7 +524,6 @@ export const canvasSlice = createSlice({
|
|||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.shouldShowStagingOutline = true;
|
state.shouldShowStagingOutline = true;
|
||||||
state.shouldShowStagingImage = true;
|
state.shouldShowStagingImage = true;
|
||||||
state.batchIds = [];
|
|
||||||
},
|
},
|
||||||
setBoundingBoxScaleMethod: {
|
setBoundingBoxScaleMethod: {
|
||||||
reducer: (state, action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>) => {
|
reducer: (state, action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>) => {
|
||||||
@ -644,23 +631,6 @@ export const canvasSlice = createSlice({
|
|||||||
optimalDimension
|
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,
|
stagingAreaInitialized,
|
||||||
setShouldAntialias,
|
setShouldAntialias,
|
||||||
canvasResized,
|
canvasResized,
|
||||||
canvasBatchIdAdded,
|
|
||||||
canvasBatchIdsReset,
|
|
||||||
aspectRatioChanged,
|
aspectRatioChanged,
|
||||||
scaledBoundingBoxDimensionsReset,
|
scaledBoundingBoxDimensionsReset,
|
||||||
} = canvasSlice.actions;
|
} = canvasSlice.actions;
|
||||||
@ -736,3 +704,5 @@ export const canvasPersistConfig: PersistConfig<CanvasState> = {
|
|||||||
migrate: migrateCanvasState,
|
migrate: migrateCanvasState,
|
||||||
persistDenylist: [],
|
persistDenylist: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const matchAnyStagingAreaDismissed = isAnyOf(commitStagingAreaImage, discardStagedImages);
|
||||||
|
@ -142,7 +142,6 @@ export interface CanvasState {
|
|||||||
stageDimensions: Dimensions;
|
stageDimensions: Dimensions;
|
||||||
stageScale: number;
|
stageScale: number;
|
||||||
generationMode?: GenerationMode;
|
generationMode?: GenerationMode;
|
||||||
batchIds: string[];
|
|
||||||
aspectRatio: AspectRatioState;
|
aspectRatio: AspectRatioState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,14 +12,19 @@ import type {
|
|||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import type { BoardId } from 'features/gallery/store/types';
|
import type { BoardId } from 'features/gallery/store/types';
|
||||||
import type { FieldInputInstance, FieldInputTemplate } from 'features/nodes/types/field';
|
import type { FieldInputInstance, FieldInputTemplate } from 'features/nodes/types/field';
|
||||||
|
import type { ViewerMode } from 'features/ui/store/uiTypes';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
type BaseDropData = {
|
type BaseDropData = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CurrentImageDropData = BaseDropData & {
|
export type ViewerImageDropData = BaseDropData & {
|
||||||
actionType: 'SET_CURRENT_IMAGE';
|
actionType: 'SET_VIEWER_IMAGE';
|
||||||
|
context: {
|
||||||
|
viewerMode: ViewerMode;
|
||||||
|
currentImageName?: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitialImageDropData = BaseDropData & {
|
export type InitialImageDropData = BaseDropData & {
|
||||||
@ -59,13 +64,13 @@ export type RemoveFromBoardDropData = BaseDropData & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TypesafeDroppableData =
|
export type TypesafeDroppableData =
|
||||||
| CurrentImageDropData
|
|
||||||
| InitialImageDropData
|
| InitialImageDropData
|
||||||
| ControlAdapterDropData
|
| ControlAdapterDropData
|
||||||
| CanvasInitialImageDropData
|
| CanvasInitialImageDropData
|
||||||
| NodesImageDropData
|
| NodesImageDropData
|
||||||
| AddToBoardDropData
|
| AddToBoardDropData
|
||||||
| RemoveFromBoardDropData;
|
| RemoveFromBoardDropData
|
||||||
|
| ViewerImageDropData;
|
||||||
|
|
||||||
type BaseDragData = {
|
type BaseDragData = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -13,8 +13,11 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (actionType) {
|
switch (actionType) {
|
||||||
case 'SET_CURRENT_IMAGE':
|
case 'SET_VIEWER_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO';
|
return (
|
||||||
|
payloadType === 'IMAGE_DTO' &&
|
||||||
|
overData.context.currentImageName !== active.data.current.payload.imageDTO.image_name
|
||||||
|
);
|
||||||
case 'SET_INITIAL_IMAGE':
|
case 'SET_INITIAL_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO';
|
return payloadType === 'IMAGE_DTO';
|
||||||
case 'SET_CONTROL_ADAPTER_IMAGE':
|
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
|
<Flex
|
||||||
layerStyle="first"
|
layerStyle="first"
|
||||||
padding={4}
|
padding={4}
|
||||||
gap={1}
|
gap={4}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
width="full"
|
width="full"
|
||||||
height="full"
|
height="full"
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
position="absolute"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<ExternalLink href={image.image_url} label={image.image_name} />
|
<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 { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
||||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
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 { getIsVisible } from 'features/gallery/util/getIsVisible';
|
||||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
@ -144,7 +144,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
|||||||
if (!image || index === lastSelectedImageIndex) {
|
if (!image || index === lastSelectedImageIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageSelected(image));
|
dispatch(imageSelectionChanged(image));
|
||||||
scrollToImage(image.image_name, index);
|
scrollToImage(image.image_name, index);
|
||||||
},
|
},
|
||||||
[dispatch, lastSelectedImageIndex, data]
|
[dispatch, lastSelectedImageIndex, data]
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
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 { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@ -26,7 +25,7 @@ export const useMultiselect = (imageDTO?: ImageDTO) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isMultiSelectEnabled) {
|
if (!isMultiSelectEnabled) {
|
||||||
dispatch(selectionChanged([imageDTO]));
|
dispatch(imageSelectionChanged(imageDTO));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
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 type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { uniqBy } from 'lodash-es';
|
import { uniqBy } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
@ -26,11 +26,16 @@ export const gallerySlice = createSlice({
|
|||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
initialState: initialGalleryState,
|
initialState: initialGalleryState,
|
||||||
reducers: {
|
reducers: {
|
||||||
imageSelected: (state, action: PayloadAction<ImageDTO | null>) => {
|
imageSelectionChanged: (state, action: PayloadAction<ImageDTO[] | ImageDTO | null>) => {
|
||||||
state.selection = action.payload ? [action.payload] : [];
|
if (!action.payload) {
|
||||||
},
|
state.selection = [];
|
||||||
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
return;
|
||||||
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
}
|
||||||
|
if (Array.isArray(action.payload)) {
|
||||||
|
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.selection = [action.payload];
|
||||||
},
|
},
|
||||||
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitch = action.payload;
|
state.shouldAutoSwitch = action.payload;
|
||||||
@ -97,14 +102,13 @@ export const gallerySlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
imageSelected,
|
|
||||||
shouldAutoSwitchChanged,
|
shouldAutoSwitchChanged,
|
||||||
autoAssignBoardOnClickChanged,
|
autoAssignBoardOnClickChanged,
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
autoAddBoardIdChanged,
|
autoAddBoardIdChanged,
|
||||||
galleryViewChanged,
|
galleryViewChanged,
|
||||||
selectionChanged,
|
imageSelectionChanged,
|
||||||
boardSearchTextChanged,
|
boardSearchTextChanged,
|
||||||
moreImagesLoaded,
|
moreImagesLoaded,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
@ -130,3 +134,10 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
|
|||||||
migrate: migrateGalleryState,
|
migrate: migrateGalleryState,
|
||||||
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit'],
|
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) {
|
if (imageDTO) {
|
||||||
return (
|
return (
|
||||||
<Wrapper nodeProps={props}>
|
<Wrapper nodeProps={props}>
|
||||||
<IAIDndImage imageDTO={imageDTO} isDragDisabled useThumbailFallback />
|
<IAIDndImage imageDTO={imageDTO} isDragDisabled fallbackSrc={imageDTO?.thumbnail_url} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
|||||||
droppableData={droppableData}
|
droppableData={droppableData}
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
postUploadAction={postUploadAction}
|
postUploadAction={postUploadAction}
|
||||||
useThumbailFallback
|
fallbackSrc={imageDTO?.thumbnail_url}
|
||||||
uploadElement={<UploadElement />}
|
uploadElement={<UploadElement />}
|
||||||
dropLabel={<DropLabel />}
|
dropLabel={<DropLabel />}
|
||||||
minSize={8}
|
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 { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import ProgressBar from 'features/progress/components/ProgressBar';
|
||||||
import ClearQueueIconButton from 'features/queue/components/ClearQueueIconButton';
|
import ClearQueueIconButton from 'features/queue/components/ClearQueueIconButton';
|
||||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||||
import ProgressBar from 'features/system/components/ProgressBar';
|
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { memo } from 'react';
|
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 ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||||
import { ClearAllQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
import { ClearAllQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||||
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
||||||
|
import { useIsProcessing } from 'features/queue/hooks/useIsProcessing';
|
||||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCircleNotchBold, PiSlidersHorizontalBold } from 'react-icons/pi';
|
import { PiCircleNotchBold, PiSlidersHorizontalBold } from 'react-icons/pi';
|
||||||
import { RiSparklingFill } from 'react-icons/ri';
|
import { RiSparklingFill } from 'react-icons/ri';
|
||||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
|
||||||
|
|
||||||
const floatingButtonStyles: SystemStyleObject = {
|
const floatingButtonStyles: SystemStyleObject = {
|
||||||
borderStartRadius: 0,
|
borderStartRadius: 0,
|
||||||
@ -24,16 +24,16 @@ type Props = {
|
|||||||
const FloatingSidePanelButtons = (props: Props) => {
|
const FloatingSidePanelButtons = (props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
||||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
const isProcessing = useIsProcessing();
|
||||||
|
|
||||||
const queueButtonIcon = useMemo(
|
const queueButtonIcon = useMemo(
|
||||||
() =>
|
() =>
|
||||||
!isDisabled && queueStatus?.processor.is_processing ? (
|
!isDisabled && isProcessing ? (
|
||||||
<Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />
|
<Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />
|
||||||
) : (
|
) : (
|
||||||
<RiSparklingFill size="16px" />
|
<RiSparklingFill size="16px" />
|
||||||
),
|
),
|
||||||
[isDisabled, queueStatus?.processor.is_processing]
|
[isDisabled, isProcessing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
const disclosure = useDisclosure();
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
import { Viewer } from 'features/viewer/components/Viewer';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<Flex w="full" h="full">
|
<Viewer />
|
||||||
<CurrentImageDisplay />
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { galleryImageClicked } from 'features/gallery/store/gallerySlice';
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
|
|
||||||
import type { InvokeTabName } from './tabMap';
|
import type { InvokeTabName } from './tabMap';
|
||||||
import type { UIState } from './uiTypes';
|
import type { UIState, ViewerMode } from './uiTypes';
|
||||||
|
|
||||||
export const initialUIState: UIState = {
|
export const initialUIState: UIState = {
|
||||||
_version: 1,
|
_version: 1,
|
||||||
@ -14,6 +15,7 @@ export const initialUIState: UIState = {
|
|||||||
panels: {},
|
panels: {},
|
||||||
accordions: {},
|
accordions: {},
|
||||||
expanders: {},
|
expanders: {},
|
||||||
|
viewerMode: 'image',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uiSlice = createSlice({
|
export const uiSlice = createSlice({
|
||||||
@ -40,11 +42,22 @@ export const uiSlice = createSlice({
|
|||||||
const { id, isOpen } = action.payload;
|
const { id, isOpen } = action.payload;
|
||||||
state.expanders[id] = isOpen;
|
state.expanders[id] = isOpen;
|
||||||
},
|
},
|
||||||
|
viewerModeChanged: (state, action: PayloadAction<ViewerMode>) => {
|
||||||
|
state.viewerMode = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
builder.addCase(initialImageChanged, (state) => {
|
builder.addCase(initialImageChanged, (state) => {
|
||||||
state.activeTab = 'img2img';
|
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,
|
panelsChanged,
|
||||||
accordionStateChanged,
|
accordionStateChanged,
|
||||||
expanderStateChanged,
|
expanderStateChanged,
|
||||||
|
viewerModeChanged,
|
||||||
} = uiSlice.actions;
|
} = uiSlice.actions;
|
||||||
|
|
||||||
export const selectUiSlice = (state: RootState) => state.ui;
|
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.
|
* 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>;
|
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