mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): more staging fixes
This commit is contained in:
@@ -18,7 +18,7 @@ const sx = {
|
||||
h: 108,
|
||||
w: 108,
|
||||
flexShrink: 0,
|
||||
borderWidth: 1,
|
||||
borderWidth: 2,
|
||||
borderRadius: 'base',
|
||||
'&[data-selected="true"]': {
|
||||
borderColor: 'invokeBlue.300',
|
||||
@@ -46,8 +46,10 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
ctx.$lastLoadedItemId.set(item.item_id);
|
||||
}, [ctx.$lastLoadedItemId, item.item_id]);
|
||||
if (ctx.$progressData.get()[item.item_id]) {
|
||||
ctx.$lastLoadedItemId.set(item.item_id);
|
||||
}
|
||||
}, [ctx.$lastLoadedItemId, ctx.$progressData, item.item_id]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -22,25 +22,25 @@ export const StagingAreaItemsList = memo(() => {
|
||||
|
||||
return effect([ctx.$selectedItem, ctx.$progressData], (selectedItem, progressData) => {
|
||||
if (!selectedItem) {
|
||||
canvasManager.stagingArea.render();
|
||||
canvasManager.stagingArea.$imageSrc.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const outputImageName = getOutputImageName(selectedItem);
|
||||
|
||||
if (outputImageName) {
|
||||
canvasManager.stagingArea.render({ type: 'imageName', data: outputImageName });
|
||||
canvasManager.stagingArea.$imageSrc.set({ type: 'imageName', data: outputImageName });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = progressData[selectedItem.item_id];
|
||||
|
||||
if (data?.progressImage) {
|
||||
canvasManager.stagingArea.render({ type: 'dataURL', data: data.progressImage.dataURL });
|
||||
canvasManager.stagingArea.$imageSrc.set({ type: 'dataURL', data: data.progressImage.dataURL });
|
||||
return;
|
||||
}
|
||||
|
||||
canvasManager.stagingArea.render();
|
||||
canvasManager.stagingArea.$imageSrc.set(null);
|
||||
});
|
||||
}, [canvasManager, ctx.$progressData, ctx.$selectedItem]);
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const setProgress = ($progressData: WritableAtom<Record<number, ProgressData>>,
|
||||
}
|
||||
};
|
||||
|
||||
export const clearProgressEvent = ($progressData: WritableAtom<Record<number, ProgressData>>, itemId: number) => {
|
||||
const clearProgressEvent = ($progressData: WritableAtom<Record<number, ProgressData>>, itemId: number) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[itemId];
|
||||
if (!current) {
|
||||
@@ -81,7 +81,7 @@ export const clearProgressEvent = ($progressData: WritableAtom<Record<number, Pr
|
||||
});
|
||||
};
|
||||
|
||||
export const clearProgressImage = ($progressData: WritableAtom<Record<number, ProgressData>>, itemId: number) => {
|
||||
const clearProgressImage = ($progressData: WritableAtom<Record<number, ProgressData>>, itemId: number) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[itemId];
|
||||
if (!current) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageNameToImageObject } from 'features/controlLayers/store/util';
|
||||
@@ -12,6 +13,7 @@ import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCheckBold } from 'react-icons/pi';
|
||||
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
@@ -22,6 +24,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
|
||||
const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -37,7 +40,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
};
|
||||
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
|
||||
}, [bboxRect, selectedItemImageName, dispatch, selectedEntityIdentifier?.type]);
|
||||
dispatch(canvasSessionGenerationFinished());
|
||||
deleteByDestination({ destination: ctx.session.id });
|
||||
}, [selectedItemImageName, bboxRect, dispatch, selectedEntityIdentifier?.type, deleteByDestination, ctx.session.id]);
|
||||
|
||||
useHotkeys(
|
||||
['enter'],
|
||||
@@ -56,7 +61,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
icon={<PiCheckBold />}
|
||||
onClick={acceptSelected}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedItemImageName}
|
||||
isDisabled={!selectedItemImageName || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
export const StagingAreaToolbarDiscardAllButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const ctx = useCanvasSessionContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
|
||||
const discardAll = useCallback(() => {
|
||||
deleteByDestination({ destination: ctx.session.id });
|
||||
}, [deleteByDestination, ctx.session.id]);
|
||||
dispatch(canvasSessionGenerationFinished());
|
||||
}, [deleteByDestination, ctx.session.id, dispatch]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
@@ -22,6 +30,7 @@ export const StagingAreaToolbarDiscardAllButton = memo(() => {
|
||||
onClick={discardAll}
|
||||
colorScheme="error"
|
||||
fontSize={16}
|
||||
isDisabled={!shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const ctx = useCanvasSessionContext();
|
||||
const [deleteQueueItem] = useDeleteQueueItemMutation();
|
||||
const selectedItemId = useStore(ctx.$selectedItemId);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -28,7 +31,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
|
||||
onClick={discardSelected}
|
||||
colorScheme="invokeBlue"
|
||||
fontSize={16}
|
||||
isDisabled={selectedItemId === null}
|
||||
isDisabled={selectedItemId === null || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -13,10 +14,12 @@ import { copyImage } from 'services/api/endpoints/images';
|
||||
const uploadImageArg = { image_category: 'general', is_intermediate: true, silent: true } as const;
|
||||
|
||||
export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const { t } = useTranslation();
|
||||
const ctx = useCanvasSessionContext();
|
||||
const imageName = useStore(ctx.$selectedItemOutputImageName);
|
||||
const store = useAppStore();
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
|
||||
const toastSentToCanvas = useCallback(() => {
|
||||
toast({
|
||||
@@ -101,19 +104,35 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
tooltip={t('controlLayers.newLayerFromImage')}
|
||||
icon={<PiDotsThreeBold />}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!imageName}
|
||||
isDisabled={!imageName || !shouldShowStagedImage}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!imageName}>
|
||||
<MenuItem
|
||||
icon={<NewLayerIcon />}
|
||||
onClickCapture={onClickNewInpaintMaskFromImage}
|
||||
isDisabled={!imageName || !shouldShowStagedImage}
|
||||
>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRegionalGuidanceFromImage} isDisabled={!imageName}>
|
||||
<MenuItem
|
||||
icon={<NewLayerIcon />}
|
||||
onClickCapture={onClickNewRegionalGuidanceFromImage}
|
||||
isDisabled={!imageName || !shouldShowStagedImage}
|
||||
>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewControlLayerFromImage} isDisabled={!imageName}>
|
||||
<MenuItem
|
||||
icon={<NewLayerIcon />}
|
||||
onClickCapture={onClickNewControlLayerFromImage}
|
||||
isDisabled={!imageName || !shouldShowStagedImage}
|
||||
>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRasterLayerFromImage} isDisabled={!imageName}>
|
||||
<MenuItem
|
||||
icon={<NewLayerIcon />}
|
||||
onClickCapture={onClickNewRasterLayerFromImage}
|
||||
isDisabled={!imageName || !shouldShowStagedImage}
|
||||
>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -13,9 +14,11 @@ import { copyImage } from 'services/api/endpoints/images';
|
||||
const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY';
|
||||
|
||||
export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const ctx = useCanvasSessionContext();
|
||||
const imageName = useStore(ctx.$selectedItemOutputImageName);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -61,7 +64,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
icon={<PiFloppyDiskBold />}
|
||||
onClick={saveSelectedImageToGallery}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!imageName}
|
||||
isDisabled={!imageName || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,8 +12,8 @@ export const StagingAreaToolbarToggleShowResultsButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleShowResults = useCallback(() => {
|
||||
canvasManager.stagingArea.$shouldShowStagedImage.set(!shouldShowStagedImage);
|
||||
}, [canvasManager.stagingArea.$shouldShowStagedImage, shouldShowStagedImage]);
|
||||
canvasManager.stagingArea.$shouldShowStagedImage.set(!canvasManager.stagingArea.$shouldShowStagedImage.get());
|
||||
}, [canvasManager.stagingArea.$shouldShowStagedImage]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
@@ -21,8 +22,10 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
image: CanvasObjectImage | null;
|
||||
mutex = new Mutex();
|
||||
|
||||
$imageSrc = atom<{ type: 'imageName'; data: string } | { type: 'dataURL'; data: string } | null>(null);
|
||||
|
||||
$shouldShowStagedImage = atom<boolean>(true);
|
||||
$isStaging = atom(true); //TODO: wire up to queue?
|
||||
$isStaging = atom<boolean>(false);
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
super();
|
||||
@@ -40,9 +43,26 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
/**
|
||||
* When we change this flag, we need to re-render the staging area, which hides or shows the staged image.
|
||||
*/
|
||||
this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render));
|
||||
|
||||
/**
|
||||
* Rerender when the image source changes.
|
||||
*/
|
||||
this.subscriptions.add(this.$imageSrc.listen(this.render));
|
||||
|
||||
/**
|
||||
* Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy
|
||||
* state of the canvas.
|
||||
*
|
||||
* We also set the $shouldShowStagedImage flag when we enter staging mode, so that the staged images are shown,
|
||||
* even if the user disabled this in the last staging session.
|
||||
*/
|
||||
this.subscriptions.add(
|
||||
this.$shouldShowStagedImage.listen(() => {
|
||||
this.render();
|
||||
this.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging, oldIsStaging) => {
|
||||
this.$isStaging.set(isStaging);
|
||||
if (isStaging && !oldIsStaging) {
|
||||
this.$shouldShowStagedImage.set(true);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -50,6 +70,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render();
|
||||
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
|
||||
};
|
||||
|
||||
getImageFromSrc = (
|
||||
@@ -72,7 +93,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
render = async (imageSrc?: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string }) => {
|
||||
render = async () => {
|
||||
const release = await this.mutex.acquire();
|
||||
try {
|
||||
this.log.trace('Rendering staging area');
|
||||
@@ -82,6 +103,8 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
|
||||
this.konva.group.position({ x, y });
|
||||
|
||||
const imageSrc = this.$imageSrc.get();
|
||||
|
||||
if (imageSrc) {
|
||||
const image = this.getImageFromSrc(imageSrc, width, height);
|
||||
if (!this.image) {
|
||||
@@ -91,13 +114,13 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
} else if (this.image.isLoading || this.image.isError) {
|
||||
// noop
|
||||
} else {
|
||||
await this.image.update({ ...this.image.state, image }, true);
|
||||
await this.image.update({ ...this.image.state, image });
|
||||
}
|
||||
this.image.konva.group.visible(shouldShowStagedImage);
|
||||
} else {
|
||||
this.image?.destroy();
|
||||
this.image = null;
|
||||
}
|
||||
this.konva.group.visible(shouldShowStagedImage);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user