feat(ui): wip gallery migration

This commit is contained in:
psychedelicious 2023-04-04 22:58:46 +10:00
parent cfe86ec541
commit cc3401a159
15 changed files with 231 additions and 98 deletions

View File

@ -1,5 +1,10 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import {
GeneratorProgressEvent,
InvocationCompleteEvent,
InvocationErrorEvent,
InvocationStartedEvent,
} from 'services/events/types';
/** /**
* We can't use redux-toolkit's createSlice() to make these actions, * We can't use redux-toolkit's createSlice() to make these actions,
* because they have no associated reducer. They only exist to dispatch * because they have no associated reducer. They only exist to dispatch
@ -9,3 +14,23 @@ import { createAction } from '@reduxjs/toolkit';
export const emitSubscribe = createAction<string>('socketio/subscribe'); export const emitSubscribe = createAction<string>('socketio/subscribe');
export const emitUnsubscribe = createAction<string>('socketio/unsubscribe'); export const emitUnsubscribe = createAction<string>('socketio/unsubscribe');
type Timestamp = {
timestamp: Date;
};
export const invocationStarted = createAction<
{ data: InvocationStartedEvent } & Timestamp
>('socketio/invocationStarted');
export const invocationComplete = createAction<
{ data: InvocationCompleteEvent } & Timestamp
>('socketio/invocationComplete');
export const invocationError = createAction<
{ data: InvocationErrorEvent } & Timestamp
>('socketio/invocationError');
export const generatorProgress = createAction<
{ data: GeneratorProgressEvent } & Timestamp
>('socketio/generatorProgress');

View File

@ -10,6 +10,8 @@ import {
setIsCancelable, setIsCancelable,
setIsConnected, setIsConnected,
setIsProcessing, setIsProcessing,
socketioConnected,
socketioDisconnected,
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
import { import {
@ -32,13 +34,13 @@ import {
setStatus, setStatus,
STATUS, STATUS,
} from 'services/apiSlice'; } from 'services/apiSlice';
import { emitUnsubscribe } from './actions'; import { emitUnsubscribe, invocationComplete } from './actions';
import { resultAdded } from 'features/gallery/store/resultsSlice'; import { resultAdded } from 'features/gallery/store/resultsSlice';
import { import {
getNextResultsPage, receivedResultImagesPage,
getNextUploadsPage, receivedUploadImagesPage,
} from 'services/thunks/gallery'; } from 'services/thunks/gallery';
import { processImageField } from 'services/util/processImageField'; import { deserializeImageField } from 'services/util/deserializeImageField';
/** /**
* Returns an object containing listener callbacks * Returns an object containing listener callbacks
@ -54,15 +56,15 @@ const makeSocketIOListeners = (
*/ */
onConnect: () => { onConnect: () => {
try { try {
dispatch(setIsConnected(true)); dispatch(socketioConnected());
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
// fetch more images, but only if we don't already have images // fetch more images, but only if we don't already have images
if (!getState().results.ids.length) { if (!getState().results.ids.length) {
dispatch(getNextResultsPage()); dispatch(receivedResultImagesPage());
} }
if (!getState().uploads.ids.length) { if (!getState().uploads.ids.length) {
dispatch(getNextUploadsPage()); dispatch(receivedUploadImagesPage());
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -73,8 +75,8 @@ const makeSocketIOListeners = (
*/ */
onDisconnect: () => { onDisconnect: () => {
try { try {
dispatch(setIsConnected(false)); dispatch(socketioDisconnected());
dispatch(setCurrentStatus(i18n.t('common.statusDisconnected'))); dispatch(emitUnsubscribe(getState().api.sessionId));
dispatch( dispatch(
addLogEntry({ addLogEntry({
@ -97,38 +99,40 @@ const makeSocketIOListeners = (
onInvocationComplete: (data: InvocationCompleteEvent) => { onInvocationComplete: (data: InvocationCompleteEvent) => {
console.log('invocation_complete', data); console.log('invocation_complete', data);
try { try {
dispatch(invocationComplete({ data, timestamp: new Date() }));
const sessionId = data.graph_execution_state_id; const sessionId = data.graph_execution_state_id;
if (data.result.type === 'image') { if (data.result.type === 'image') {
const resultImage = processImageField(data.result.image); // const resultImage = deserializeImageField(data.result.image);
dispatch(resultAdded(resultImage)); // dispatch(resultAdded(resultImage));
// // need to update the type for this or figure out how to get these values // // need to update the type for this or figure out how to get these values
dispatch( // dispatch(
addImage({ // addImage({
category: 'result', // category: 'result',
image: { // image: {
uuid: uuidv4(), // uuid: uuidv4(),
url: resultImage.url, // url: resultImage.url,
thumbnail: '', // thumbnail: '',
width: 512, // width: 512,
height: 512, // height: 512,
category: 'result', // category: 'result',
name: resultImage.name, // name: resultImage.name,
mtime: new Date().getTime(), // mtime: new Date().getTime(),
}, // },
}) // })
); // );
// dispatch(setIsProcessing(false));
// dispatch(setIsCancelable(false));
dispatch( // dispatch(
addLogEntry({ // addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'), // timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Generated: ${data.result.image.image_name}`, // message: `Generated: ${data.result.image.image_name}`,
}) // })
); // );
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(false));
dispatch(emitUnsubscribe(sessionId)); dispatch(emitUnsubscribe(sessionId));
dispatch(setSessionId(null)); // dispatch(setSessionId(''));
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -10,6 +10,7 @@ import {
InvocationErrorEvent, InvocationErrorEvent,
InvocationStartedEvent, InvocationStartedEvent,
} from 'services/events/types'; } from 'services/events/types';
import { invocationComplete } from './actions';
const socket_url = `ws://${window.location.host}`; const socket_url = `ws://${window.location.host}`;
@ -40,6 +41,12 @@ export const socketioMiddleware = () => {
areListenersSet = true; areListenersSet = true;
// use the action's match() function for type narrowing and safety
if (invocationComplete.match(action)) {
emitUnsubscribe(action.payload.data.graph_execution_state_id);
socketio.removeAllListeners();
}
/** /**
* Handle redux actions caught by middleware. * Handle redux actions caught by middleware.
*/ */
@ -63,12 +70,12 @@ export const socketioMiddleware = () => {
break; break;
} }
case 'socketio/unsubscribe': { // case 'socketio/unsubscribe': {
emitUnsubscribe(action.payload); // emitUnsubscribe(action.payload);
socketio.removeAllListeners(); // socketio.removeAllListeners();
break; // break;
} // }
} }
next(action); next(action);

View File

@ -4,17 +4,20 @@ import { useAppSelector } from 'app/storeHooks';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { MdPhoto } from 'react-icons/md'; import { MdPhoto } from 'react-icons/md';
import { gallerySelector } from '../store/gallerySelectors'; import {
gallerySelector,
selectedImageSelector,
} from '../store/gallerySelectors';
import CurrentImageButtons from './CurrentImageButtons'; import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview'; import CurrentImagePreview from './CurrentImagePreview';
export const currentImageDisplaySelector = createSelector( export const currentImageDisplaySelector = createSelector(
[gallerySelector], [gallerySelector, selectedImageSelector],
(gallery) => { (gallery, selectedImage) => {
const { currentImage, intermediateImage } = gallery; const { currentImage, intermediateImage } = gallery;
return { return {
hasAnImageToDisplay: currentImage || intermediateImage, hasAnImageToDisplay: selectedImage || intermediateImage,
}; };
}, },
{ {

View File

@ -6,19 +6,22 @@ import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { APP_METADATA_HEIGHT } from 'theme/util/constants'; import { APP_METADATA_HEIGHT } from 'theme/util/constants';
import { gallerySelector } from '../store/gallerySelectors'; import {
gallerySelector,
selectedImageSelector,
} from '../store/gallerySelectors';
import CurrentImageFallback from './CurrentImageFallback'; import CurrentImageFallback from './CurrentImageFallback';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[gallerySelector, uiSelector], [gallerySelector, uiSelector, selectedImageSelector],
(gallery: GalleryState, ui) => { (gallery: GalleryState, ui, selectedImage) => {
const { currentImage, intermediateImage } = gallery; const { currentImage, intermediateImage } = gallery;
const { shouldShowImageDetails } = ui; const { shouldShowImageDetails } = ui;
return { return {
imageToDisplay: intermediateImage ? intermediateImage : currentImage, imageToDisplay: intermediateImage ? intermediateImage : selectedImage,
isIntermediate: Boolean(intermediateImage), isIntermediate: Boolean(intermediateImage),
shouldShowImageDetails, shouldShowImageDetails,
}; };
@ -33,7 +36,7 @@ export const imagesSelector = createSelector(
export default function CurrentImagePreview() { export default function CurrentImagePreview() {
const { shouldShowImageDetails, imageToDisplay, isIntermediate } = const { shouldShowImageDetails, imageToDisplay, isIntermediate } =
useAppSelector(imagesSelector); useAppSelector(imagesSelector);
console.log(imageToDisplay);
return ( return (
<Flex <Flex
sx={{ sx={{
@ -74,7 +77,7 @@ export default function CurrentImagePreview() {
maxHeight: APP_METADATA_HEIGHT, maxHeight: APP_METADATA_HEIGHT,
}} }}
> >
<ImageMetadataViewer image={imageToDisplay} /> {/* <ImageMetadataViewer image={imageToDisplay} /> */}
</Box> </Box>
)} )}
</Flex> </Flex>

View File

@ -9,7 +9,10 @@ import {
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { setCurrentImage } from 'features/gallery/store/gallerySlice'; import {
imageSelected,
setCurrentImage,
} from 'features/gallery/store/gallerySlice';
import { import {
setAllImageToImageParameters, setAllImageToImageParameters,
setAllParameters, setAllParameters,
@ -33,14 +36,14 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
interface HoverableImageProps { interface HoverableImageProps {
image: InvokeAI._Image; image: InvokeAI.Image;
isSelected: boolean; isSelected: boolean;
} }
const memoEqualityCheck = ( const memoEqualityCheck = (
prev: HoverableImageProps, prev: HoverableImageProps,
next: HoverableImageProps next: HoverableImageProps
) => prev.image.uuid === next.image.uuid && prev.isSelected === next.isSelected; ) => prev.image.name === next.image.name && prev.isSelected === next.isSelected;
/** /**
* Gallery image component with delete/use all/use seed buttons on hover. * Gallery image component with delete/use all/use seed buttons on hover.
@ -55,7 +58,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn,
} = useAppSelector(hoverableImageSelector); } = useAppSelector(hoverableImageSelector);
const { image, isSelected } = props; const { image, isSelected } = props;
const { url, thumbnail, uuid, metadata } = image; const { url, thumbnail, name, metadata } = image;
const [isHovered, setIsHovered] = useState<boolean>(false); const [isHovered, setIsHovered] = useState<boolean>(false);
@ -92,7 +95,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleSendToImageToImage = () => { const handleSendToImageToImage = () => {
dispatch(setInitialImage(image)); // dispatch(setInitialImage(image));
if (activeTabName !== 'img2img') { if (activeTabName !== 'img2img') {
dispatch(setActiveTab('img2img')); dispatch(setActiveTab('img2img'));
} }
@ -105,7 +108,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleSendToCanvas = () => { const handleSendToCanvas = () => {
dispatch(setInitialCanvasImage(image)); // dispatch(setInitialCanvasImage(image));
dispatch(resizeAndScaleCanvas()); dispatch(resizeAndScaleCanvas());
@ -155,16 +158,19 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}); });
}; };
const handleSelectImage = () => dispatch(setCurrentImage(image)); const handleSelectImage = () => {
dispatch(imageSelected(image.name));
// dispatch(setCurrentImage(image));
};
const handleDragStart = (e: DragEvent<HTMLDivElement>) => { const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('invokeai/imageUuid', uuid); // e.dataTransfer.setData('invokeai/imageUuid', uuid);
e.dataTransfer.effectAllowed = 'move'; // e.dataTransfer.effectAllowed = 'move';
}; };
const handleLightBox = () => { const handleLightBox = () => {
dispatch(setCurrentImage(image)); // dispatch(setCurrentImage(image));
dispatch(setIsLightboxOpen(true)); // dispatch(setIsLightboxOpen(true));
}; };
return ( return (
@ -209,9 +215,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.sendToUnifiedCanvas')} {t('parameters.sendToUnifiedCanvas')}
</MenuItem> </MenuItem>
<MenuItem data-warning> <MenuItem data-warning>
<DeleteImageModal image={image}> {/* <DeleteImageModal image={image}>
<p>{t('parameters.deleteImage')}</p> <p>{t('parameters.deleteImage')}</p>
</DeleteImageModal> </DeleteImageModal> */}
</MenuItem> </MenuItem>
</MenuList> </MenuList>
)} )}
@ -219,7 +225,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{(ref) => ( {(ref) => (
<Box <Box
position="relative" position="relative"
key={uuid} key={name}
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut} onMouseOut={handleMouseOut}
userSelect="none" userSelect="none"
@ -290,7 +296,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
insetInlineEnd: 1, insetInlineEnd: 1,
}} }}
> >
<DeleteImageModal image={image}> {/* <DeleteImageModal image={image}>
<IAIIconButton <IAIIconButton
aria-label={t('parameters.deleteImage')} aria-label={t('parameters.deleteImage')}
icon={<FaTrashAlt />} icon={<FaTrashAlt />}
@ -298,7 +304,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
fontSize={14} fontSize={14}
isDisabled={!mayDeleteImage} isDisabled={!mayDeleteImage}
/> />
</DeleteImageModal> </DeleteImageModal> */}
</Box> </Box>
)} )}
</Box> </Box>

View File

@ -31,8 +31,8 @@ import {
selectResultsTotal, selectResultsTotal,
} from '../store/resultsSlice'; } from '../store/resultsSlice';
import { import {
getNextResultsPage, receivedResultImagesPage,
getNextUploadsPage, receivedUploadImagesPage,
} from 'services/thunks/gallery'; } from 'services/thunks/gallery';
import { selectUploadsAll, uploadsAdapter } from '../store/uploadsSlice'; import { selectUploadsAll, uploadsAdapter } from '../store/uploadsSlice';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
@ -90,11 +90,11 @@ const ImageGalleryContent = () => {
// }; // };
const handleClickLoadMore = () => { const handleClickLoadMore = () => {
if (currentCategory === 'result') { if (currentCategory === 'result') {
dispatch(getNextResultsPage()); dispatch(receivedResultImagesPage());
} }
if (currentCategory === 'user') { if (currentCategory === 'user') {
dispatch(getNextUploadsPage()); dispatch(receivedUploadImagesPage());
} }
}; };
@ -249,20 +249,17 @@ const ImageGalleryContent = () => {
gap={2} gap={2}
style={{ gridTemplateColumns: galleryGridTemplateColumns }} style={{ gridTemplateColumns: galleryGridTemplateColumns }}
> >
{/* {images.map((image) => { {images.map((image) => {
const { uuid } = image; const { name } = image;
const isSelected = currentImageUuid === uuid; const isSelected = currentImageUuid === name;
return ( return (
<HoverableImage <HoverableImage
key={uuid} key={name}
image={image} image={image}
isSelected={isSelected} isSelected={isSelected}
/> />
); );
})} */} })}
{images.map((image) => (
<Image key={image.name} src={image.thumbnail} />
))}
</Grid> </Grid>
<IAIButton <IAIButton
onClick={handleClickLoadMore} onClick={handleClickLoadMore}

View File

@ -7,6 +7,8 @@ import {
uiSelector, uiSelector,
} from 'features/ui/store/uiSelectors'; } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { selectResultsAll, selectResultsEntities } from './resultsSlice';
import { selectUploadsAll, selectUploadsEntities } from './uploadsSlice';
export const gallerySelector = (state: RootState) => state.gallery; export const gallerySelector = (state: RootState) => state.gallery;
@ -75,3 +77,18 @@ export const hoverableImageSelector = createSelector(
}, },
} }
); );
export const selectedImageSelector = createSelector(
[gallerySelector, selectResultsEntities, selectUploadsEntities],
(gallery, allResults, allUploads) => {
const selectedImageName = gallery.selectedImageName;
if (selectedImageName in allResults) {
return allResults[selectedImageName];
}
if (selectedImageName in allUploads) {
return allUploads[selectedImageName];
}
}
);

View File

@ -1,9 +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 * as InvokeAI from 'app/invokeai'; import * as InvokeAI from 'app/invokeai';
import { invocationComplete } from 'app/nodesSocketio/actions';
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types'; import { IRect } from 'konva/lib/types';
import { clamp } from 'lodash'; import { clamp } from 'lodash';
import { isImageOutput } from 'services/types/guards';
export type GalleryCategory = 'user' | 'result'; export type GalleryCategory = 'user' | 'result';
@ -23,6 +25,7 @@ export type Gallery = {
}; };
export interface GalleryState { export interface GalleryState {
selectedImageName: string;
currentImage?: InvokeAI._Image; currentImage?: InvokeAI._Image;
currentImageUuid: string; currentImageUuid: string;
intermediateImage?: InvokeAI._Image & { intermediateImage?: InvokeAI._Image & {
@ -42,6 +45,7 @@ export interface GalleryState {
} }
const initialState: GalleryState = { const initialState: GalleryState = {
selectedImageName: '',
currentImageUuid: '', currentImageUuid: '',
galleryImageMinimumWidth: 64, galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover', galleryImageObjectFit: 'cover',
@ -69,6 +73,9 @@ export const gallerySlice = createSlice({
name: 'gallery', name: 'gallery',
initialState, initialState,
reducers: { reducers: {
imageSelected: (state, action: PayloadAction<string>) => {
state.selectedImageName = action.payload;
},
setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => { setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
state.currentImage = action.payload; state.currentImage = action.payload;
state.currentImageUuid = action.payload.uuid; state.currentImageUuid = action.payload.uuid;
@ -255,9 +262,19 @@ export const gallerySlice = createSlice({
state.shouldUseSingleGalleryColumn = action.payload; state.shouldUseSingleGalleryColumn = action.payload;
}, },
}, },
extraReducers(builder) {
builder.addCase(invocationComplete, (state, action) => {
const { data } = action.payload;
if (isImageOutput(data.result)) {
state.selectedImageName = data.result.image.image_name;
state.intermediateImage = undefined;
}
});
},
}); });
export const { export const {
imageSelected,
addImage, addImage,
clearIntermediateImage, clearIntermediateImage,
removeImage, removeImage,

View File

@ -1,9 +1,16 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { Image } from 'app/invokeai'; import { Image } from 'app/invokeai';
import { invocationComplete } from 'app/nodesSocketio/actions';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { getNextResultsPage, IMAGES_PER_PAGE } from 'services/thunks/gallery'; import { socketioConnected } from 'features/system/store/systemSlice';
import { processImageField } from 'services/util/processImageField'; import {
receivedResultImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { isImageOutput } from 'services/types/guards';
import { deserializeImageField } from 'services/util/deserializeImageField';
import { setCurrentCategory } from './gallerySlice';
// use `createEntityAdapter` to create a slice for results images // use `createEntityAdapter` to create a slice for results images
// https://redux-toolkit.js.org/api/createEntityAdapter#overview // https://redux-toolkit.js.org/api/createEntityAdapter#overview
@ -47,13 +54,14 @@ const resultsSlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
// here we can respond to a fulfilled call of the `getNextResultsPage` thunk // here we can respond to a fulfilled call of the `getNextResultsPage` thunk
// because we pass in the fulfilled thunk action creator, everything is typed // because we pass in the fulfilled thunk action creator, everything is typed
builder.addCase(getNextResultsPage.pending, (state) => { builder.addCase(receivedResultImagesPage.pending, (state) => {
state.isLoading = true; state.isLoading = true;
}); });
builder.addCase(getNextResultsPage.fulfilled, (state, action) => {
builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload; const { items, page, pages } = action.payload;
const resultImages = items.map((image) => processImageField(image)); const resultImages = items.map((image) => deserializeImageField(image));
// use the adapter reducer to append all the results to state // use the adapter reducer to append all the results to state
resultsAdapter.addMany(state, resultImages); resultsAdapter.addMany(state, resultImages);
@ -63,6 +71,15 @@ const resultsSlice = createSlice({
state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
state.isLoading = false; state.isLoading = false;
}); });
builder.addCase(invocationComplete, (state, action) => {
const { data } = action.payload;
if (isImageOutput(data.result)) {
const resultImage = deserializeImageField(data.result.image);
resultsAdapter.addOne(state, resultImage);
}
});
}, },
}); });

View File

@ -2,8 +2,11 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/invokeai'; import { Image } from 'app/invokeai';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { getNextUploadsPage, IMAGES_PER_PAGE } from 'services/thunks/gallery'; import {
import { processImageField } from 'services/util/processImageField'; receivedUploadImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { deserializeImageField } from 'services/util/deserializeImageField';
export const uploadsAdapter = createEntityAdapter<Image>({ export const uploadsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name, selectId: (image) => image.name,
@ -29,13 +32,13 @@ const uploadsSlice = createSlice({
uploadAdded: uploadsAdapter.addOne, uploadAdded: uploadsAdapter.addOne,
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getNextUploadsPage.pending, (state) => { builder.addCase(receivedUploadImagesPage.pending, (state) => {
state.isLoading = true; state.isLoading = true;
}); });
builder.addCase(getNextUploadsPage.fulfilled, (state, action) => { builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload; const { items, page, pages } = action.payload;
const images = items.map((image) => processImageField(image)); const images = items.map((image) => deserializeImageField(image));
uploadsAdapter.addMany(state, images); uploadsAdapter.addMany(state, images);

View File

@ -2,7 +2,12 @@ import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai'; import * as InvokeAI from 'app/invokeai';
import { invocationComplete } from 'app/nodesSocketio/actions';
import { resultAdded } from 'features/gallery/store/resultsSlice';
import dateFormat from 'dateformat';
import i18n from 'i18n'; import i18n from 'i18n';
import { isImageOutput } from 'services/types/guards';
export type LogLevel = 'info' | 'warning' | 'error'; export type LogLevel = 'info' | 'warning' | 'error';
@ -271,6 +276,29 @@ export const systemSlice = createSlice({
setCancelAfter: (state, action: PayloadAction<number | null>) => { setCancelAfter: (state, action: PayloadAction<number | null>) => {
state.cancelOptions.cancelAfter = action.payload; state.cancelOptions.cancelAfter = action.payload;
}, },
socketioConnected: (state) => {
state.isConnected = true;
state.currentStatus = i18n.t('common.statusConnected');
},
socketioDisconnected: (state) => {
state.isConnected = false;
state.currentStatus = i18n.t('common.statusDisconnected');
},
},
extraReducers(builder) {
builder.addCase(invocationComplete, (state, action) => {
const { data, timestamp } = action.payload;
state.isProcessing = false;
state.isCancelable = false;
if (isImageOutput(data.result)) {
state.log.push({
timestamp: dateFormat(timestamp, 'isoDateTime'),
message: `Generated: ${data.result.image.image_name}`,
level: 'info',
});
}
});
}, },
}); });
@ -306,6 +334,8 @@ export const {
setOpenModel, setOpenModel,
setCancelType, setCancelType,
setCancelAfter, setCancelAfter,
socketioConnected,
socketioDisconnected,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
import { ProgressImage } from './events/types'; import { ProgressImage } from './events/types';
import { createSession, invokeSession } from 'services/thunks/session'; import { createSession, invokeSession } from 'services/thunks/session';
import { getImage, uploadImage } from './thunks/image'; import { getImage, uploadImage } from './thunks/image';
import { invocationComplete } from 'app/nodesSocketio/actions';
/** /**
* Just temp until we work out better statuses * Just temp until we work out better statuses
@ -17,14 +18,14 @@ export enum STATUS {
* Type for the temp (?) API slice. * Type for the temp (?) API slice.
*/ */
export interface APIState { export interface APIState {
sessionId: string | null; sessionId: string;
progressImage: ProgressImage | null; progressImage: ProgressImage | null;
progress: number | null; progress: number | null;
status: STATUS; status: STATUS;
} }
const initialSystemState: APIState = { const initialSystemState: APIState = {
sessionId: null, sessionId: '',
status: STATUS.idle, status: STATUS.idle,
progress: null, progress: null,
progressImage: null, progressImage: null,
@ -106,6 +107,9 @@ export const apiSlice = createSlice({
// !HTTP 200 // !HTTP 200
// state.networkStatus = 'idle' // state.networkStatus = 'idle'
}); });
builder.addCase(invocationComplete, (state) => {
state.sessionId = '';
});
}, },
}); });

View File

@ -3,8 +3,8 @@ import { ImagesService } from 'services/api';
export const IMAGES_PER_PAGE = 20; export const IMAGES_PER_PAGE = 20;
export const getNextResultsPage = createAppAsyncThunk( export const receivedResultImagesPage = createAppAsyncThunk(
'results/getInitialResultsPage', 'results/receivedResultImagesPage',
async (_arg, { getState }) => { async (_arg, { getState }) => {
const response = await ImagesService.listImages({ const response = await ImagesService.listImages({
imageType: 'results', imageType: 'results',
@ -16,8 +16,8 @@ export const getNextResultsPage = createAppAsyncThunk(
} }
); );
export const getNextUploadsPage = createAppAsyncThunk( export const receivedUploadImagesPage = createAppAsyncThunk(
'uploads/getNextUploadsPage', 'uploads/receivedUploadImagesPage',
async (_arg, { getState }) => { async (_arg, { getState }) => {
const response = await ImagesService.listImages({ const response = await ImagesService.listImages({
imageType: 'uploads', imageType: 'uploads',

View File

@ -27,7 +27,7 @@ export const extractTimestampFromImageName = (imageName: string) => {
return Number(timestamp); return Number(timestamp);
}; };
export const processImageField = (image: ImageField): Image => { export const deserializeImageField = (image: ImageField): Image => {
const name = image.image_name; const name = image.image_name;
const type = image.image_type; const type = image.image_type;