mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
fix(ui): image uploading handling
Rework uploadImage and uploadImages helpers and the RTK listener, ensuring gallery view isn't changed unexpectedly and preventing extraneous toasts. Fix staging area save to gallery button to essentially make a copy of the image, instead of changing its intermediate status.
This commit is contained in:
committed by
Kent Keirsey
parent
d5ff7ef250
commit
878093f64e
@@ -1,17 +1,19 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
import { useAddImagesToBoardMutation, useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images';
|
||||
import { uploadImage } from 'services/api/endpoints/images';
|
||||
|
||||
const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY';
|
||||
|
||||
export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const selectedImage = useAppSelector(selectSelectedImage);
|
||||
const [addImageToBoard] = useAddImagesToBoardMutation();
|
||||
const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -19,21 +21,42 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
if (!selectedImage) {
|
||||
return;
|
||||
}
|
||||
if (autoAddBoardId !== 'none') {
|
||||
await addImageToBoard({ imageDTOs: [selectedImage.imageDTO], board_id: autoAddBoardId }).unwrap();
|
||||
// The changeIsImageIntermediate request will use the board_id on this specific imageDTO object, so we need to
|
||||
// update it before making the request - else the optimistic board updates will get out of whack.
|
||||
changeIsImageIntermediate({
|
||||
imageDTO: { ...selectedImage.imageDTO, board_id: autoAddBoardId },
|
||||
|
||||
// To save the image to gallery, we will download it and re-upload it. This allows the user to delete the image
|
||||
// the gallery without borking the canvas, which may need this image to exist.
|
||||
const result = await withResultAsync(async () => {
|
||||
// Download the image
|
||||
const res = await fetch(selectedImage.imageDTO.image_url);
|
||||
const blob = await res.blob();
|
||||
// Create a new file with the same name, which we will upload
|
||||
const file = new File([blob], `copy_of_${selectedImage.imageDTO.image_name}`, { type: 'image/png' });
|
||||
|
||||
await uploadImage({
|
||||
file,
|
||||
// Image should show up in the Images tab
|
||||
image_category: 'general',
|
||||
is_intermediate: false,
|
||||
// TODO(psyche): Maybe this should just save to the currently-selected board?
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
// We will do our own toast - opt out of the default handling
|
||||
silent: true,
|
||||
});
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
toast({
|
||||
id: TOAST_ID,
|
||||
title: t('controlLayers.savedToGalleryOk'),
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
changeIsImageIntermediate({
|
||||
imageDTO: selectedImage.imageDTO,
|
||||
is_intermediate: false,
|
||||
toast({
|
||||
id: TOAST_ID,
|
||||
title: t('controlLayers.savedToGalleryError'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, [addImageToBoard, autoAddBoardId, changeIsImageIntermediate, selectedImage]);
|
||||
}, [autoAddBoardId, selectedImage, t]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
@@ -42,7 +65,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
icon={<PiFloppyDiskBold />}
|
||||
onClick={saveSelectedImageToGallery}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
|
||||
isDisabled={!selectedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ import type {
|
||||
RegionalGuidanceReferenceImageState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -70,6 +72,11 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
|
||||
metadata = selectCanvasMetadata(store.getState());
|
||||
}
|
||||
|
||||
let boardId: BoardId | undefined = undefined;
|
||||
if (saveToGallery) {
|
||||
boardId = selectAutoAddBoardId(store.getState());
|
||||
}
|
||||
|
||||
const result = await withResultAsync(() => {
|
||||
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
return canvasManager.compositor.getCompositeImageDTO(
|
||||
@@ -78,6 +85,8 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
|
||||
{
|
||||
is_intermediate: !saveToGallery,
|
||||
metadata,
|
||||
board_id: boardId,
|
||||
silent: true,
|
||||
},
|
||||
undefined,
|
||||
true // force upload the image to ensure it gets added to the gallery
|
||||
@@ -222,8 +231,8 @@ export const useNewRasterLayerFromBbox = () => {
|
||||
toastError: t('controlLayers.newRasterLayerError'),
|
||||
};
|
||||
}, [dispatch, t]);
|
||||
const newRasterLayerFromBbox = useSaveCanvas(arg);
|
||||
return newRasterLayerFromBbox;
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewControlLayerFromBbox = () => {
|
||||
|
||||
@@ -28,19 +28,17 @@ import type {
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import type { UploadImageArg } from 'services/api/endpoints/images';
|
||||
import { getImageDTOSafe, uploadImage } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { ImageDTO, UploadImageArg } from 'services/api/types';
|
||||
import stableHash from 'stable-hash';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
import type { JsonObject, SetOptional } from 'type-fest';
|
||||
|
||||
type CompositingOptions = {
|
||||
/**
|
||||
@@ -259,7 +257,7 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
getCompositeImageDTO = async (
|
||||
adapters: CanvasEntityAdapter[],
|
||||
rect: Rect,
|
||||
uploadOptions: Pick<UploadImageArg, 'is_intermediate' | 'metadata'>,
|
||||
uploadOptions: SetOptional<Omit<UploadImageArg, 'file'>, 'image_category'>,
|
||||
compositingOptions?: CompositingOptions,
|
||||
forceUpload?: boolean
|
||||
): Promise<ImageDTO> => {
|
||||
@@ -299,10 +297,7 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
uploadImage({
|
||||
file: new File([blob], 'canvas-composite.png', { type: 'image/png' }),
|
||||
image_category: 'general',
|
||||
is_intermediate: uploadOptions.is_intermediate,
|
||||
board_id: uploadOptions.is_intermediate ? undefined : selectAutoAddBoardId(this.manager.store.getState()),
|
||||
metadata: uploadOptions.metadata,
|
||||
withToast: false,
|
||||
...uploadOptions,
|
||||
})
|
||||
);
|
||||
this.$isUploading.set(false);
|
||||
|
||||
@@ -493,7 +493,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
file: new File([blob], `${this.id}_rasterized.png`, { type: 'image/png' }),
|
||||
image_category: 'other',
|
||||
is_intermediate: true,
|
||||
withToast: false,
|
||||
silent: true,
|
||||
});
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
if (replaceObjects) {
|
||||
|
||||
@@ -13,8 +13,9 @@ import { selectMaxImageUploadCount } from 'features/system/store/configSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type UploadImageArg, uploadImages } from 'services/api/endpoints/images';
|
||||
import { uploadImages } from 'services/api/endpoints/images';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
import type { UploadImageArg } from 'services/api/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpg', 'image/jpeg'];
|
||||
@@ -94,11 +95,12 @@ export const FullscreenDropzone = memo(() => {
|
||||
}
|
||||
const autoAddBoardId = selectAutoAddBoardId(getState());
|
||||
|
||||
const uploadArgs: UploadImageArg[] = files.map((file) => ({
|
||||
const uploadArgs: UploadImageArg[] = files.map((file, i) => ({
|
||||
file,
|
||||
image_category: 'user',
|
||||
is_intermediate: false,
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
isFirstUploadOfBatch: i === 0,
|
||||
}));
|
||||
|
||||
uploadImages(uploadArgs);
|
||||
|
||||
@@ -36,7 +36,10 @@ export const addImageToImage = async ({
|
||||
}: AddImageToImageArg): Promise<Invocation<'img_resize' | 'l2i' | 'flux_vae_decode' | 'sd3_l2i'>> => {
|
||||
denoise.denoising_start = denoising_start;
|
||||
const adapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const { image_name } = await manager.compositor.getCompositeImageDTO(adapters, bbox.rect, { is_intermediate: true });
|
||||
const { image_name } = await manager.compositor.getCompositeImageDTO(adapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
if (!isEqual(scaledSize, originalSize)) {
|
||||
// Resize the initial image to the scaled size, denoise, then resize back to the original size
|
||||
|
||||
@@ -51,11 +51,13 @@ export const addInpaint = async ({
|
||||
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
|
||||
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
if (!isEqual(scaledSize, originalSize)) {
|
||||
|
||||
@@ -52,11 +52,13 @@ export const addOutpaint = async ({
|
||||
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
|
||||
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const infill = getInfill(g, params);
|
||||
|
||||
Reference in New Issue
Block a user