feat(ui): fix canvas saving

- fix "bounding box region only" not being respected when saving
- add toasts for each action
- improve workflow `take()` predicates to use the requestId
This commit is contained in:
psychedelicious 2023-06-06 11:00:15 +10:00
parent cc22427f25
commit 229de2dbb8
7 changed files with 80 additions and 36 deletions

View File

@ -28,6 +28,13 @@ export const addCanvasCopiedToClipboardListener = () => {
} }
copyBlobToClipboard(blob); copyBlobToClipboard(blob);
dispatch(
addToast({
title: 'Canvas Copied to Clipboard',
status: 'success',
})
);
}, },
}); });
}; };

View File

@ -27,7 +27,8 @@ export const addCanvasDownloadedAsImageListener = () => {
return; return;
} }
downloadBlob(blob, 'mergedCanvas.png'); downloadBlob(blob, 'canvas.png');
dispatch(addToast({ title: 'Canvas Downloaded', status: 'success' }));
}, },
}); });
}; };

View File

@ -1,22 +1,20 @@
import { canvasMerged } from 'features/canvas/store/actions'; import { canvasMerged } from 'features/canvas/store/actions';
import { startAppListening } from '..'; import { startAppListening } from '..';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
import { v4 as uuidv4 } from 'uuid';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice'; import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' }); const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
export const MERGED_CANVAS_FILENAME = 'mergedCanvas.png';
export const addCanvasMergedListener = () => { export const addCanvasMergedListener = () => {
startAppListening({ startAppListening({
actionCreator: canvasMerged, actionCreator: canvasMerged,
effect: async (action, { dispatch, getState, take }) => { effect: async (action, { dispatch, getState, take }) => {
const state = getState(); const blob = await getFullBaseLayerBlob();
const blob = await getBaseLayerBlob(state, true);
if (!blob) { if (!blob) {
moduleLog.error('Problem getting base layer blob'); moduleLog.error('Problem getting base layer blob');
@ -48,12 +46,12 @@ export const addCanvasMergedListener = () => {
relativeTo: canvasBaseLayer.getParent(), relativeTo: canvasBaseLayer.getParent(),
}); });
const filename = `mergedCanvas_${uuidv4()}.png`; const imageUploadedRequest = dispatch(
dispatch(
imageUploaded({ imageUploaded({
formData: { formData: {
file: new File([blob], filename, { type: 'image/png' }), file: new File([blob], MERGED_CANVAS_FILENAME, {
type: 'image/png',
}),
}, },
imageCategory: 'general', imageCategory: 'general',
isIntermediate: true, isIntermediate: true,
@ -61,9 +59,11 @@ export const addCanvasMergedListener = () => {
); );
const [{ payload }] = await take( const [{ payload }] = await take(
(action): action is ReturnType<typeof imageUploaded.fulfilled> => (
imageUploaded.fulfilled.match(action) && uploadedImageAction
action.meta.arg.formData.file.name === filename ): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(uploadedImageAction) &&
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
); );
const mergedCanvasImage = payload; const mergedCanvasImage = payload;

View File

@ -4,9 +4,10 @@ import { log } from 'app/logging/useLogger';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { v4 as uuidv4 } from 'uuid';
import { imageUpserted } from 'features/gallery/store/imagesSlice'; import { imageUpserted } from 'features/gallery/store/imagesSlice';
export const SAVED_CANVAS_FILENAME = 'savedCanvas.png';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' }); const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasSavedToGalleryListener = () => { export const addCanvasSavedToGalleryListener = () => {
@ -15,7 +16,7 @@ export const addCanvasSavedToGalleryListener = () => {
effect: async (action, { dispatch, getState, take }) => { effect: async (action, { dispatch, getState, take }) => {
const state = getState(); const state = getState();
const blob = await getBaseLayerBlob(state, true); const blob = await getBaseLayerBlob(state);
if (!blob) { if (!blob) {
moduleLog.error('Problem getting base layer blob'); moduleLog.error('Problem getting base layer blob');
@ -29,12 +30,12 @@ export const addCanvasSavedToGalleryListener = () => {
return; return;
} }
const filename = `mergedCanvas_${uuidv4()}.png`; const imageUploadedRequest = dispatch(
dispatch(
imageUploaded({ imageUploaded({
formData: { formData: {
file: new File([blob], filename, { type: 'image/png' }), file: new File([blob], SAVED_CANVAS_FILENAME, {
type: 'image/png',
}),
}, },
imageCategory: 'general', imageCategory: 'general',
isIntermediate: false, isIntermediate: false,
@ -42,9 +43,11 @@ export const addCanvasSavedToGalleryListener = () => {
); );
const [{ payload: uploadedImageDTO }] = await take( const [{ payload: uploadedImageDTO }] = await take(
(action): action is ReturnType<typeof imageUploaded.fulfilled> => (
imageUploaded.fulfilled.match(action) && uploadedImageAction
action.meta.arg.formData.file.name === filename ): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(uploadedImageAction) &&
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
); );
dispatch(imageUpserted(uploadedImageDTO)); dispatch(imageUpserted(uploadedImageDTO));

View File

@ -3,6 +3,8 @@ import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { imageUpserted } from 'features/gallery/store/imagesSlice'; import { imageUpserted } from 'features/gallery/store/imagesSlice';
import { SAVED_CANVAS_FILENAME } from './canvasSavedToGallery';
import { MERGED_CANVAS_FILENAME } from './canvasMerged';
const moduleLog = log.child({ namespace: 'image' }); const moduleLog = log.child({ namespace: 'image' });
@ -19,9 +21,22 @@ export const addImageUploadedFulfilledListener = () => {
return; return;
} }
const state = getState(); const originalFileName = action.meta.arg.formData.file.name;
dispatch(imageUpserted(image)); dispatch(imageUpserted(image));
if (originalFileName === SAVED_CANVAS_FILENAME) {
dispatch(
addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
);
return;
}
if (originalFileName === MERGED_CANVAS_FILENAME) {
dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
return;
}
dispatch(addToast({ title: 'Image Uploaded', status: 'success' })); dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
}, },
}); });

View File

@ -2,10 +2,10 @@ import { getCanvasBaseLayer } from './konvaInstanceProvider';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { konvaNodeToBlob } from './konvaNodeToBlob'; import { konvaNodeToBlob } from './konvaNodeToBlob';
export const getBaseLayerBlob = async ( /**
state: RootState, * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave`
withoutBoundingBox?: boolean */
) => { export const getBaseLayerBlob = async (state: RootState) => {
const canvasBaseLayer = getCanvasBaseLayer(); const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) { if (!canvasBaseLayer) {
@ -24,15 +24,14 @@ export const getBaseLayerBlob = async (
const absPos = clonedBaseLayer.getAbsolutePosition(); const absPos = clonedBaseLayer.getAbsolutePosition();
const boundingBox = const boundingBox = shouldCropToBoundingBoxOnSave
shouldCropToBoundingBoxOnSave && !withoutBoundingBox ? {
? { x: boundingBoxCoordinates.x + absPos.x,
x: boundingBoxCoordinates.x + absPos.x, y: boundingBoxCoordinates.y + absPos.y,
y: boundingBoxCoordinates.y + absPos.y, width: boundingBoxDimensions.width,
width: boundingBoxDimensions.width, height: boundingBoxDimensions.height,
height: boundingBoxDimensions.height, }
} : clonedBaseLayer.getClientRect();
: clonedBaseLayer.getClientRect();
return konvaNodeToBlob(clonedBaseLayer, boundingBox); return konvaNodeToBlob(clonedBaseLayer, boundingBox);
}; };

View File

@ -0,0 +1,19 @@
import { getCanvasBaseLayer } from './konvaInstanceProvider';
import { konvaNodeToBlob } from './konvaNodeToBlob';
/**
* Gets the canvas base layer blob, without bounding box
*/
export const getFullBaseLayerBlob = async () => {
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) {
return;
}
const clonedBaseLayer = canvasBaseLayer.clone();
clonedBaseLayer.scale({ x: 1, y: 1 });
return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect());
};