From 56938ca0a1e611ec01f0a8cf8c62aaba75e107d9 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:45:56 +1000
Subject: [PATCH] feat(ui): rough out canvas staging area
---
.../addCommitStagingAreaImageListener.ts | 2 +-
.../listeners/imageDeletionListeners.ts | 2 +-
.../AdvancedSession/AdvancedSession.tsx | 33 +++--
.../SimpleSession/QueueItemPreviewMini.tsx | 14 +-
.../SimpleSession/QueueItemProgressImage.tsx | 9 +-
.../SimpleSession/StagingAreaItemsList.tsx | 37 ++++-
.../components/SimpleSession/context.tsx | 132 ++++++++++++++++-
.../components/SimpleSession/shared.ts | 2 +-
.../SimpleSession/use-staging-keyboard-nav.ts | 54 +------
.../StagingArea/StagingAreaToolbar.tsx | 17 ++-
.../StagingAreaToolbarAcceptButton.tsx | 20 +--
.../StagingAreaToolbarDiscardAllButton.tsx | 11 +-
...tagingAreaToolbarDiscardSelectedButton.tsx | 25 ++--
.../StagingAreaToolbarImageCountButton.tsx | 11 +-
.../StagingAreaToolbarNextButton.tsx | 20 +--
.../StagingAreaToolbarPrevButton.tsx | 20 +--
.../StagingAreaToolbarSaveAsMenu.tsx | 86 +++++------
...AreaToolbarSaveSelectedToGalleryButton.tsx | 15 +-
.../controlLayers/hooks/useInvokeCanvas.ts | 5 +
.../controlLayers/konva/CanvasManager.ts | 6 -
.../konva/CanvasObject/CanvasObjectImage.ts | 110 +++++++++-----
.../konva/CanvasStagingAreaModule.ts | 138 +++++++-----------
.../src/features/controlLayers/store/types.ts | 9 +-
.../src/features/controlLayers/store/util.ts | 17 +++
.../deleteImageModal/store/selectors.ts | 6 +-
.../queue/hooks/useCancelCurrentQueueItem.ts | 4 +-
.../queue/hooks/useCancelQueueItem.ts | 2 +-
.../web/src/services/api/endpoints/images.ts | 7 +
.../web/src/services/api/endpoints/queue.ts | 34 ++++-
.../frontend/web/src/services/api/schema.ts | 105 ++++++++++++-
30 files changed, 622 insertions(+), 331 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
index a83da52d21..6ef2af6c2f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
@@ -17,7 +17,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
effect: async (_, { dispatch }) => {
try {
const req = dispatch(
- queueApi.endpoints.cancelByBatchDestination.initiate(
+ queueApi.endpoints.cancelByDestination.initiate(
{ destination: 'canvas' },
{ fixedCacheKey: 'cancelByBatchOrigin' }
)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
index f0fd91e69b..150cb73e83 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
@@ -58,7 +58,7 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
let shouldDelete = false;
for (const obj of objects) {
- if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
+ if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
shouldDelete = true;
break;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
index 73f50ca36f..47fd674016 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx
@@ -13,7 +13,8 @@ import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
-import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
+import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
@@ -52,7 +53,7 @@ const canvasBgSx = {
},
};
-export const AdvancedSession = memo((_props: { session: AdvancedSessionIdentifier }) => {
+export const AdvancedSession = memo(({ session }: { session: AdvancedSessionIdentifier }) => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -106,13 +107,27 @@ export const AdvancedSession = memo((_props: { session: AdvancedSessionIdentifie
)}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
index 2578731bfa..acb907325b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -11,24 +11,18 @@ import { memo, useCallback, useState } from 'react';
import type { S } from 'services/api/types';
const sx = {
- cursor: 'pointer',
userSelect: 'none',
pos: 'relative',
alignItems: 'center',
justifyContent: 'center',
- overflow: 'hidden',
- h: 'full',
- maxH: 'full',
- maxW: 'full',
- minW: 0,
- minH: 0,
+ h: 108,
+ w: 108,
+ flexShrink: 0,
borderWidth: 1,
borderRadius: 'base',
'&[data-selected="true"]': {
borderColor: 'invokeBlue.300',
},
- aspectRatio: '1/1',
- flexShrink: 0,
} satisfies SystemStyleObject;
type Props = {
@@ -64,7 +58,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
onDoubleClick={onDoubleClick}
>
- {imageDTO && }
+ {imageDTO && }
{!imageLoaded && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index 2ea3dd827e..c21e41e12a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -1,7 +1,8 @@
import type { ImageProps } from '@invoke-ai/ui-library';
-import { Image } from '@invoke-ai/ui-library';
+import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
+import { PiImageBold } from 'react-icons/pi';
type Props = { itemId: number } & ImageProps;
@@ -10,7 +11,11 @@ export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
- return null;
+ return (
+
+
+
+ );
}
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
index d679835707..3e9b9cee37 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
@@ -4,16 +4,49 @@ import { useStore } from '@nanostores/react';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
-import { memo } from 'react';
+import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
+import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { effect } from 'nanostores';
+import { memo, useEffect } from 'react';
export const StagingAreaItemsList = memo(() => {
+ const canvasManager = useCanvasManagerSafe();
const ctx = useCanvasSessionContext();
const items = useStore(ctx.$items);
const selectedItemId = useStore(ctx.$selectedItemId);
+ useEffect(() => {
+ if (!canvasManager) {
+ return;
+ }
+
+ return effect([ctx.$selectedItem, ctx.$progressData], (selectedItem, progressData) => {
+ if (!selectedItem) {
+ canvasManager.stagingArea.render();
+ return;
+ }
+
+ const outputImageName = getOutputImageName(selectedItem);
+
+ if (outputImageName) {
+ canvasManager.stagingArea.render({ type: 'imageName', data: outputImageName });
+ return;
+ }
+
+ const data = progressData[selectedItem.item_id];
+
+ if (data?.progressImage) {
+ canvasManager.stagingArea.render({ type: 'dataURL', data: data.progressImage.dataURL });
+ return;
+ }
+
+ canvasManager.stagingArea.render();
+ });
+ }, [canvasManager, ctx.$progressData, ctx.$selectedItem]);
+
return (
-
+
{items.map((item, i) => (
;
+ $itemCount: Atom;
$hasItems: Atom;
$progressData: WritableAtom>;
$selectedItemId: WritableAtom;
$selectedItem: Atom;
$selectedItemIndex: Atom;
+ $selectedItemOutputImageName: Atom;
$autoSwitch: WritableAtom;
$lastLoadedItemId: WritableAtom;
+ selectNext: () => void;
+ selectPrev: () => void;
+ selectFirst: () => void;
+ selectLast: () => void;
};
const CanvasSessionContext = createContext(null);
@@ -153,6 +160,11 @@ export const CanvasSessionContextProvider = memo(
*/
const $items = useState(() => atom([]))[0];
+ /**
+ * Manually-synced atom containing the queue items for the current session.
+ */
+ const $prevItems = useState(() => atom([]))[0];
+
/**
* Whether auto-switch is enabled.
*/
@@ -174,6 +186,11 @@ export const CanvasSessionContextProvider = memo(
*/
const $selectedItemId = useState(() => atom(null))[0];
+ /**
+ * The number of items. Computed from the queue items array.
+ */
+ const $itemCount = useState(() => computed([$items], (items) => items.length))[0];
+
/**
* Whether there are any items. Computed from the queue items array.
*/
@@ -209,6 +226,23 @@ export const CanvasSessionContextProvider = memo(
})
)[0];
+ /**
+ * The currently selected queue item's output image name, or null if one is not selected or there is no output
+ * image recorded.
+ */
+ const $selectedItemOutputImageName = useState(() =>
+ computed([$selectedItem], (selectedItem) => {
+ if (selectedItem === null) {
+ return null;
+ }
+ const outputImageName = getOutputImageName(selectedItem);
+ if (outputImageName === null) {
+ return null;
+ }
+ return outputImageName;
+ })
+ )[0];
+
/**
* A redux selector to select all queue items from the RTK Query cache. It's important that this returns stable
* references if possible to reduce re-renders. All derivations of the queue items (e.g. filtering out canceled
@@ -223,6 +257,54 @@ export const CanvasSessionContextProvider = memo(
[session.id]
);
+ const selectNext = useCallback(() => {
+ const selectedItemId = $selectedItemId.get();
+ if (selectedItemId === null) {
+ return;
+ }
+ const items = $items.get();
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const nextIndex = (currentIndex + 1) % items.length;
+ const nextItem = items[nextIndex];
+ if (!nextItem) {
+ return;
+ }
+ $selectedItemId.set(nextItem.item_id);
+ }, [$items, $selectedItemId]);
+
+ const selectPrev = useCallback(() => {
+ const selectedItemId = $selectedItemId.get();
+ if (selectedItemId === null) {
+ return;
+ }
+ const items = $items.get();
+ const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
+ const prevItem = items[prevIndex];
+ if (!prevItem) {
+ return;
+ }
+ $selectedItemId.set(prevItem.item_id);
+ }, [$items, $selectedItemId]);
+
+ const selectFirst = useCallback(() => {
+ const items = $items.get();
+ const first = items.at(0);
+ if (!first) {
+ return;
+ }
+ $selectedItemId.set(first.item_id);
+ }, [$items, $selectedItemId]);
+
+ const selectLast = useCallback(() => {
+ const items = $items.get();
+ const last = items.at(-1);
+ if (!last) {
+ return;
+ }
+ $selectedItemId.set(last.item_id);
+ }, [$items, $selectedItemId]);
+
// Set up socket listeners
useEffect(() => {
if (!socket) {
@@ -236,10 +318,23 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
+ const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
+ if (data.destination !== session.id) {
+ return;
+ }
+
+ if (data.status === 'canceled' || data.status === 'failed') {
+ clearProgressEvent($progressData, data.item_id);
+ clearProgressImage($progressData, data.item_id);
+ }
+ };
+
socket.on('invocation_progress', onProgress);
+ socket.on('queue_item_status_changed', onQueueItemStatusChanged);
return () => {
socket.off('invocation_progress', onProgress);
+ socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
}, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]);
@@ -253,6 +348,7 @@ export const CanvasSessionContextProvider = memo(
const prevItems = $items.get();
const items = selectQueueItems(store.getState());
if (items !== prevItems) {
+ $prevItems.set(prevItems);
$items.set(items);
}
});
@@ -272,13 +368,16 @@ export const CanvasSessionContextProvider = memo(
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
// the above case, selecting the first item if there are any.
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
- $selectedItemId.set(null);
+ const prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId);
+ const nextItem = items[prevIndex];
+ $selectedItemId.set(nextItem?.item_id ?? null);
return;
}
});
// Clean up the progress data when a queue item is discarded.
- const unsubCleanUpProgressData = effect([$items, $progressData], (items, progressData) => {
+ const unsubCleanUpProgressData = $items.listen((items) => {
+ const progressData = $progressData.get();
const toDelete: number[] = [];
for (const datum of Object.values(progressData)) {
if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) {
@@ -292,7 +391,6 @@ export const CanvasSessionContextProvider = memo(
for (const itemId of toDelete) {
delete newProgressData[itemId];
}
- // This will re-trigger the effect - maybe this could just be a listener on $items? Brain hurt
$progressData.set(newProgressData);
});
@@ -331,7 +429,17 @@ export const CanvasSessionContextProvider = memo(
$progressData.set({});
$selectedItemId.set(null);
};
- }, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
+ }, [
+ $autoSwitch,
+ $items,
+ $lastLoadedItemId,
+ $prevItems,
+ $progressData,
+ $selectedItemId,
+ selectQueueItems,
+ session.id,
+ store,
+ ]);
const value = useMemo(
() => ({
@@ -344,17 +452,29 @@ export const CanvasSessionContextProvider = memo(
$selectedItem,
$selectedItemIndex,
$lastLoadedItemId,
+ $selectedItemOutputImageName,
+ $itemCount,
+ selectNext,
+ selectPrev,
+ selectFirst,
+ selectLast,
}),
[
$autoSwitch,
- $hasItems,
$items,
+ $hasItems,
$lastLoadedItemId,
$progressData,
$selectedItem,
$selectedItemId,
$selectedItemIndex,
session,
+ $selectedItemOutputImageName,
+ $itemCount,
+ selectNext,
+ selectPrev,
+ selectFirst,
+ selectLast,
]
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
index 1723b4bebf..d8b7ebc7b1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -23,7 +23,7 @@ export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0p
export const getQueueItemElementId = (itemId: number) => `queue-item-status-card-${itemId}`;
-const getOutputImageName = (item: S['SessionQueueItem']) => {
+export const getOutputImageName = (item: S['SessionQueueItem']) => {
const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
isCanvasOutputNodeId(nodeId)
)?.[1][0];
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
index 6a11b5fa99..bb3989b565 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/use-staging-keyboard-nav.ts
@@ -1,57 +1,11 @@
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
export const useStagingAreaKeyboardNav = () => {
const ctx = useCanvasSessionContext();
- const onNext = useCallback(() => {
- const selectedItemId = ctx.$selectedItemId.get();
- if (selectedItemId === null) {
- return;
- }
- const items = ctx.$items.get();
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const nextIndex = (currentIndex + 1) % items.length;
- const nextItem = items[nextIndex];
- if (!nextItem) {
- return;
- }
- ctx.$selectedItemId.set(nextItem.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
- const onPrev = useCallback(() => {
- const selectedItemId = ctx.$selectedItemId.get();
- if (selectedItemId === null) {
- return;
- }
- const items = ctx.$items.get();
- const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
- const prevItem = items[prevIndex];
- if (!prevItem) {
- return;
- }
- ctx.$selectedItemId.set(prevItem.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
- const onFirst = useCallback(() => {
- const items = ctx.$items.get();
- const first = items.at(0);
- if (!first) {
- return;
- }
- ctx.$selectedItemId.set(first.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
- const onLast = useCallback(() => {
- const items = ctx.$items.get();
- const last = items.at(-1);
- if (!last) {
- return;
- }
- ctx.$selectedItemId.set(last.item_id);
- }, [ctx.$items, ctx.$selectedItemId]);
-
- useHotkeys('left', onPrev, { preventDefault: true });
- useHotkeys('right', onNext, { preventDefault: true });
- useHotkeys('meta+left', onFirst, { preventDefault: true });
- useHotkeys('meta+right', onLast, { preventDefault: true });
+ useHotkeys('left', ctx.selectPrev, { preventDefault: true });
+ useHotkeys('right', ctx.selectNext, { preventDefault: true });
+ useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true });
+ useHotkeys('meta+right', ctx.selectLast, { preventDefault: true });
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
index 1404616380..4ac13e74a3 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -1,4 +1,6 @@
import { ButtonGroup } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
@@ -8,9 +10,22 @@ import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/
import { StagingAreaToolbarSaveAsMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu';
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
-import { memo } from 'react';
+import { memo, useEffect } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
export const StagingAreaToolbar = memo(() => {
+ const ctx = useCanvasSessionContext();
+
+ useEffect(() => {
+ return ctx.$selectedItemId.listen((id) => {
+ if (id !== null) {
+ document.getElementById(getQueueItemElementId(id))?.scrollIntoView();
+ }
+ });
+ }, [ctx.$selectedItemId]);
+
+ useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true });
+ useHotkeys('meta+right', ctx.selectLast, { preventDefault: true });
return (
<>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
index 7d01422854..3a1feddebc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -2,48 +2,48 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
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 {
selectImageCount,
- selectSelectedImage,
stagingAreaReset,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
-import { imageDTOToImageObject } from 'features/controlLayers/store/util';
+import { imageNameToImageObject } from 'features/controlLayers/store/util';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
export const StagingAreaToolbarAcceptButton = memo(() => {
+ const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const bboxRect = useAppSelector(selectBboxRect);
- const selectedImage = useAppSelector(selectSelectedImage);
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const imageCount = useAppSelector(selectImageCount);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
+ const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
const { t } = useTranslation();
const acceptSelected = useCallback(() => {
- if (!selectedImage) {
+ if (!selectedItemImageName) {
return;
}
- const { x, y } = bboxRect;
- const { imageDTO, offsetX, offsetY } = selectedImage;
- const imageObject = imageDTOToImageObject(imageDTO);
+ const { x, y, width, height } = bboxRect;
+ const imageObject = imageNameToImageObject(selectedItemImageName, { width, height });
const overrides: Partial = {
- position: { x: x + offsetX, y: y + offsetY },
+ position: { x, y },
objects: [imageObject],
};
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
dispatch(stagingAreaReset());
- }, [bboxRect, dispatch, selectedEntityIdentifier?.type, selectedImage]);
+ }, [bboxRect, selectedItemImageName, dispatch, selectedEntityIdentifier?.type]);
useHotkeys(
['enter'],
@@ -62,7 +62,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
icon={}
onClick={acceptSelected}
colorScheme="invokeBlue"
- isDisabled={!selectedImage}
+ isDisabled={!selectedItemImageName}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
index ee038c075e..dbc978a42a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,17 +1,18 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
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 dispatch = useAppDispatch();
+ const ctx = useCanvasSessionContext();
const { t } = useTranslation();
+ const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
const discardAll = useCallback(() => {
- dispatch(stagingAreaReset());
- }, [dispatch]);
+ deleteByDestination({ destination: ctx.session.id });
+ }, [deleteByDestination, ctx.session.id]);
return (
{
+ const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
+ const [deleteQueueItem] = useDeleteQueueItemMutation();
+ const selectedItemId = useStore(ctx.$selectedItemId);
const index = useAppSelector(selectStagedImageIndex);
const selectedImage = useAppSelector(selectSelectedImage);
const imageCount = useAppSelector(selectImageCount);
@@ -20,15 +24,16 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
const { t } = useTranslation();
const discardSelected = useCallback(() => {
- if (!selectedImage) {
+ if (selectedItemId === null) {
return;
}
- if (imageCount === 1) {
- dispatch(stagingAreaReset());
- } else {
- dispatch(stagingAreaStagedImageDiscarded({ index }));
- }
- }, [selectedImage, imageCount, dispatch, index]);
+ deleteQueueItem({ item_id: selectedItemId });
+ // if (imageCount === 1) {
+ // dispatch(stagingAreaReset());
+ // } else {
+ // dispatch(stagingAreaStagedImageDiscarded({ index }));
+ // }
+ }, [selectedItemId, deleteQueueItem]);
return (
{
onClick={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
- isDisabled={!selectedImage}
+ isDisabled={selectedItemId === null}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
index d408bf1c90..7e14ff580a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
@@ -1,19 +1,24 @@
import { Button } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { selectImageCount, selectStagedImageIndex } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useMemo } from 'react';
export const StagingAreaToolbarImageCountButton = memo(() => {
+ const ctx = useCanvasSessionContext();
+ const selectItemIndex = useStore(ctx.$selectedItemIndex);
+ const itemCount = useStore(ctx.$itemCount);
const index = useAppSelector(selectStagedImageIndex);
const imageCount = useAppSelector(selectImageCount);
const counterText = useMemo(() => {
- if (imageCount > 0) {
- return `${(index ?? 0) + 1} of ${imageCount}`;
+ if (itemCount > 0 && selectItemIndex !== null) {
+ return `${selectItemIndex + 1} of ${itemCount}`;
} else {
return `0 of 0`;
}
- }, [imageCount, index]);
+ }, [itemCount, selectItemIndex]);
return (