mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Refactors upload-related async thunks
- Now standard thunks instead of RTK createAsyncThunk() - Adds toasts for all canvas upload-related actions
This commit is contained in:
parent
d82a21cfb2
commit
bc46c46835
@ -25,7 +25,7 @@ import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||
import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
|
||||
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
||||
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
||||
import { mergeAndUploadCanvas } from 'features/canvas/util/mergeAndUploadCanvas';
|
||||
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
||||
import {
|
||||
canvasSelector,
|
||||
isStagingSelector,
|
||||
@ -151,14 +151,19 @@ const IAICanvasOutpaintingControls = () => {
|
||||
};
|
||||
|
||||
const handleMergeVisible = () => {
|
||||
dispatch(mergeAndUploadCanvas({}));
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: false,
|
||||
shouldSetAsInitialImage: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveToGallery = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: true,
|
||||
saveToGallery: true,
|
||||
shouldSaveToGallery: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -167,7 +172,7 @@ const IAICanvasOutpaintingControls = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: true,
|
||||
copyAfterSaving: true,
|
||||
shouldCopy: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -176,7 +181,7 @@ const IAICanvasOutpaintingControls = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: true,
|
||||
downloadAfterSaving: true,
|
||||
shouldDownload: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -8,12 +8,11 @@ import {
|
||||
roundDownToMultiple,
|
||||
roundToMultiple,
|
||||
} from 'common/util/roundDownToMultiple';
|
||||
import { canvasExtraReducers } from './reducers/extraReducers';
|
||||
import { setInitialCanvasImage as setInitialCanvasImage_reducer } from './reducers/setInitialCanvasImage';
|
||||
import calculateScale from '../util/calculateScale';
|
||||
import calculateCoordinates from '../util/calculateCoordinates';
|
||||
import floorCoordinates from '../util/floorCoordinates';
|
||||
import {
|
||||
CanvasImage,
|
||||
CanvasLayer,
|
||||
CanvasLayerState,
|
||||
CanvasState,
|
||||
@ -152,7 +151,45 @@ export const canvasSlice = createSlice({
|
||||
state.cursorPosition = action.payload;
|
||||
},
|
||||
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
setInitialCanvasImage_reducer(state, action.payload);
|
||||
const image = action.payload;
|
||||
const newBoundingBoxDimensions = {
|
||||
width: roundDownToMultiple(_.clamp(image.width, 64, 512), 64),
|
||||
height: roundDownToMultiple(_.clamp(image.height, 64, 512), 64),
|
||||
};
|
||||
|
||||
const newBoundingBoxCoordinates = {
|
||||
x: roundToMultiple(
|
||||
image.width / 2 - newBoundingBoxDimensions.width / 2,
|
||||
64
|
||||
),
|
||||
y: roundToMultiple(
|
||||
image.height / 2 - newBoundingBoxDimensions.height / 2,
|
||||
64
|
||||
),
|
||||
};
|
||||
|
||||
state.boundingBoxDimensions = newBoundingBoxDimensions;
|
||||
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
|
||||
|
||||
state.pastLayerStates.push(state.layerState);
|
||||
state.layerState = {
|
||||
...initialLayerState,
|
||||
objects: [
|
||||
{
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
image: image,
|
||||
},
|
||||
],
|
||||
};
|
||||
state.futureLayerStates = [];
|
||||
|
||||
state.isCanvasInitialized = false;
|
||||
state.doesCanvasNeedScaling = true;
|
||||
},
|
||||
setStageDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||
state.stageDimensions = action.payload;
|
||||
@ -599,8 +636,16 @@ export const canvasSlice = createSlice({
|
||||
setShouldShowStagingImage: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowStagingImage = action.payload;
|
||||
},
|
||||
setMergedCanvas: (state, action: PayloadAction<CanvasImage>) => {
|
||||
state.pastLayerStates.push({
|
||||
...state.layerState,
|
||||
});
|
||||
|
||||
state.futureLayerStates = [];
|
||||
|
||||
state.layerState.objects = [action.payload];
|
||||
},
|
||||
},
|
||||
extraReducers: canvasExtraReducers,
|
||||
});
|
||||
|
||||
export const {
|
||||
@ -659,6 +704,7 @@ export const {
|
||||
setCanvasContainerDimensions,
|
||||
fitBoundingBoxToStage,
|
||||
setShouldShowStagingImage,
|
||||
setMergedCanvas,
|
||||
} = canvasSlice.actions;
|
||||
|
||||
export default canvasSlice.reducer;
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { CanvasState } from '../canvasTypes';
|
||||
import _ from 'lodash';
|
||||
import { mergeAndUploadCanvas } from '../../util/mergeAndUploadCanvas';
|
||||
import { uploadImage } from 'features/gallery/util/uploadImage';
|
||||
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
|
||||
import { setInitialCanvasImage } from './setInitialCanvasImage';
|
||||
|
||||
export const canvasExtraReducers = (
|
||||
builder: ActionReducerMapBuilder<CanvasState>
|
||||
) => {
|
||||
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
|
||||
if (!action.payload) return;
|
||||
const { image, kind, originalBoundingBox } = action.payload;
|
||||
|
||||
if (kind === 'temp_merged_canvas') {
|
||||
state.pastLayerStates.push({
|
||||
...state.layerState,
|
||||
});
|
||||
|
||||
state.futureLayerStates = [];
|
||||
|
||||
state.layerState.objects = [
|
||||
{
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
...originalBoundingBox,
|
||||
image,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||
if (!action.payload) return;
|
||||
const { image, kind, activeTabName } = action.payload;
|
||||
|
||||
if (kind !== 'init') return;
|
||||
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
setInitialCanvasImage(state, image);
|
||||
}
|
||||
});
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
import { initialLayerState } from '../canvasSlice';
|
||||
import { CanvasState } from '../canvasTypes';
|
||||
import {
|
||||
roundDownToMultiple,
|
||||
roundToMultiple,
|
||||
} from 'common/util/roundDownToMultiple';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const setInitialCanvasImage = (
|
||||
state: CanvasState,
|
||||
image: InvokeAI.Image
|
||||
) => {
|
||||
const newBoundingBoxDimensions = {
|
||||
width: roundDownToMultiple(_.clamp(image.width, 64, 512), 64),
|
||||
height: roundDownToMultiple(_.clamp(image.height, 64, 512), 64),
|
||||
};
|
||||
|
||||
const newBoundingBoxCoordinates = {
|
||||
x: roundToMultiple(
|
||||
image.width / 2 - newBoundingBoxDimensions.width / 2,
|
||||
64
|
||||
),
|
||||
y: roundToMultiple(
|
||||
image.height / 2 - newBoundingBoxDimensions.height / 2,
|
||||
64
|
||||
),
|
||||
};
|
||||
|
||||
state.boundingBoxDimensions = newBoundingBoxDimensions;
|
||||
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
|
||||
|
||||
state.pastLayerStates.push(state.layerState);
|
||||
state.layerState = {
|
||||
...initialLayerState,
|
||||
objects: [
|
||||
{
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
image: image,
|
||||
},
|
||||
],
|
||||
};
|
||||
state.futureLayerStates = [];
|
||||
|
||||
state.isCanvasInitialized = false;
|
||||
state.doesCanvasNeedScaling = true;
|
||||
};
|
@ -0,0 +1,138 @@
|
||||
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import layerToDataURL from '../../util/layerToDataURL';
|
||||
import downloadFile from '../../util/downloadFile';
|
||||
import copyImage from '../../util/copyImage';
|
||||
import { getCanvasBaseLayer } from '../../util/konvaInstanceProvider';
|
||||
import { addToast } from 'features/system/systemSlice';
|
||||
import { addImage } from 'features/gallery/gallerySlice';
|
||||
import { setMergedCanvas } from '../canvasSlice';
|
||||
|
||||
type MergeAndUploadCanvasConfig = {
|
||||
cropVisible?: boolean;
|
||||
shouldSaveToGallery?: boolean;
|
||||
shouldDownload?: boolean;
|
||||
shouldCopy?: boolean;
|
||||
shouldSetAsInitialImage?: boolean;
|
||||
};
|
||||
|
||||
const defaultConfig: MergeAndUploadCanvasConfig = {
|
||||
cropVisible: false,
|
||||
shouldSaveToGallery: false,
|
||||
shouldDownload: false,
|
||||
shouldCopy: false,
|
||||
shouldSetAsInitialImage: true,
|
||||
};
|
||||
|
||||
export const mergeAndUploadCanvas =
|
||||
(config = defaultConfig): ThunkAction<void, RootState, unknown, AnyAction> =>
|
||||
async (dispatch, getState) => {
|
||||
const {
|
||||
cropVisible,
|
||||
shouldSaveToGallery,
|
||||
shouldDownload,
|
||||
shouldCopy,
|
||||
shouldSetAsInitialImage,
|
||||
} = config;
|
||||
|
||||
const state = getState() as RootState;
|
||||
|
||||
const stageScale = state.canvas.stageScale;
|
||||
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) return;
|
||||
|
||||
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
|
||||
canvasBaseLayer,
|
||||
stageScale
|
||||
);
|
||||
|
||||
if (!dataURL) return;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
dataURL,
|
||||
filename: 'merged_canvas.png',
|
||||
kind: shouldSaveToGallery ? 'result' : 'temp',
|
||||
cropVisible,
|
||||
})
|
||||
);
|
||||
|
||||
const response = await fetch(window.location.origin + '/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const { url, mtime, width, height } =
|
||||
(await response.json()) as InvokeAI.ImageUploadResponse;
|
||||
|
||||
const newImage: InvokeAI.Image = {
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
mtime,
|
||||
category: shouldSaveToGallery ? 'result' : 'user',
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
if (shouldDownload) {
|
||||
downloadFile(url);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Download Started',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldCopy) {
|
||||
copyImage(url, width, height);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Copied',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldSaveToGallery) {
|
||||
dispatch(addImage({ image: newImage, category: 'result' }));
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Saved to Gallery',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldSetAsInitialImage) {
|
||||
dispatch(
|
||||
setMergedCanvas({
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
...originalBoundingBox,
|
||||
image: newImage,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Canvas Merged',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
@ -1,103 +0,0 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import layerToDataURL from './layerToDataURL';
|
||||
import downloadFile from './downloadFile';
|
||||
import copyImage from './copyImage';
|
||||
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||
import { addToast } from 'features/system/systemSlice';
|
||||
|
||||
export const mergeAndUploadCanvas = createAsyncThunk(
|
||||
'canvas/mergeAndUploadCanvas',
|
||||
async (
|
||||
args: {
|
||||
cropVisible?: boolean;
|
||||
saveToGallery?: boolean;
|
||||
downloadAfterSaving?: boolean;
|
||||
copyAfterSaving?: boolean;
|
||||
},
|
||||
thunkAPI
|
||||
) => {
|
||||
const { saveToGallery, downloadAfterSaving, cropVisible, copyAfterSaving } =
|
||||
args;
|
||||
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
|
||||
const state = getState() as RootState;
|
||||
|
||||
const stageScale = state.canvas.stageScale;
|
||||
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) return;
|
||||
|
||||
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
|
||||
canvasBaseLayer,
|
||||
stageScale
|
||||
);
|
||||
|
||||
if (!dataURL) return;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
dataURL,
|
||||
filename: 'merged_canvas.png',
|
||||
kind: saveToGallery ? 'result' : 'temp',
|
||||
cropVisible,
|
||||
})
|
||||
);
|
||||
|
||||
const response = await fetch(window.location.origin + '/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const { url, mtime, width, height } =
|
||||
(await response.json()) as InvokeAI.ImageUploadResponse;
|
||||
|
||||
if (downloadAfterSaving) {
|
||||
downloadFile(url);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Download Started',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (copyAfterSaving) {
|
||||
copyImage(url, width, height);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Copied',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newImage: InvokeAI.Image = {
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
mtime,
|
||||
category: saveToGallery ? 'result' : 'user',
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
return {
|
||||
image: newImage,
|
||||
kind: saveToGallery ? 'merged_canvas' : 'temp_merged_canvas',
|
||||
originalBoundingBox,
|
||||
};
|
||||
}
|
||||
);
|
@ -4,8 +4,6 @@ import _, { clamp } from 'lodash';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
||||
import { mergeAndUploadCanvas } from 'features/canvas/util/mergeAndUploadCanvas';
|
||||
import { uploadImage } from './util/uploadImage';
|
||||
|
||||
export type GalleryCategory = 'user' | 'result';
|
||||
|
||||
@ -266,46 +264,6 @@ export const gallerySlice = createSlice({
|
||||
state.galleryWidth = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
|
||||
if (!action.payload) return;
|
||||
const { image, kind, originalBoundingBox } = action.payload;
|
||||
|
||||
if (kind === 'merged_canvas') {
|
||||
const { uuid, url, mtime } = image;
|
||||
|
||||
state.categories.result.images.unshift(image);
|
||||
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.currentImageUuid = uuid;
|
||||
state.currentImage = image;
|
||||
state.currentCategory = 'result';
|
||||
}
|
||||
|
||||
state.intermediateImage = undefined;
|
||||
state.categories.result.latest_mtime = mtime;
|
||||
}
|
||||
});
|
||||
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||
if (!action.payload) return;
|
||||
const { image, kind } = action.payload;
|
||||
|
||||
if (kind === 'init') {
|
||||
const { uuid, mtime } = image;
|
||||
|
||||
state.categories.result.images.unshift(image);
|
||||
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.currentImageUuid = uuid;
|
||||
state.currentImage = image;
|
||||
state.currentCategory = 'user';
|
||||
}
|
||||
|
||||
state.intermediateImage = undefined;
|
||||
state.categories.result.latest_mtime = mtime;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { setInitialImage } from 'features/options/optionsSlice';
|
||||
import { addImage } from '../gallerySlice';
|
||||
|
||||
export const uploadImage = createAsyncThunk(
|
||||
'gallery/uploadImage',
|
||||
async (
|
||||
args: {
|
||||
imageFile: File;
|
||||
},
|
||||
thunkAPI
|
||||
) => {
|
||||
const { imageFile } = args;
|
||||
type UploadImageConfig = {
|
||||
imageFile: File;
|
||||
};
|
||||
|
||||
const { getState } = thunkAPI;
|
||||
export const uploadImage =
|
||||
(
|
||||
config: UploadImageConfig
|
||||
): ThunkAction<void, RootState, unknown, AnyAction> =>
|
||||
async (dispatch, getState) => {
|
||||
const { imageFile } = config;
|
||||
|
||||
const state = getState() as RootState;
|
||||
|
||||
@ -47,10 +49,11 @@ export const uploadImage = createAsyncThunk(
|
||||
height: height,
|
||||
};
|
||||
|
||||
return {
|
||||
image: newImage,
|
||||
kind: 'init',
|
||||
activeTabName,
|
||||
};
|
||||
}
|
||||
);
|
||||
dispatch(addImage({ image: newImage, category: 'user' }));
|
||||
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
dispatch(setInitialCanvasImage(newImage));
|
||||
} else if (activeTabName === 'img2img') {
|
||||
dispatch(setInitialImage(newImage));
|
||||
}
|
||||
};
|
||||
|
@ -5,7 +5,6 @@ import promptToString from 'common/util/promptToString';
|
||||
import { seedWeightsToString } from 'common/util/seedWeightPairs';
|
||||
import { FACETOOL_TYPES } from 'app/constants';
|
||||
import { InvokeTabName, tabMap } from 'features/tabs/InvokeTabs';
|
||||
import { uploadImage } from 'features/gallery/util/uploadImage';
|
||||
|
||||
export type UpscalingLevel = 2 | 4;
|
||||
|
||||
@ -362,16 +361,6 @@ export const optionsSlice = createSlice({
|
||||
state.isLightBoxOpen = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||
if (!action.payload) return;
|
||||
const { image, kind, activeTabName } = action.payload;
|
||||
|
||||
if (kind === 'init' && activeTabName === 'img2img') {
|
||||
state.initialImage = image;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
Loading…
Reference in New Issue
Block a user