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:
psychedelicious 2022-11-19 14:04:29 +11:00 committed by blessedcoolant
parent d82a21cfb2
commit bc46c46835
9 changed files with 219 additions and 277 deletions

View File

@ -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,
})
);
};

View File

@ -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;

View File

@ -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);
}
});
};

View File

@ -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;
};

View File

@ -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,
})
);
}
};

View File

@ -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,
};
}
);

View File

@ -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 {

View File

@ -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));
}
};

View File

@ -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 {