mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): rough out canvas staging area
This commit is contained in:
@@ -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' }
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
|
||||
<CanvasManagerProviderGate>
|
||||
<StagingAreaIsStagingGate>
|
||||
<StagingAreaToolbar />
|
||||
</StagingAreaIsStagingGate>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasSessionContextProvider session={session}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
bottom={4}
|
||||
gap={2}
|
||||
align="center"
|
||||
justify="center"
|
||||
left={4}
|
||||
right={4}
|
||||
>
|
||||
<Flex position="relative" maxW="full" w="full" h={108}>
|
||||
<StagingAreaItemsList />
|
||||
</Flex>
|
||||
<Flex gap={2}>
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CanvasSessionContextProvider>
|
||||
</CanvasManagerProviderGate>
|
||||
<Flex position="absolute" bottom={4}>
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<QueueItemStatusLabel status={item.status} position="absolute" margin="auto" />
|
||||
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} />}
|
||||
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} asThumbnail />}
|
||||
{!imageLoaded && <QueueItemProgressImage itemId={item.item_id} position="absolute" />}
|
||||
<QueueItemNumber number={number} position="absolute" top={0} left={1} />
|
||||
<QueueItemCircularProgress itemId={item.item_id} status={item.status} position="absolute" top={1} right={2} />
|
||||
|
||||
@@ -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 (
|
||||
<Flex w="full" h="full" bg="base.700" alignItems="center" justifyContent="center">
|
||||
<Icon as={PiImageBold} boxSize={16} opacity={0.2} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 (
|
||||
<ScrollableContent overflowX="scroll" overflowY="hidden">
|
||||
<Flex gap={2} w="full" h="full">
|
||||
<Flex gap={2} w="full" h="full" justifyContent="safe center">
|
||||
{items.map((item, i) => (
|
||||
<QueueItemPreviewMini
|
||||
key={`${item.item_id}-mini`}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import type {
|
||||
AdvancedSessionIdentifier,
|
||||
SimpleSessionIdentifier,
|
||||
@@ -10,7 +11,7 @@ import type { ProgressImage } from 'features/nodes/types/common';
|
||||
import type { Atom, WritableAtom } from 'nanostores';
|
||||
import { atom, computed, effect } from 'nanostores';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { S } from 'services/api/types';
|
||||
import { $socket } from 'services/events/stores';
|
||||
@@ -122,13 +123,19 @@ export const clearProgressImage = ($progressData: WritableAtom<Record<number, Pr
|
||||
export type CanvasSessionContextValue = {
|
||||
session: SimpleSessionIdentifier | AdvancedSessionIdentifier;
|
||||
$items: Atom<S['SessionQueueItem'][]>;
|
||||
$itemCount: Atom<number>;
|
||||
$hasItems: Atom<boolean>;
|
||||
$progressData: WritableAtom<Record<string, ProgressData>>;
|
||||
$selectedItemId: WritableAtom<number | null>;
|
||||
$selectedItem: Atom<S['SessionQueueItem'] | null>;
|
||||
$selectedItemIndex: Atom<number | null>;
|
||||
$selectedItemOutputImageName: Atom<string | null>;
|
||||
$autoSwitch: WritableAtom<boolean>;
|
||||
$lastLoadedItemId: WritableAtom<number | null>;
|
||||
selectNext: () => void;
|
||||
selectPrev: () => void;
|
||||
selectFirst: () => void;
|
||||
selectLast: () => void;
|
||||
};
|
||||
|
||||
const CanvasSessionContext = createContext<CanvasSessionContextValue | null>(null);
|
||||
@@ -153,6 +160,11 @@ export const CanvasSessionContextProvider = memo(
|
||||
*/
|
||||
const $items = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
|
||||
|
||||
/**
|
||||
* Manually-synced atom containing the queue items for the current session.
|
||||
*/
|
||||
const $prevItems = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
|
||||
|
||||
/**
|
||||
* Whether auto-switch is enabled.
|
||||
*/
|
||||
@@ -174,6 +186,11 @@ export const CanvasSessionContextProvider = memo(
|
||||
*/
|
||||
const $selectedItemId = useState(() => atom<number | null>(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<CanvasSessionContextValue>(
|
||||
() => ({
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
|
||||
@@ -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<CanvasRasterLayerState> = {
|
||||
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={<PiCheckBold />}
|
||||
onClick={acceptSelected}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage}
|
||||
isDisabled={!selectedItemImageName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import {
|
||||
selectImageCount,
|
||||
selectSelectedImage,
|
||||
selectStagedImageIndex,
|
||||
stagingAreaReset,
|
||||
stagingAreaStagedImageDiscarded,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
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 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 (
|
||||
<IconButton
|
||||
@@ -38,7 +43,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
|
||||
onClick={discardSelected}
|
||||
colorScheme="invokeBlue"
|
||||
fontSize={16}
|
||||
isDisabled={!selectedImage}
|
||||
isDisabled={selectedItemId === null}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<Button colorScheme="base" pointerEvents="none" minW={28}>
|
||||
|
||||
@@ -2,17 +2,17 @@ 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 {
|
||||
selectImageCount,
|
||||
stagingAreaNextStagedImageSelected,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowRightBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaToolbarNextButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const itemCount = useStore(ctx.$itemCount);
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
@@ -22,17 +22,17 @@ export const StagingAreaToolbarNextButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const selectNext = useCallback(() => {
|
||||
dispatch(stagingAreaNextStagedImageSelected());
|
||||
}, [dispatch]);
|
||||
ctx.selectNext();
|
||||
}, [ctx]);
|
||||
|
||||
useHotkeys(
|
||||
['right'],
|
||||
selectNext,
|
||||
ctx.selectNext,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && itemCount > 1,
|
||||
},
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, itemCount, ctx.selectNext]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -42,7 +42,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
|
||||
icon={<PiArrowRightBold />}
|
||||
onClick={selectNext}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
isDisabled={itemCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,17 +2,17 @@ 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 {
|
||||
selectImageCount,
|
||||
stagingAreaPrevStagedImageSelected,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowLeftBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const itemCount = useStore(ctx.$itemCount);
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
@@ -22,17 +22,17 @@ export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const selectPrev = useCallback(() => {
|
||||
dispatch(stagingAreaPrevStagedImageSelected());
|
||||
}, [dispatch]);
|
||||
ctx.selectPrev();
|
||||
}, [ctx]);
|
||||
|
||||
useHotkeys(
|
||||
['left'],
|
||||
selectPrev,
|
||||
ctx.selectPrev,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && itemCount > 1,
|
||||
},
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, itemCount, ctx.selectPrev]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -42,7 +42,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
icon={<PiArrowLeftBold />}
|
||||
onClick={selectPrev}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
isDisabled={itemCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDotsThreeBold } from 'react-icons/pi';
|
||||
import { imageDTOToFile, uploadImage } from 'services/api/endpoints/images';
|
||||
import { copyImage } from 'services/api/endpoints/images';
|
||||
|
||||
const uploadImageArg = { image_category: 'general', is_intermediate: true, silent: true } as const;
|
||||
|
||||
export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const selectedImage = useAppSelector(selectSelectedImage);
|
||||
const ctx = useCanvasSessionContext();
|
||||
const imageName = useStore(ctx.$selectedItemOutputImageName);
|
||||
const store = useAppStore();
|
||||
|
||||
const toastSentToCanvas = useCallback(() => {
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
const onClickNewRasterLayerFromImage = useCallback(async () => {
|
||||
if (!selectedImage) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
const { dispatch, getState } = store;
|
||||
const file = await imageDTOToFile(selectedImage.imageDTO);
|
||||
const imageDTO = await uploadImage({ file, ...uploadImageArg });
|
||||
const imageDTO = await copyImage(imageName, uploadImageArg);
|
||||
createNewCanvasEntityFromImage({
|
||||
imageDTO,
|
||||
type: 'raster_layer',
|
||||
@@ -31,20 +39,16 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
getState,
|
||||
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
|
||||
});
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [selectedImage, store, t]);
|
||||
toastSentToCanvas();
|
||||
}, [imageName, store, toastSentToCanvas]);
|
||||
|
||||
const onClickNewControlLayerFromImage = useCallback(async () => {
|
||||
if (!selectedImage) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
const { dispatch, getState } = store;
|
||||
const file = await imageDTOToFile(selectedImage.imageDTO);
|
||||
const imageDTO = await uploadImage({ file, ...uploadImageArg });
|
||||
const imageDTO = await copyImage(imageName, uploadImageArg);
|
||||
|
||||
createNewCanvasEntityFromImage({
|
||||
imageDTO,
|
||||
type: 'control_layer',
|
||||
@@ -52,20 +56,16 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
getState,
|
||||
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
|
||||
});
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [selectedImage, store, t]);
|
||||
toastSentToCanvas();
|
||||
}, [imageName, store, toastSentToCanvas]);
|
||||
|
||||
const onClickNewInpaintMaskFromImage = useCallback(async () => {
|
||||
if (!selectedImage) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
const { dispatch, getState } = store;
|
||||
const file = await imageDTOToFile(selectedImage.imageDTO);
|
||||
const imageDTO = await uploadImage({ file, ...uploadImageArg });
|
||||
const imageDTO = await copyImage(imageName, uploadImageArg);
|
||||
|
||||
createNewCanvasEntityFromImage({
|
||||
imageDTO,
|
||||
type: 'inpaint_mask',
|
||||
@@ -73,20 +73,16 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
getState,
|
||||
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
|
||||
});
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [selectedImage, store, t]);
|
||||
toastSentToCanvas();
|
||||
}, [imageName, store, toastSentToCanvas]);
|
||||
|
||||
const onClickNewRegionalGuidanceFromImage = useCallback(async () => {
|
||||
if (!selectedImage) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
const { dispatch, getState } = store;
|
||||
const file = await imageDTOToFile(selectedImage.imageDTO);
|
||||
const imageDTO = await uploadImage({ file, ...uploadImageArg });
|
||||
const imageDTO = await copyImage(imageName, uploadImageArg);
|
||||
|
||||
createNewCanvasEntityFromImage({
|
||||
imageDTO,
|
||||
type: 'regional_guidance',
|
||||
@@ -94,12 +90,8 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
getState,
|
||||
overrides: { isEnabled: false }, // We are adding the layer while staging, it should be disabled by default
|
||||
});
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [selectedImage, store, t]);
|
||||
toastSentToCanvas();
|
||||
}, [imageName, store, toastSentToCanvas]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
@@ -109,23 +101,19 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
|
||||
tooltip={t('controlLayers.newLayerFromImage')}
|
||||
icon={<PiDotsThreeBold />}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage}
|
||||
isDisabled={!imageName}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!selectedImage}>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!imageName}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<NewLayerIcon />}
|
||||
onClickCapture={onClickNewRegionalGuidanceFromImage}
|
||||
isDisabled={!selectedImage}
|
||||
>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRegionalGuidanceFromImage} isDisabled={!imageName}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewControlLayerFromImage} isDisabled={!selectedImage}>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewControlLayerFromImage} isDisabled={!imageName}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRasterLayerFromImage} isDisabled={!selectedImage}>
|
||||
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRasterLayerFromImage} isDisabled={!imageName}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
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 { 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 { imageDTOToFile, uploadImage } from 'services/api/endpoints/images';
|
||||
import { copyImage } 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 ctx = useCanvasSessionContext();
|
||||
const imageName = useStore(ctx.$selectedItemOutputImageName);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const saveSelectedImageToGallery = useCallback(async () => {
|
||||
if (!selectedImage) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,10 +30,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
// the gallery without borking the canvas, which may need this image to exist.
|
||||
const result = await withResultAsync(async () => {
|
||||
// Create a new file with the same name, which we will upload
|
||||
const file = await imageDTOToFile(selectedImage.imageDTO);
|
||||
|
||||
await uploadImage({
|
||||
file,
|
||||
await copyImage(imageName, {
|
||||
// Image should show up in the Images tab
|
||||
image_category: 'general',
|
||||
is_intermediate: false,
|
||||
@@ -53,7 +54,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, [autoAddBoardId, selectedImage, t]);
|
||||
}, [autoAddBoardId, imageName, t]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -52,6 +52,11 @@ export const useInvokeCanvas = (): ((el: HTMLDivElement | null) => void) => {
|
||||
|
||||
const manager = new CanvasManager(container, store, socket);
|
||||
manager.initialize();
|
||||
|
||||
return () => {
|
||||
manager.destroy();
|
||||
$canvasManager.set(null);
|
||||
};
|
||||
}, [container, socket, store]);
|
||||
|
||||
return containerRef;
|
||||
|
||||
@@ -10,7 +10,6 @@ import { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konv
|
||||
import type { CanvasEntityAdapter, CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||
import { CanvasEntityRendererModule } from 'features/controlLayers/konva/CanvasEntityRendererModule';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule';
|
||||
import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule';
|
||||
import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule';
|
||||
import { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
|
||||
@@ -66,7 +65,6 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
compositor: CanvasCompositorModule;
|
||||
tool: CanvasToolModule;
|
||||
stagingArea: CanvasStagingAreaModule;
|
||||
progressImage: CanvasProgressImageModule;
|
||||
|
||||
konva: {
|
||||
previewLayer: Konva.Layer;
|
||||
@@ -131,11 +129,9 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
this.stage.addLayer(this.konva.previewLayer);
|
||||
|
||||
this.tool = new CanvasToolModule(this);
|
||||
this.progressImage = new CanvasProgressImageModule(this);
|
||||
|
||||
// Must add in this order for correct z-index
|
||||
this.konva.previewLayer.add(this.stagingArea.konva.group);
|
||||
this.konva.previewLayer.add(this.progressImage.konva.group);
|
||||
this.konva.previewLayer.add(this.tool.konva.group);
|
||||
}
|
||||
|
||||
@@ -238,7 +234,6 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
return [
|
||||
this.stagingArea,
|
||||
this.tool,
|
||||
this.progressImage,
|
||||
this.stateApi,
|
||||
this.background,
|
||||
this.worker,
|
||||
@@ -285,7 +280,6 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
stateApi: this.stateApi.repr(),
|
||||
stagingArea: this.stagingArea.repr(),
|
||||
tool: this.tool.repr(),
|
||||
progressImage: this.progressImage.repr(),
|
||||
background: this.background.repr(),
|
||||
worker: this.worker.repr(),
|
||||
entityRenderer: this.entityRenderer.repr(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getKonvaNodeDebugAttrs, loadImage } from 'features/controlLayers/konva/
|
||||
import type { CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import { t } from 'i18next';
|
||||
import Konva from 'konva';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Logger } from 'roarr';
|
||||
import { getImageDTOSafe } from 'services/api/endpoints/images';
|
||||
|
||||
@@ -94,7 +95,7 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
updateImageSource = async (imageName: string) => {
|
||||
updateImageSourceByImageName = async (imageName: string) => {
|
||||
this.log.trace({ imageName }, 'Updating image source');
|
||||
|
||||
this.isLoading = true;
|
||||
@@ -121,7 +122,30 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
|
||||
this.imageElement = imageElementResult.value;
|
||||
|
||||
await this.updateImageElement();
|
||||
this.updateImageElement();
|
||||
};
|
||||
|
||||
updateImageSourceByDataURL = async (dataURL: string) => {
|
||||
this.log.trace({ dataURL: `${dataURL.substring(0, 16)}...` }, 'Updating image source');
|
||||
|
||||
this.isLoading = true;
|
||||
this.konva.group.visible(true);
|
||||
|
||||
if (!this.konva.image) {
|
||||
this.konva.placeholder.group.visible(false);
|
||||
this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
|
||||
}
|
||||
|
||||
const imageElementResult = await withResultAsync(() => loadImage(dataURL, false));
|
||||
if (imageElementResult.isErr()) {
|
||||
// Image loading failed (e.g. the URL to the "physical" image is invalid)
|
||||
this.onFailedToLoadImage(t('controlLayers.unableToLoadImage', 'Unable to load image'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageElement = imageElementResult.value;
|
||||
|
||||
this.updateImageElement();
|
||||
};
|
||||
|
||||
onFailedToLoadImage = (message: string) => {
|
||||
@@ -133,43 +157,37 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.konva.placeholder.group.visible(true);
|
||||
};
|
||||
|
||||
updateImageElement = async () => {
|
||||
const release = await this.mutex.acquire();
|
||||
updateImageElement = () => {
|
||||
if (this.imageElement) {
|
||||
const { width, height } = this.state.image;
|
||||
|
||||
try {
|
||||
if (this.imageElement) {
|
||||
const { width, height } = this.state.image;
|
||||
|
||||
if (this.konva.image) {
|
||||
this.log.trace('Updating Konva image attrs');
|
||||
this.konva.image.setAttrs({
|
||||
image: this.imageElement,
|
||||
width,
|
||||
height,
|
||||
visible: true,
|
||||
});
|
||||
} else {
|
||||
this.log.trace('Creating new Konva image');
|
||||
this.konva.image = new Konva.Image({
|
||||
name: `${this.type}:image`,
|
||||
listening: false,
|
||||
image: this.imageElement,
|
||||
width,
|
||||
height,
|
||||
perfectDrawEnabled: false,
|
||||
});
|
||||
this.konva.group.add(this.konva.image);
|
||||
}
|
||||
|
||||
this.konva.placeholder.rect.setAttrs({ width, height });
|
||||
this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
|
||||
|
||||
this.isLoading = false;
|
||||
this.isError = false;
|
||||
this.konva.placeholder.group.visible(false);
|
||||
if (this.konva.image) {
|
||||
this.log.trace('Updating Konva image attrs');
|
||||
this.konva.image.setAttrs({
|
||||
image: this.imageElement,
|
||||
width,
|
||||
height,
|
||||
visible: true,
|
||||
});
|
||||
} else {
|
||||
this.log.trace('Creating new Konva image');
|
||||
this.konva.image = new Konva.Image({
|
||||
name: `${this.type}:image`,
|
||||
listening: false,
|
||||
image: this.imageElement,
|
||||
width,
|
||||
height,
|
||||
perfectDrawEnabled: false,
|
||||
});
|
||||
this.konva.group.add(this.konva.image);
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
|
||||
this.konva.placeholder.rect.setAttrs({ width, height });
|
||||
this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
|
||||
|
||||
this.isLoading = false;
|
||||
this.isError = false;
|
||||
this.konva.placeholder.group.visible(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,10 +196,22 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.log.trace({ state }, 'Updating image');
|
||||
|
||||
const { image } = state;
|
||||
const { width, height, image_name } = image;
|
||||
if (force || (this.state.image.image_name !== image_name && !this.isLoading)) {
|
||||
await this.updateImageSource(image_name);
|
||||
const { width, height } = image;
|
||||
|
||||
if (force || (!isEqual(this.state, state) && !this.isLoading)) {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
try {
|
||||
if ('image_name' in image) {
|
||||
await this.updateImageSourceByImageName(image.image_name);
|
||||
} else {
|
||||
await this.updateImageSourceByDataURL(image.dataURL);
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
this.konva.image?.setAttrs({ width, height });
|
||||
this.state = state;
|
||||
return true;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
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 { selectCanvasStagingAreaSlice, selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import type { CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
@@ -20,10 +19,10 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
subscriptions: Set<() => void> = new Set();
|
||||
konva: { group: Konva.Group };
|
||||
image: CanvasObjectImage | null;
|
||||
selectedImage: StagingAreaImage | null;
|
||||
mutex = new Mutex();
|
||||
|
||||
$shouldShowStagedImage = atom<boolean>(true);
|
||||
$isStaging = atom<boolean>(false);
|
||||
$isStaging = atom(true); //TODO: wire up to queue?
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
super();
|
||||
@@ -37,30 +36,13 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
|
||||
this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }) };
|
||||
this.image = null;
|
||||
this.selectedImage = null;
|
||||
|
||||
/**
|
||||
* 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));
|
||||
/**
|
||||
* When the staging redux state changes (i.e. when the selected staged image is changed, or we add/discard a staged
|
||||
* image), we need to re-render the staging area.
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasStagingAreaSlice, 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.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging, oldIsStaging) => {
|
||||
this.$isStaging.set(isStaging);
|
||||
if (isStaging && !oldIsStaging) {
|
||||
this.$shouldShowStagedImage.set(true);
|
||||
}
|
||||
this.$shouldShowStagedImage.listen(() => {
|
||||
this.render();
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -68,71 +50,56 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render();
|
||||
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
|
||||
};
|
||||
|
||||
render = async () => {
|
||||
this.log.trace('Rendering staging area');
|
||||
const stagingArea = this.manager.stateApi.runSelector(selectCanvasStagingAreaSlice);
|
||||
|
||||
const { x, y } = this.manager.stateApi.getBbox().rect;
|
||||
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
|
||||
|
||||
this.selectedImage = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
|
||||
this.konva.group.position({ x, y });
|
||||
|
||||
if (this.selectedImage) {
|
||||
const { imageDTO } = this.selectedImage;
|
||||
const image = imageDTOToImageWithDims(imageDTO);
|
||||
|
||||
/**
|
||||
* When the final output image of a generation is received, we should clear that generation's last progress image.
|
||||
*
|
||||
* It's possible that we have already rendered the progress image from the next generation before the output image
|
||||
* from the previous is fully loaded/rendered. This race condition results in a flicker:
|
||||
* - LAST GENERATION: Render the final progress image
|
||||
* - LAST GENERATION: Start loading the final output image...
|
||||
* - NEXT GENERATION: Render the first progress image
|
||||
* - LAST GENERATION: ...Finish loading the final output image & render it, clearing the progress image <-- Flicker!
|
||||
* - NEXT GENERATION: Render the next progress image
|
||||
*
|
||||
* We can detect the race condition by stashing the session ID of the last progress image when we begin loading
|
||||
* that session's output image. After we render it, if the progress image's session ID is the same as the one we
|
||||
* stashed, we know that we have not yet gotten that next generation's first progress image. We can clear the
|
||||
* progress image without causing a flicker.
|
||||
*/
|
||||
const lastProgressEventSessionId = this.manager.progressImage.$lastProgressEvent.get()?.session_id;
|
||||
const hideProgressIfSameSession = () => {
|
||||
const currentProgressEventSessionId = this.manager.progressImage.$lastProgressEvent.get()?.session_id;
|
||||
if (lastProgressEventSessionId === currentProgressEventSessionId) {
|
||||
this.manager.progressImage.$lastProgressEvent.set(null);
|
||||
}
|
||||
getImageFromSrc = (
|
||||
{ type, data }: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string },
|
||||
width: number,
|
||||
height: number
|
||||
): CanvasImageState['image'] => {
|
||||
if (type === 'imageName') {
|
||||
return {
|
||||
image_name: data,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
if (!this.image) {
|
||||
this.image = new CanvasObjectImage(
|
||||
{
|
||||
id: 'staging-area-image',
|
||||
type: 'image',
|
||||
image,
|
||||
},
|
||||
this
|
||||
);
|
||||
await this.image.update(this.image.state, true);
|
||||
this.konva.group.add(this.image.konva.group);
|
||||
hideProgressIfSameSession();
|
||||
} else if (this.image.isLoading) {
|
||||
// noop - just wait for the image to load
|
||||
} else if (this.image.state.image.image_name !== image.image_name) {
|
||||
await this.image.update({ ...this.image.state, image }, true);
|
||||
hideProgressIfSameSession();
|
||||
} else if (this.image.isError) {
|
||||
hideProgressIfSameSession();
|
||||
}
|
||||
this.image.konva.group.visible(shouldShowStagedImage);
|
||||
} else {
|
||||
this.image?.destroy();
|
||||
this.image = null;
|
||||
return {
|
||||
dataURL: data,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
render = async (imageSrc?: { type: 'imageName'; data: string } | { type: 'dataURL'; data: string }) => {
|
||||
const release = await this.mutex.acquire();
|
||||
try {
|
||||
this.log.trace('Rendering staging area');
|
||||
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
|
||||
|
||||
this.konva.group.position({ x, y });
|
||||
|
||||
if (imageSrc) {
|
||||
const image = this.getImageFromSrc(imageSrc, width, height);
|
||||
if (!this.image) {
|
||||
this.image = new CanvasObjectImage({ id: 'staging-area-image', type: 'image', image }, this);
|
||||
await this.image.update(this.image.state, true);
|
||||
this.konva.group.add(this.image.konva.group);
|
||||
} else if (this.image.isLoading || this.image.isError) {
|
||||
// noop
|
||||
} else {
|
||||
await this.image.update({ ...this.image.state, image }, true);
|
||||
}
|
||||
this.image.konva.group.visible(shouldShowStagedImage);
|
||||
} else {
|
||||
this.image?.destroy();
|
||||
this.image = null;
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,7 +124,6 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
selectedImage: this.selectedImage,
|
||||
$shouldShowStagedImage: this.$shouldShowStagedImage.get(),
|
||||
$isStaging: this.$isStaging.get(),
|
||||
image: this.image?.repr() ?? null,
|
||||
|
||||
@@ -59,6 +59,13 @@ const zImageWithDims = z
|
||||
});
|
||||
export type ImageWithDims = z.infer<typeof zImageWithDims>;
|
||||
|
||||
const zImageWithDimsDataURL = z.object({
|
||||
dataURL: z.string(),
|
||||
width: z.number().int().positive(),
|
||||
height: z.number().int().positive(),
|
||||
});
|
||||
export type ImageWithDimsDataURL = z.infer<typeof zImageWithDimsDataURL>;
|
||||
|
||||
const zBeginEndStepPct = z
|
||||
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
|
||||
.refine(([begin, end]) => begin < end, {
|
||||
@@ -231,7 +238,7 @@ export type CanvasRectState = z.infer<typeof zCanvasRectState>;
|
||||
const zCanvasImageState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('image'),
|
||||
image: zImageWithDims,
|
||||
image: z.union([zImageWithDims, zImageWithDimsDataURL]),
|
||||
});
|
||||
export type CanvasImageState = z.infer<typeof zCanvasImageState>;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ChatGPT4oReferenceImageConfig,
|
||||
ControlLoRAConfig,
|
||||
ControlNetConfig,
|
||||
Dimensions,
|
||||
FLUXReduxConfig,
|
||||
ImageWithDims,
|
||||
IPAdapterConfig,
|
||||
@@ -34,6 +35,22 @@ export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<Ca
|
||||
};
|
||||
};
|
||||
|
||||
export const imageNameToImageObject = (
|
||||
imageName: string,
|
||||
dimensions: Dimensions,
|
||||
overrides?: Partial<CanvasImageState>
|
||||
): CanvasImageState => {
|
||||
return {
|
||||
id: getPrefixedId('image'),
|
||||
type: 'image',
|
||||
image: {
|
||||
image_name: imageName,
|
||||
...dimensions,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
||||
image_name,
|
||||
width,
|
||||
|
||||
@@ -38,15 +38,15 @@ export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: U
|
||||
);
|
||||
|
||||
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name)
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name)
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name)
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
|
||||
|
||||
@@ -13,11 +13,11 @@ export const useCancelCurrentQueueItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const currentQueueItemId = useMemo(() => queueStatus?.queue.item_id, [queueStatus?.queue.item_id]);
|
||||
const cancelQueueItem = useCallback(async () => {
|
||||
if (!currentQueueItemId) {
|
||||
if (currentQueueItemId !== null || currentQueueItemId !== undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await trigger(currentQueueItemId).unwrap();
|
||||
await trigger({ item_id: currentQueueItemId }).unwrap();
|
||||
toast({
|
||||
id: 'QUEUE_CANCEL_SUCCEEDED',
|
||||
title: t('queue.cancelSucceeded'),
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useCancelQueueItem = (item_id: number) => {
|
||||
const { t } = useTranslation();
|
||||
const cancelQueueItem = useCallback(async () => {
|
||||
try {
|
||||
await trigger(item_id).unwrap();
|
||||
await trigger({ item_id }).unwrap();
|
||||
toast({
|
||||
id: 'QUEUE_CANCEL_SUCCEEDED',
|
||||
title: t('queue.cancelSucceeded'),
|
||||
|
||||
@@ -674,6 +674,13 @@ export const uploadImage = (arg: UploadImageArg): Promise<ImageDTO> => {
|
||||
return req.unwrap();
|
||||
};
|
||||
|
||||
export const copyImage = async (imageName: string, uploadImageArg: Omit<UploadImageArg, 'file'>): Promise<ImageDTO> => {
|
||||
const originalImageDTO = await getImageDTO(imageName);
|
||||
const file = await imageDTOToFile(originalImageDTO);
|
||||
const imageDTO = await uploadImage({ file, ...uploadImageArg });
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
export const uploadImages = async (args: UploadImageArg[]): Promise<ImageDTO[]> => {
|
||||
const { dispatch } = getStore();
|
||||
const results = await Promise.allSettled(
|
||||
|
||||
@@ -79,7 +79,12 @@ export const queueApi = api.injectEndpoints({
|
||||
url: buildQueueUrl('prune'),
|
||||
method: 'PUT',
|
||||
}),
|
||||
invalidatesTags: ['SessionQueueStatus', 'BatchStatus', { type: 'SessionQueueItem', id: LIST_TAG }],
|
||||
invalidatesTags: [
|
||||
'SessionQueueStatus',
|
||||
'BatchStatus',
|
||||
{ type: 'SessionQueueItem', id: LIST_TAG },
|
||||
{ type: 'SessionQueueItem', id: LIST_ALL_TAG },
|
||||
],
|
||||
}),
|
||||
clearQueue: build.mutation<
|
||||
paths['/api/v1/queue/{queue_id}/clear']['put']['responses']['200']['content']['application/json'],
|
||||
@@ -176,9 +181,9 @@ export const queueApi = api.injectEndpoints({
|
||||
}),
|
||||
cancelQueueItem: build.mutation<
|
||||
paths['/api/v1/queue/{queue_id}/i/{item_id}/cancel']['put']['responses']['200']['content']['application/json'],
|
||||
number
|
||||
{ item_id: number }
|
||||
>({
|
||||
query: (item_id) => ({
|
||||
query: ({ item_id }) => ({
|
||||
url: buildQueueUrl(`i/${item_id}/cancel`),
|
||||
method: 'PUT',
|
||||
}),
|
||||
@@ -219,7 +224,7 @@ export const queueApi = api.injectEndpoints({
|
||||
];
|
||||
},
|
||||
}),
|
||||
cancelByBatchDestination: build.mutation<
|
||||
cancelByDestination: build.mutation<
|
||||
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
|
||||
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query']
|
||||
>({
|
||||
@@ -319,6 +324,24 @@ export const queueApi = api.injectEndpoints({
|
||||
return tags;
|
||||
},
|
||||
}),
|
||||
deleteQueueItem: build.mutation<void, { item_id: number }>({
|
||||
query: ({ item_id }) => ({
|
||||
url: buildQueueUrl(`i/${item_id}`),
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: (result, error, { item_id }) => [{ type: 'SessionQueueItem', id: item_id }],
|
||||
}),
|
||||
deleteQueueItemsByDestination: build.mutation<void, { destination: string }>({
|
||||
query: ({ destination }) => ({
|
||||
url: buildQueueUrl(`d/${destination}`),
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: (result, error, { destination }) => [
|
||||
{ type: 'QueueCountsByDestination', id: destination },
|
||||
{ type: 'SessionQueueItem', id: LIST_TAG },
|
||||
{ type: 'SessionQueueItem', id: LIST_ALL_TAG },
|
||||
],
|
||||
}),
|
||||
getQueueCountsByDestination: build.query<
|
||||
paths['/api/v1/queue/{queue_id}/counts_by_destination']['get']['responses']['200']['content']['application/json'],
|
||||
paths['/api/v1/queue/{queue_id}/counts_by_destination']['get']['parameters']['query']
|
||||
@@ -345,6 +368,9 @@ export const {
|
||||
useListQueueItemsQuery,
|
||||
useListAllQueueItemsQuery,
|
||||
useCancelQueueItemMutation,
|
||||
useCancelByDestinationMutation,
|
||||
useDeleteQueueItemMutation,
|
||||
useDeleteQueueItemsByDestinationMutation,
|
||||
useGetBatchStatusQuery,
|
||||
useGetCurrentQueueItemQuery,
|
||||
useGetQueueCountsByDestinationQuery,
|
||||
|
||||
@@ -1438,7 +1438,11 @@ export type paths = {
|
||||
get: operations["get_queue_item"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
/**
|
||||
* Delete Queue Item
|
||||
* @description Deletes a queue item
|
||||
*/
|
||||
delete: operations["delete_queue_item"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
@@ -1484,6 +1488,26 @@ export type paths = {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/queue/{queue_id}/d/{destination}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
/**
|
||||
* Delete By Destination
|
||||
* @description Deletes all items with the given destination
|
||||
*/
|
||||
delete: operations["delete_by_destination"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/workflows/i/{workflow_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -5879,6 +5903,17 @@ export type components = {
|
||||
*/
|
||||
deleted_images: string[];
|
||||
};
|
||||
/**
|
||||
* DeleteByDestinationResult
|
||||
* @description Result of deleting by a destination
|
||||
*/
|
||||
DeleteByDestinationResult: {
|
||||
/**
|
||||
* Deleted
|
||||
* @description Number of queue items deleted
|
||||
*/
|
||||
deleted: number;
|
||||
};
|
||||
/** DeleteImagesFromListResult */
|
||||
DeleteImagesFromListResult: {
|
||||
/** Deleted Images */
|
||||
@@ -24784,6 +24819,40 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
delete_queue_item: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description The queue id to perform this operation on */
|
||||
queue_id: string;
|
||||
/** @description The queue item to delete */
|
||||
item_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
cancel_queue_item: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -24853,6 +24922,40 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
delete_by_destination: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description The queue id to query */
|
||||
queue_id: string;
|
||||
/** @description The destination to query */
|
||||
destination: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeleteByDestinationResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_workflow: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
Reference in New Issue
Block a user