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 (