diff --git a/.gitignore b/.gitignore
index fba699d5e99..c48e47ca26b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -190,3 +190,5 @@ installer/update.bat
installer/update.sh
installer/InvokeAI-Installer/
.aider*
+
+.claude/
diff --git a/invokeai/frontend/web/.prettierignore b/invokeai/frontend/web/.prettierignore
index 0f53a0b0a8c..658baa261ed 100644
--- a/invokeai/frontend/web/.prettierignore
+++ b/invokeai/frontend/web/.prettierignore
@@ -14,3 +14,4 @@ static/
src/theme/css/overlayscrollbars.css
src/theme_/css/overlayscrollbars.css
pnpm-lock.yaml
+.claude
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 452c666cd93..152540d582a 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -56,7 +56,7 @@
"chakra-react-select": "^4.9.2",
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
- "dockview": "^4.4.0",
+ "dockview": "^4.4.1",
"es-toolkit": "^1.39.7",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.0",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index 6561702558f..bbd109e09a9 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -60,8 +60,8 @@ importers:
specifier: ^6.1.1
version: 6.1.1
dockview:
- specifier: ^4.4.0
- version: 4.4.0(react@18.3.1)
+ specifier: ^4.4.1
+ version: 4.4.1(react@18.3.1)
es-toolkit:
specifier: ^1.39.7
version: 1.39.7
@@ -2247,11 +2247,11 @@ packages:
discontinuous-range@1.0.0:
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
- dockview-core@4.4.0:
- resolution: {integrity: sha512-UsBJwS3lfZXM+gaTA+bJs8rAxLd7ZEmNcUf5CbKKhiPeKIPJrNCxXxTLcnQb3IXMJUGkE0aX1ZJ4BDaZGMtzlA==}
+ dockview-core@4.4.1:
+ resolution: {integrity: sha512-pDQPlVfDYDuN3zSebVUMVn2x21bpYPGD1ybGYrKJMI1KDkSQSqy57FJRJXi7yEnkcrmBUF0xEEo4d0Yi3j2vGA==}
- dockview@4.4.0:
- resolution: {integrity: sha512-cWi5R40R5kDky69vAqsKGznRx5tA0gk3Mdqe5aS2r4ollK951mWNJ/EeMmac+UP/juw4cbl0/APhXTV+EMnAbg==}
+ dockview@4.4.1:
+ resolution: {integrity: sha512-XEAwl+VYVZGkBd3hprF6kRLspWSF/hydbRHuV3KEg3BHev1i5xc+H+Kjp+u5DHTQ97klFAATl5MntNoVXQeg0w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -6601,11 +6601,11 @@ snapshots:
discontinuous-range@1.0.0: {}
- dockview-core@4.4.0: {}
+ dockview-core@4.4.1: {}
- dockview@4.4.0(react@18.3.1):
+ dockview@4.4.1(react@18.3.1):
dependencies:
- dockview-core: 4.4.0
+ dockview-core: 4.4.1
react: 18.3.1
doctrine@2.1.0:
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 1ad174863cb..755fc5e1be4 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -30,16 +30,16 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
}, [clearStorage]);
return (
-
-
+
+
{!didStudioInit && }
-
-
+
+
);
};
diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
index 9aa70a55122..0a21348e984 100644
--- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
@@ -2,6 +2,7 @@ import { useGlobalModifiersInit } from '@invoke-ai/ui-library';
import { setupListeners } from '@reduxjs/toolkit/query';
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
import { useStudioInitAction } from 'app/hooks/useStudioInitAction';
+import { useSyncLangDirection } from 'app/hooks/useSyncLangDirection';
import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus';
import { useLogger } from 'app/logging/useLogger';
import { useSyncLoggingConfig } from 'app/logging/useSyncLoggingConfig';
@@ -50,6 +51,7 @@ export const GlobalHookIsolator = memo(
useNavigationApi();
useDndMonitor();
useSyncNodeErrors();
+ useSyncLangDirection();
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
// and/or in progress canvas sessions.
diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
index 25ee01903bc..437cecd4925 100644
--- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
+++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
@@ -3,43 +3,39 @@ import 'overlayscrollbars/overlayscrollbars.css';
import '@xyflow/react/dist/base.css';
import 'common/components/OverlayScrollbars/overlayscrollbars.css';
-import { ChakraProvider, DarkMode, extendTheme, theme as _theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
+import { ChakraProvider, DarkMode, extendTheme, theme as baseTheme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { $direction } from 'app/hooks/useSyncLangDirection';
import type { ReactNode } from 'react';
-import { memo, useEffect, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
+import { memo, useMemo } from 'react';
type ThemeLocaleProviderProps = {
children: ReactNode;
};
-function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
- const { i18n } = useTranslation();
-
- const direction = i18n.dir();
-
- const theme = useMemo(() => {
- return extendTheme({
- ..._theme,
- direction,
- shadows: {
- ..._theme.shadows,
- selected:
- 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
- hoverSelected:
- 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
- hoverUnselected:
- 'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
- selectedForCompare:
- 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
- hoverSelectedForCompare:
- 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
- },
- });
- }, [direction]);
+const buildTheme = (direction: 'ltr' | 'rtl') => {
+ return extendTheme({
+ ...baseTheme,
+ direction,
+ shadows: {
+ ...baseTheme.shadows,
+ selected:
+ 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
+ hoverSelected:
+ 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
+ hoverUnselected:
+ 'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
+ selectedForCompare:
+ 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
+ hoverSelectedForCompare:
+ 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
+ },
+ });
+};
- useEffect(() => {
- document.body.dir = direction;
- }, [direction]);
+function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
+ const direction = useStore($direction);
+ const theme = useMemo(() => buildTheme(direction), [direction]);
return (
diff --git a/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts b/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts
new file mode 100644
index 00000000000..da1e0dbbcb3
--- /dev/null
+++ b/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts
@@ -0,0 +1,36 @@
+import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { atom } from 'nanostores';
+import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+/**
+ * Global atom storing the language direction, to be consumed by the Chakra theme.
+ *
+ * Why do we need this? We have a kind of catch-22:
+ * - The Chakra theme needs to know the language direction to apply the correct styles.
+ * - The language direction is determined by i18n and the language selection.
+ * - We want our error boundary to be themed.
+ * - It's possible that i18n can throw if the language selection is invalid or not supported.
+ *
+ * Previously, we had the logic in this file in the theme provider, which wrapped the error boundary. The error
+ * was properly themed. But then, if i18n threw in the theme provider, the error boundary does not catch the
+ * error. The app would crash to a white screen.
+ *
+ * We tried swapping the component hierarchy so that the error boundary wraps the theme provider, but then the
+ * error boundary isn't themed!
+ *
+ * The solution is to move this i18n direction logic out of the theme provider and into a hook that we can use
+ * within the error boundary. The error boundary will be themed, _and_ catch any i18n errors.
+ */
+export const $direction = atom<'ltr' | 'rtl'>('ltr');
+
+export const useSyncLangDirection = () => {
+ useAssertSingleton('useSyncLangDirection');
+ const { i18n, t } = useTranslation();
+
+ useEffect(() => {
+ const direction = i18n.dir();
+ $direction.set(direction);
+ document.body.dir = direction;
+ }, [i18n, t]);
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
deleted file mode 100644
index f38c37bb701..00000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ /dev/null
@@ -1,498 +0,0 @@
-import { useStore } from '@nanostores/react';
-import { useAppSelector, useAppStore } from 'app/store/storeHooks';
-import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
-import { loadImage } from 'features/controlLayers/konva/util';
-import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice';
-import {
- buildSelectCanvasQueueItems,
- canvasQueueItemDiscarded,
- canvasSessionReset,
- selectCanvasSessionId,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
-import type { ProgressImage } from 'features/nodes/types/common';
-import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores';
-import { atom, computed, effect, map, subscribeKeys } from 'nanostores';
-import type { PropsWithChildren } from 'react';
-import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { getImageDTOSafe } from 'services/api/endpoints/images';
-import { queueApi } from 'services/api/endpoints/queue';
-import type { ImageDTO, S } from 'services/api/types';
-import { $socket } from 'services/events/stores';
-import { assert, objectEntries } from 'tsafe';
-
-export type ProgressData = {
- itemId: number;
- progressEvent: S['InvocationProgressEvent'] | null;
- progressImage: ProgressImage | null;
- imageDTO: ImageDTO | null;
- imageLoaded: boolean;
-};
-
-const getInitialProgressData = (itemId: number): ProgressData => ({
- itemId,
- progressEvent: null,
- progressImage: null,
- imageDTO: null,
- imageLoaded: false,
-});
-
-export const useProgressData = ($progressData: ProgressDataMap, itemId: number): ProgressData => {
- const getInitialValue = useCallback(
- () => $progressData.get()[itemId] ?? getInitialProgressData(itemId),
- [$progressData, itemId]
- );
- const [value, setValue] = useState(getInitialValue);
- useEffect(() => {
- const unsub = subscribeKeys($progressData, [itemId], (data) => {
- const progressData = data[itemId];
- if (!progressData) {
- return;
- }
- setValue(progressData);
- });
- return () => {
- unsub();
- };
- }, [$progressData, itemId]);
-
- return value;
-};
-
-const setProgress = ($progressData: ProgressDataMap, data: S['InvocationProgressEvent']) => {
- const progressData = $progressData.get();
- const current = progressData[data.item_id];
- if (current) {
- const next = { ...current };
- next.progressEvent = data;
- if (data.image) {
- next.progressImage = data.image;
- }
- $progressData.set({
- ...progressData,
- [data.item_id]: next,
- });
- } else {
- $progressData.set({
- ...progressData,
- [data.item_id]: {
- itemId: data.item_id,
- progressEvent: data,
- progressImage: data.image ?? null,
- imageDTO: null,
- imageLoaded: false,
- },
- });
- }
-};
-
-export type ProgressDataMap = MapStore>;
-
-type CanvasSessionContextValue = {
- $items: Atom;
- $itemCount: Atom;
- $hasItems: Atom;
- $progressData: ProgressDataMap;
- $selectedItemId: WritableAtom;
- $selectedItem: Atom;
- $selectedItemIndex: Atom;
- $selectedItemOutputImageDTO: Atom;
- selectNext: () => void;
- selectPrev: () => void;
- selectFirst: () => void;
- selectLast: () => void;
- discard: (itemId: number) => void;
- discardAll: () => void;
-};
-
-const CanvasSessionContext = createContext(null);
-
-export const CanvasSessionContextProvider = memo(({ children }: PropsWithChildren) => {
- /**
- * For best performance and interop with the Canvas, which is outside react but needs to interact with the react
- * app, all canvas session state is packaged as nanostores atoms. The trickiest part is syncing the queue items
- * with a nanostores atom.
- */
-
- /**
- * App store
- */
- const store = useAppStore();
-
- const sessionId = useAppSelector(selectCanvasSessionId);
-
- const socket = useStore($socket);
-
- /**
- * Track the last completed item. Used to implement autoswitch.
- */
- const $lastCompletedItemId = useState(() => atom(null))[0];
-
- /**
- * Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
- * and kept in sync with it via a redux subscription.
- */
- const $items = useState(() => atom([]))[0];
-
- /**
- * An ephemeral store of progress events and images for all items in the current session.
- */
- const $progressData = useState(() => map>({}))[0];
-
- /**
- * The currently selected queue item's ID, or null if one is not selected.
- */
- 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.
- */
- const $hasItems = useState(() => computed([$items], (items) => items.length > 0))[0];
-
- /**
- * Whether there are any pending or in-progress items. Computed from the queue items array.
- */
- const $isPending = useState(() =>
- computed([$items], (items) => items.some((item) => item.status === 'pending' || item.status === 'in_progress'))
- )[0];
-
- /**
- * The currently selected queue item, or null if one is not selected.
- */
- const $selectedItem = useState(() =>
- computed([$items, $selectedItemId], (items, selectedItemId) => {
- if (items.length === 0) {
- return null;
- }
- if (selectedItemId === null) {
- return null;
- }
- return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
- })
- )[0];
-
- /**
- * The currently selected queue item's index in the list of items, or null if one is not selected.
- */
- const $selectedItemIndex = useState(() =>
- computed([$items, $selectedItemId], (items, selectedItemId) => {
- if (items.length === 0) {
- return null;
- }
- if (selectedItemId === null) {
- return null;
- }
- return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
- })
- )[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 $selectedItemOutputImageDTO = useState(() =>
- computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
- if (selectedItemId === null) {
- return null;
- }
- const datum = progressData[selectedItemId];
- if (!datum) {
- return null;
- }
- return datum.imageDTO;
- })
- )[0];
-
- /**
- * A redux selector to select all queue items from the RTK Query cache.
- */
- const selectQueueItems = useMemo(() => buildSelectCanvasQueueItems(sessionId), [sessionId]);
-
- const discard = useCallback(
- (itemId: number) => {
- store.dispatch(canvasQueueItemDiscarded({ itemId }));
- },
- [store]
- );
-
- const discardAll = useCallback(() => {
- store.dispatch(canvasSessionReset());
- }, [store]);
-
- 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) {
- return;
- }
-
- const onProgress = (data: S['InvocationProgressEvent']) => {
- if (data.destination !== sessionId) {
- return;
- }
- setProgress($progressData, data);
- };
-
- const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== sessionId) {
- return;
- }
- if (data.status === 'completed') {
- /**
- * There is an unpleasant bit of indirection here. When an item is completed, and auto-switch is set to
- * switch_on_finish, we want to load the image and switch to it. In this socket handler, we don't have
- * access to the full queue item, which we need to get the output image and load it. We get the full
- * queue items as part of the list query, so it's rather inefficient to fetch it again here.
- *
- * To reduce the number of extra network requests, we instead store this item as the last completed item.
- * Then in the progress data sync effect, we process the queue item load its image.
- */
- $lastCompletedItemId.set(data.item_id);
- }
- if (data.status === 'in_progress' && selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start') {
- $selectedItemId.set(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);
- };
- }, [$progressData, $selectedItemId, sessionId, socket, $lastCompletedItemId, store]);
-
- // Set up state subscriptions and effects
- useEffect(() => {
- let _prevItems: readonly S['SessionQueueItem'][] = [];
- // Seed the $items atom with the initial query cache state
- $items.set(selectQueueItems(store.getState()));
-
- // Manually keep the $items atom in sync as the query cache is updated
- const unsubReduxSyncToItemsAtom = store.subscribe(() => {
- const prevItems = $items.get();
- const items = selectQueueItems(store.getState());
- if (items !== prevItems) {
- _prevItems = prevItems;
- $items.set(items);
- }
- });
-
- // Handle cases that could result in a nonexistent queue item being selected.
- const unsubEnsureSelectedItemIdExists = effect([$items, $selectedItemId], (items, selectedItemId) => {
- if (items.length === 0) {
- // If there are no items, cannot have a selected item.
- $selectedItemId.set(null);
- } else if (selectedItemId === null && items.length > 0) {
- // If there is no selected item but there are items, select the first one.
- $selectedItemId.set(items[0]?.item_id ?? null);
- return;
- } else if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
- // 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.
- let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
- if (prevIndex >= items.length) {
- prevIndex = items.length - 1;
- }
- const nextItem = items[prevIndex];
- $selectedItemId.set(nextItem?.item_id ?? null);
- }
-
- if (items !== _prevItems) {
- _prevItems = items;
- }
- });
-
- // Sync progress data - remove canceled/failed items, update progress data with new images, and load images
- const unsubSyncProgressData = $items.subscribe(async (items) => {
- const progressData = $progressData.get();
-
- const toDelete: number[] = [];
- const toUpdate: ProgressData[] = [];
-
- for (const [id, datum] of objectEntries(progressData)) {
- if (!datum) {
- toDelete.push(id);
- continue;
- }
- const item = items.find(({ item_id }) => item_id === datum.itemId);
- if (!item) {
- toDelete.push(datum.itemId);
- } else if (item.status === 'canceled' || item.status === 'failed') {
- toUpdate.push({
- ...datum,
- progressEvent: null,
- progressImage: null,
- imageDTO: null,
- });
- }
- }
-
- for (const item of items) {
- const datum = progressData[item.item_id];
-
- if (datum?.imageDTO) {
- continue;
- }
- const outputImageName = getOutputImageName(item);
- if (!outputImageName) {
- continue;
- }
- const imageDTO = await getImageDTOSafe(outputImageName);
- if (!imageDTO) {
- continue;
- }
-
- // This is the load logic mentioned in the comment in the QueueItemStatusChangedEvent handler above.
- if (
- $lastCompletedItemId.get() === item.item_id &&
- selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
- ) {
- loadImage(imageDTO.image_url, true).then(() => {
- $selectedItemId.set(item.item_id);
- $lastCompletedItemId.set(null);
- });
- }
-
- toUpdate.push({
- ...getInitialProgressData(item.item_id),
- ...datum,
- imageDTO,
- });
- }
-
- for (const itemId of toDelete) {
- $progressData.setKey(itemId, undefined);
- }
-
- for (const datum of toUpdate) {
- $progressData.setKey(datum.itemId, datum);
- }
- });
-
- // Create an RTK Query subscription. Without this, the query cache selector will never return anything bc RTK
- // doesn't know we care about it.
- const { unsubscribe: unsubQueueItemsQuery } = store.dispatch(
- queueApi.endpoints.listAllQueueItems.initiate({ destination: sessionId })
- );
-
- // Clean up all subscriptions and top-level (i.e. non-computed/derived state)
- return () => {
- unsubQueueItemsQuery();
- unsubReduxSyncToItemsAtom();
- unsubEnsureSelectedItemIdExists();
- unsubSyncProgressData();
- $items.set([]);
- $progressData.set({});
- $selectedItemId.set(null);
- };
- }, [$items, $progressData, $selectedItemId, selectQueueItems, sessionId, store, $lastCompletedItemId]);
-
- const value = useMemo(
- () => ({
- $items,
- $hasItems,
- $isPending,
- $progressData,
- $selectedItemId,
- $selectedItem,
- $selectedItemIndex,
- $selectedItemOutputImageDTO,
- $itemCount,
- selectNext,
- selectPrev,
- selectFirst,
- selectLast,
- discard,
- discardAll,
- }),
- [
- $items,
- $hasItems,
- $isPending,
- $progressData,
- $selectedItem,
- $selectedItemId,
- $selectedItemIndex,
- $selectedItemOutputImageDTO,
- $itemCount,
- selectNext,
- selectPrev,
- selectFirst,
- selectLast,
- discard,
- discardAll,
- ]
- );
-
- return {children};
-});
-CanvasSessionContextProvider.displayName = 'CanvasSessionContextProvider';
-
-export const useCanvasSessionContext = () => {
- const ctx = useContext(CanvasSessionContext);
- assert(ctx !== null, "'useCanvasSessionContext' must be used within a CanvasSessionContextProvider");
- return ctx;
-};
-
-export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
- const ctx = useCanvasSessionContext();
- const $imageDTO = useState(() =>
- computed([ctx.$progressData], (progressData) => progressData[item.item_id]?.imageDTO ?? null)
- )[0];
- const imageDTO = useStore($imageDTO);
-
- return imageDTO;
-};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemCircularProgress.tsx
similarity index 81%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemCircularProgress.tsx
index 80da7d250d4..0a92106a6e6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemCircularProgress.tsx
@@ -1,10 +1,11 @@
import type { CircularProgressProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { CircularProgress, Tooltip } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
-import { getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
+import { getProgressMessage } from 'features/controlLayers/components/StagingArea/shared';
import { memo } from 'react';
import type { S } from 'services/api/types';
+import { useProgressDatum } from './context';
+
const circleStyles: SystemStyleObject = {
circle: {
transitionProperty: 'none',
@@ -18,8 +19,7 @@ const circleStyles: SystemStyleObject = {
type Props = { itemId: number; status: S['SessionQueueItem']['status'] } & CircularProgressProps;
export const QueueItemCircularProgress = memo(({ itemId, status, ...rest }: Props) => {
- const { $progressData } = useCanvasSessionContext();
- const { progressEvent } = useProgressData($progressData, itemId);
+ const { progressEvent } = useProgressDatum(itemId);
if (status !== 'in_progress') {
return null;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemNumber.tsx
similarity index 81%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemNumber.tsx
index 33686a5c836..60734b2fd7e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemNumber.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemNumber.tsx
@@ -1,8 +1,9 @@
import type { TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
-import { DROP_SHADOW } from 'features/controlLayers/components/SimpleSession/shared';
import { memo } from 'react';
+import { DROP_SHADOW } from './shared';
+
export const QueueItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) => {
return {`#${number}`};
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx
similarity index 70%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx
index ee31573d32c..b84dff0ed30 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx
@@ -1,25 +1,23 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import {
- useCanvasSessionContext,
- useOutputImageDTO,
- useProgressData,
-} from 'features/controlLayers/components/SimpleSession/context';
-import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
-import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
-import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
-import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
-import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
+import { QueueItemCircularProgress } from 'features/controlLayers/components/StagingArea/QueueItemCircularProgress';
+import { QueueItemProgressImage } from 'features/controlLayers/components/StagingArea/QueueItemProgressImage';
+import { QueueItemStatusLabel } from 'features/controlLayers/components/StagingArea/QueueItemStatusLabel';
+import { getQueueItemElementId } from 'features/controlLayers/components/StagingArea/shared';
import {
selectStagingAreaAutoSwitch,
settingsStagingAreaAutoSwitchChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { DndImage } from 'features/dnd/DndImage';
import { toast } from 'features/toast/toast';
-import { memo, useCallback } from 'react';
+import { memo, useCallback, useMemo } from 'react';
import type { S } from 'services/api/types';
+import { useOutputImageDTO, useStagingAreaContext } from './context';
+import { QueueItemNumber } from './QueueItemNumber';
+
const sx = {
cursor: 'pointer',
userSelect: 'none',
@@ -41,19 +39,19 @@ const sx = {
type Props = {
item: S['SessionQueueItem'];
index: number;
- isSelected: boolean;
};
-export const QueueItemPreviewMini = memo(({ item, isSelected, index }: Props) => {
+export const QueueItemPreviewMini = memo(({ item, index }: Props) => {
+ const ctx = useStagingAreaContext();
const dispatch = useAppDispatch();
- const ctx = useCanvasSessionContext();
- const { imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
- const imageDTO = useOutputImageDTO(item);
+ const $isSelected = useMemo(() => ctx.buildIsSelectedComputed(item.item_id), [ctx, item.item_id]);
+ const isSelected = useStore($isSelected);
+ const imageDTO = useOutputImageDTO(item.item_id);
const autoSwitch = useAppSelector(selectStagingAreaAutoSwitch);
const onClick = useCallback(() => {
- ctx.$selectedItemId.set(item.item_id);
- }, [ctx.$selectedItemId, item.item_id]);
+ ctx.select(item.item_id);
+ }, [ctx, item.item_id]);
const onDoubleClick = useCallback(() => {
if (autoSwitch !== 'off') {
@@ -74,7 +72,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, index }: Props) =>
>
{imageDTO && }
- {!imageLoaded && }
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemProgressImage.tsx
similarity index 72%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemProgressImage.tsx
index 2ea3dd827eb..20eb6c1ae4d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemProgressImage.tsx
@@ -1,13 +1,13 @@
import type { ImageProps } from '@invoke-ai/ui-library';
import { Image } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
+import { useProgressDatum } from './context';
+
type Props = { itemId: number } & ImageProps;
export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
- const ctx = useCanvasSessionContext();
- const { progressImage } = useProgressData(ctx.$progressData, itemId);
+ const { progressImage } = useProgressDatum(itemId);
if (!progressImage) {
return null;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemStatusLabel.tsx
similarity index 84%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemStatusLabel.tsx
index 35fb76b28d3..d1acbd9487a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemStatusLabel.tsx
@@ -1,16 +1,16 @@
import type { TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
import type { S } from 'services/api/types';
+import { useProgressDatum } from './context';
+
type Props = { item: S['SessionQueueItem'] } & TextProps;
export const QueueItemStatusLabel = memo(({ item, ...rest }: Props) => {
- const ctx = useCanvasSessionContext();
- const { progressImage, imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
+ const { progressImage } = useProgressDatum(item.item_id);
- if (progressImage || imageLoaded) {
+ if (progressImage) {
return null;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx
index 8d98eec2914..f9fc483eea5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx
@@ -1,5 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import {
selectStagingAreaAutoSwitch,
settingsStagingAreaAutoSwitchChanged,
@@ -8,6 +10,9 @@ import { memo, useCallback } from 'react';
import { PiCaretLineRightBold, PiCaretRightBold, PiMoonBold } from 'react-icons/pi';
export const StagingAreaAutoSwitchButtons = memo(() => {
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
const autoSwitch = useAppSelector(selectStagingAreaAutoSwitch);
const dispatch = useAppDispatch();
@@ -29,6 +34,7 @@ export const StagingAreaAutoSwitchButtons = memo(() => {
icon={}
colorScheme={autoSwitch === 'off' ? 'invokeBlue' : 'base'}
onClick={onClickOff}
+ isDisabled={!shouldShowStagedImage}
/>
{
icon={}
colorScheme={autoSwitch === 'switch_on_start' ? 'invokeBlue' : 'base'}
onClick={onClickSwitchOnStart}
+ isDisabled={!shouldShowStagedImage}
/>
{
icon={}
colorScheme={autoSwitch === 'switch_on_finish' ? 'invokeBlue' : 'base'}
onClick={onClickSwitchOnFinished}
+ isDisabled={!shouldShowStagedImage}
/>
>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx
similarity index 75%
rename from invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx
index aed3f9c8750..962ad027ccf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx
@@ -1,16 +1,16 @@
import { Box, Flex, forwardRef } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { logger } from 'app/logging/logger';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
-import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { QueueItemPreviewMini } from 'features/controlLayers/components/StagingArea/QueueItemPreviewMini';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import type { CSSProperties, RefObject } from 'react';
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import type { Components, ItemContent, ListRange, VirtuosoHandle, VirtuosoProps } from 'react-virtuoso';
+import { memo, useCallback, useEffect, useRef, useState } from 'react';
+import type { Components, ComputeItemKey, ItemContent, ListRange, VirtuosoHandle, VirtuosoProps } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
import type { S } from 'services/api/types';
+import { useStagingAreaContext } from './context';
import { getQueueItemElementId } from './shared';
const log = logger('system');
@@ -20,8 +20,6 @@ const virtuosoStyles = {
height: '72px',
} satisfies CSSProperties;
-type VirtuosoContext = { selectedItemId: number | null };
-
/**
* Scroll the item at the given index into view if it is not currently visible.
*/
@@ -132,41 +130,35 @@ const useScrollableStagingArea = (rootRef: RefObject) => {
};
export const StagingAreaItemsList = memo(() => {
- const canvasManager = useCanvasManagerSafe();
- const ctx = useCanvasSessionContext();
+ const canvasManager = useCanvasManager();
+
+ const ctx = useStagingAreaContext();
const virtuosoRef = useRef(null);
const rangeRef = useRef({ startIndex: 0, endIndex: 0 });
const rootRef = useRef(null);
const items = useStore(ctx.$items);
- const selectedItemId = useStore(ctx.$selectedItemId);
- const context = useMemo(() => ({ selectedItemId }), [selectedItemId]);
const scrollerRef = useScrollableStagingArea(rootRef);
useEffect(() => {
- if (!canvasManager) {
- return;
- }
-
- return canvasManager.stagingArea.connectToSession(ctx.$items, ctx.$selectedItemId, ctx.$progressData);
- }, [canvasManager, ctx.$progressData, ctx.$selectedItemId, ctx.$items]);
+ return canvasManager.stagingArea.connectToSession(ctx.$items, ctx.$selectedItem);
+ }, [canvasManager, ctx.$progressData, ctx.$items, ctx.$selectedItem]);
useEffect(() => {
- return ctx.$selectedItemIndex.listen((index) => {
- if (!virtuosoRef.current) {
+ return ctx.$selectedItemIndex.listen((selectedItemIndex) => {
+ if (selectedItemIndex === null) {
return;
}
-
- if (!rootRef.current) {
+ if (!virtuosoRef.current) {
return;
}
- if (index === null) {
+ if (!rootRef.current) {
return;
}
- scrollIntoView(index, rootRef.current, virtuosoRef.current, rangeRef.current);
+ scrollIntoView(selectedItemIndex, rootRef.current, virtuosoRef.current, rangeRef.current);
});
}, [ctx.$selectedItemIndex]);
@@ -176,40 +168,46 @@ export const StagingAreaItemsList = memo(() => {
return (
-
+
ref={virtuosoRef}
- context={context}
data={items}
horizontalDirection
style={virtuosoStyles}
+ computeItemKey={computeItemKey}
+ increaseViewportBy={2048}
itemContent={itemContent}
components={components}
rangeChanged={onRangeChanged}
// Virtuoso expects the ref to be of HTMLElement | null | Window, but overlayscrollbars doesn't allow Window
- scrollerRef={scrollerRef as VirtuosoProps['scrollerRef']}
+ scrollerRef={scrollerRef as VirtuosoProps['scrollerRef']}
/>
);
});
StagingAreaItemsList.displayName = 'StagingAreaItemsList';
-const itemContent: ItemContent = (index, item, { selectedItemId }) => (
-
+const computeItemKey: ComputeItemKey = (_, item: S['SessionQueueItem']) => {
+ return item.item_id;
+};
+
+const itemContent: ItemContent = (index, item) => (
+
);
const listSx = {
'& > * + *': {
pl: 2,
},
+ '&[data-disabled="true"]': {
+ filter: 'grayscale(1) opacity(0.5)',
+ },
};
-const components: Components = {
+const components: Components = {
List: forwardRef(({ context: _, ...rest }, ref) => {
- return ;
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
+ return ;
}),
};
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 64df68cf267..ff73a37fc94 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -1,6 +1,5 @@
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
-import { useStore } from '@nanostores/react';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
@@ -10,17 +9,13 @@ import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/
import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
-import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { StagingAreaAutoSwitchButtons } from './StagingAreaAutoSwitchButtons';
export const StagingAreaToolbar = memo(() => {
- const canvasManager = useCanvasManager();
- const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
-
- const ctx = useCanvasSessionContext();
+ const ctx = useStagingAreaContext();
useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true });
useHotkeys('meta+right', ctx.selectLast, { preventDefault: true });
@@ -28,22 +23,22 @@ export const StagingAreaToolbar = memo(() => {
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 2161b3f1389..c18ca5cac19 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -1,65 +1,32 @@
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 { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
-import { canvasSessionReset, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
-import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
-import { imageNameToImageObject } from 'features/controlLayers/store/util';
import { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
-import { memo, useCallback } from 'react';
+import { memo } 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 ctx = useStagingAreaContext();
const canvasManager = useCanvasManager();
- const canvasSessionId = useAppSelector(selectCanvasSessionId);
- const bboxRect = useAppSelector(selectBboxRect);
- const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
- const selectedItemImageDTO = useStore(ctx.$selectedItemOutputImageDTO);
const cancelQueueItemsByDestination = useCancelQueueItemsByDestination();
+ const acceptSelectedIsEnabled = useStore(ctx.$acceptSelectedIsEnabled);
const { t } = useTranslation();
- const acceptSelected = useCallback(() => {
- if (!selectedItemImageDTO) {
- return;
- }
- const { x, y, width, height } = bboxRect;
- const imageObject = imageNameToImageObject(selectedItemImageDTO.image_name, { width, height });
- const overrides: Partial = {
- position: { x, y },
- objects: [imageObject],
- };
-
- dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
- dispatch(canvasSessionReset());
- cancelQueueItemsByDestination.trigger(canvasSessionId, { withToast: false });
- }, [
- selectedItemImageDTO,
- bboxRect,
- dispatch,
- selectedEntityIdentifier?.type,
- cancelQueueItemsByDestination,
- canvasSessionId,
- ]);
-
useHotkeys(
['enter'],
- acceptSelected,
+ ctx.acceptSelected,
{
preventDefault: true,
- enabled: isCanvasFocused && shouldShowStagedImage && selectedItemImageDTO !== null,
+ enabled: isCanvasFocused && shouldShowStagedImage && acceptSelectedIsEnabled,
},
- [isCanvasFocused, shouldShowStagedImage, selectedItemImageDTO]
+ [ctx.acceptSelected, isCanvasFocused, shouldShowStagedImage, acceptSelectedIsEnabled]
);
return (
@@ -67,9 +34,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
tooltip={`${t('common.accept')} (Enter)`}
aria-label={`${t('common.accept')} (Enter)`}
icon={}
- onClick={acceptSelected}
+ onClick={ctx.acceptSelected}
colorScheme="invokeBlue"
- isDisabled={!selectedItemImageDTO || !shouldShowStagedImage || cancelQueueItemsByDestination.isDisabled}
+ isDisabled={!acceptSelectedIsEnabled || !shouldShowStagedImage || cancelQueueItemsByDestination.isDisabled}
isLoading={cancelQueueItemsByDestination.isLoading}
/>
);
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 0c5f94206e2..467c80d6ff7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -1,31 +1,28 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
-import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { useStore } from '@nanostores/react';
+import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
-import { memo, useCallback } from 'react';
+import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
-export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
- const ctx = useCanvasSessionContext();
+export const StagingAreaToolbarDiscardAllButton = memo(() => {
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
+ const ctx = useStagingAreaContext();
const { t } = useTranslation();
const cancelQueueItemsByDestination = useCancelQueueItemsByDestination();
- const canvasSessionId = useAppSelector(selectCanvasSessionId);
-
- const discardAll = useCallback(() => {
- ctx.discardAll();
- cancelQueueItemsByDestination.trigger(canvasSessionId, { withToast: false });
- }, [cancelQueueItemsByDestination, ctx, canvasSessionId]);
return (
}
- onClick={discardAll}
+ onClick={ctx.discardAll}
colorScheme="error"
- isDisabled={isDisabled || cancelQueueItemsByDestination.isDisabled}
+ isDisabled={cancelQueueItemsByDestination.isDisabled || !shouldShowStagedImage}
isLoading={cancelQueueItemsByDestination.isLoading}
/>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
index 78cace1cb00..8f912ca52e2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -1,34 +1,30 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
-import { memo, useCallback } from 'react';
+import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
-export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
- const ctx = useCanvasSessionContext();
+export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
+ const ctx = useStagingAreaContext();
const cancelQueueItem = useCancelQueueItem();
- const selectedItemId = useStore(ctx.$selectedItemId);
+ const discardSelectedIsEnabled = useStore(ctx.$discardSelectedIsEnabled);
const { t } = useTranslation();
- const discardSelected = useCallback(async () => {
- if (selectedItemId === null) {
- return;
- }
- ctx.discard(selectedItemId);
- await cancelQueueItem.trigger(selectedItemId, { withToast: false });
- }, [selectedItemId, ctx, cancelQueueItem]);
-
return (
}
- onClick={discardSelected}
+ onClick={ctx.discardSelected}
colorScheme="invokeBlue"
- isDisabled={selectedItemId === null || cancelQueueItem.isDisabled || isDisabled}
+ isDisabled={!discardSelectedIsEnabled || cancelQueueItem.isDisabled || !shouldShowStagedImage}
isLoading={cancelQueueItem.isLoading}
/>
);
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 446dea8836b..a47a84cd8fe 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
@@ -1,23 +1,27 @@
import { Button } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useMemo } from 'react';
export const StagingAreaToolbarImageCountButton = memo(() => {
- const ctx = useCanvasSessionContext();
- const selectItemIndex = useStore(ctx.$selectedItemIndex);
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
+ const ctx = useStagingAreaContext();
+ const selectedItem = useStore(ctx.$selectedItem);
const itemCount = useStore(ctx.$itemCount);
const counterText = useMemo(() => {
- if (itemCount > 0 && selectItemIndex !== null) {
- return `${selectItemIndex + 1} of ${itemCount}`;
+ if (itemCount > 0 && selectedItem !== null) {
+ return `${selectedItem.index + 1} of ${itemCount}`;
} else {
return `0 of 0`;
}
- }, [itemCount, selectItemIndex]);
+ }, [itemCount, selectedItem]);
return (
-