mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 15:04:59 -05:00
refactor(ui): simplify canvas session logic
This commit is contained in:
@@ -2087,9 +2087,9 @@
|
||||
"resetCanvasLayers": "Reset Canvas Layers",
|
||||
"resetGenerationSettings": "Reset Generation Settings",
|
||||
"replaceCurrent": "Replace Current",
|
||||
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
|
||||
"referenceImageEmptyStateWithCanvasOptions": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this Reference Image or <PullBboxButton>pull the bounding box into this Reference Image</PullBboxButton> to get started.",
|
||||
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton> or drag an image from the <GalleryButton>gallery</GalleryButton> onto this Reference Image to get started.",
|
||||
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the gallery onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
|
||||
"referenceImageEmptyStateWithCanvasOptions": "<UploadButton>Upload an image</UploadButton>, drag an image from the gallery onto this Reference Image or <PullBboxButton>pull the bounding box into this Reference Image</PullBboxButton> to get started.",
|
||||
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton> or drag an image from the gallery onto this Reference Image to get started.",
|
||||
"uploadOrDragAnImage": "Drag an image from the gallery or <UploadButton>upload an image</UploadButton>.",
|
||||
"imageNoise": "Image Noise",
|
||||
"denoiseLimit": "Denoise Limit",
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
|
||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
|
||||
import {
|
||||
NewCanvasSessionDialog,
|
||||
NewGallerySessionDialog,
|
||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
@@ -50,8 +46,6 @@ export const GlobalModalIsolator = memo(() => {
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
<NewGallerySessionDialog />
|
||||
<NewCanvasSessionDialog />
|
||||
<ImageContextMenu />
|
||||
<FullscreenDropzone />
|
||||
<VideosModal />
|
||||
|
||||
@@ -21,7 +21,6 @@ import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/st
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { LAUNCHPAD_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -165,7 +164,6 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
// Go to the generate tab, open the launchpad
|
||||
await navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID);
|
||||
store.dispatch(paramsReset());
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
break;
|
||||
case 'canvas':
|
||||
// Go to the canvas tab, open the launchpad
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
||||
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
|
||||
import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
|
||||
@@ -152,7 +152,8 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
if (modelBase !== state.params.model?.base) {
|
||||
// Sync generate tab settings whenever the model base changes
|
||||
dispatch(syncedToOptimalDimension());
|
||||
if (!selectIsStaging(state)) {
|
||||
const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
|
||||
if (!isStaging) {
|
||||
// Canvas tab only syncs if not staging
|
||||
dispatch(bboxSyncedToOptimalDimension());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { isNil } from 'es-toolkit';
|
||||
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import {
|
||||
heightChanged,
|
||||
setCfgRescaleMultiplier,
|
||||
@@ -115,7 +115,8 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
}
|
||||
const setSizeOptions = { updateAspectRatio: true, clamp: true };
|
||||
|
||||
const isStaging = selectIsStaging(getState());
|
||||
const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
|
||||
|
||||
const activeTab = selectActiveTab(getState());
|
||||
if (activeTab === 'generate') {
|
||||
if (isParameterWidth(width)) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
|
||||
import { usePullBboxIntoLayer } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { replaceCanvasEntityObjectsWithImage } from 'features/imageActions/actions';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -21,9 +20,6 @@ export const ControlLayerSettingsEmptyState = memo(() => {
|
||||
[dispatch, entityIdentifier, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
const pullBboxIntoLayer = usePullBboxIntoLayer(entityIdentifier);
|
||||
|
||||
const components = useMemo(
|
||||
@@ -31,14 +27,11 @@ export const ControlLayerSettingsEmptyState = memo(() => {
|
||||
UploadButton: (
|
||||
<Button isDisabled={isBusy} size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />
|
||||
),
|
||||
GalleryButton: (
|
||||
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
PullBboxButton: (
|
||||
<Button onClick={pullBboxIntoLayer} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
}),
|
||||
[isBusy, onClickGalleryButton, pullBboxIntoLayer, uploadApi]
|
||||
[isBusy, pullBboxIntoLayer, uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import {
|
||||
selectSystemShouldConfirmOnNewSession,
|
||||
shouldConfirmOnNewSessionToggled,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const [useNewGallerySessionDialog] = buildUseBoolean(false);
|
||||
const [useNewCanvasSessionDialog] = buildUseBoolean(false);
|
||||
|
||||
const useNewGallerySession = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const newSessionDialog = useNewGallerySessionDialog();
|
||||
|
||||
const newGallerySessionImmediate = useCallback(() => {
|
||||
dispatch(generateSessionReset());
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
|
||||
const newGallerySessionWithDialog = useCallback(() => {
|
||||
if (shouldConfirmOnNewSession) {
|
||||
newSessionDialog.setTrue();
|
||||
return;
|
||||
}
|
||||
newGallerySessionImmediate();
|
||||
}, [newGallerySessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
|
||||
|
||||
return { newGallerySessionImmediate, newGallerySessionWithDialog };
|
||||
};
|
||||
|
||||
const useNewCanvasSession = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const newSessionDialog = useNewCanvasSessionDialog();
|
||||
|
||||
const newCanvasSessionImmediate = useCallback(() => {
|
||||
dispatch(canvasSessionReset());
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
}, [dispatch]);
|
||||
|
||||
const newCanvasSessionWithDialog = useCallback(() => {
|
||||
if (shouldConfirmOnNewSession) {
|
||||
newSessionDialog.setTrue();
|
||||
return;
|
||||
}
|
||||
|
||||
newCanvasSessionImmediate();
|
||||
}, [newCanvasSessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
|
||||
|
||||
return { newCanvasSessionImmediate, newCanvasSessionWithDialog };
|
||||
};
|
||||
|
||||
export const NewGallerySessionDialog = memo(() => {
|
||||
useAssertSingleton('NewGallerySessionDialog');
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dialog = useNewGallerySessionDialog();
|
||||
const { newGallerySessionImmediate } = useNewGallerySession();
|
||||
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirm = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={dialog.isTrue}
|
||||
onClose={dialog.setFalse}
|
||||
title={t('controlLayers.newGallerySession')}
|
||||
acceptCallback={newGallerySessionImmediate}
|
||||
acceptButtonText={t('common.ok')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('controlLayers.newGallerySessionDesc')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Checkbox isChecked={!shouldConfirmOnNewSession} onChange={onToggleConfirm} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
|
||||
NewGallerySessionDialog.displayName = 'NewGallerySessionDialog';
|
||||
|
||||
export const NewCanvasSessionDialog = memo(() => {
|
||||
useAssertSingleton('NewCanvasSessionDialog');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dialog = useNewCanvasSessionDialog();
|
||||
const { newCanvasSessionImmediate } = useNewCanvasSession();
|
||||
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirm = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={dialog.isTrue}
|
||||
onClose={dialog.setFalse}
|
||||
title={t('controlLayers.newCanvasSession')}
|
||||
acceptCallback={newCanvasSessionImmediate}
|
||||
acceptButtonText={t('common.ok')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('controlLayers.newCanvasSessionDesc')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Checkbox isChecked={!shouldConfirmOnNewSession} onChange={onToggleConfirm} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
|
||||
NewCanvasSessionDialog.displayName = 'NewCanvasSessionDialog';
|
||||
@@ -6,7 +6,6 @@ import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { setGlobalReferenceImage } from 'features/imageActions/actions';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -22,9 +21,6 @@ export const RefImageNoImageState = memo(() => {
|
||||
[dispatch, id]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
|
||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
() => setGlobalReferenceImageDndTarget.getData({ id }),
|
||||
@@ -34,9 +30,8 @@ export const RefImageNoImageState = memo(() => {
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: <Button size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />,
|
||||
GalleryButton: <Button onClick={onClickGalleryButton} size="sm" variant="link" color="base.300" />,
|
||||
}),
|
||||
[onClickGalleryButton, uploadApi]
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { setGlobalReferenceImage } from 'features/imageActions/actions';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -25,9 +24,6 @@ export const RefImageNoImageStateWithCanvasOptions = memo(() => {
|
||||
[dispatch, id]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||
|
||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
@@ -40,14 +36,11 @@ export const RefImageNoImageStateWithCanvasOptions = memo(() => {
|
||||
UploadButton: (
|
||||
<Button isDisabled={isBusy} size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />
|
||||
),
|
||||
GalleryButton: (
|
||||
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
PullBboxButton: (
|
||||
<Button onClick={pullBboxIntoIPAdapter} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
}),
|
||||
[isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
|
||||
[isBusy, pullBboxIntoIPAdapter, uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dn
|
||||
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { setRegionalGuidanceReferenceImage } from 'features/imageActions/actions';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
@@ -31,9 +30,6 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
const onDeleteIPAdapter = useCallback(() => {
|
||||
dispatch(rgRefImageDeleted({ entityIdentifier, referenceImageId }));
|
||||
}, [dispatch, entityIdentifier, referenceImageId]);
|
||||
@@ -53,14 +49,11 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
|
||||
UploadButton: (
|
||||
<Button isDisabled={isBusy} size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />
|
||||
),
|
||||
GalleryButton: (
|
||||
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
PullBboxButton: (
|
||||
<Button onClick={pullBboxIntoIPAdapter} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
}),
|
||||
[isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
|
||||
[isBusy, pullBboxIntoIPAdapter, uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -149,8 +149,8 @@ export const StagingAreaItemsList = memo(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
return canvasManager.stagingArea.connectToSession(ctx.$selectedItemId, ctx.$progressData, ctx.$isPending);
|
||||
}, [canvasManager, ctx.$progressData, ctx.$selectedItemId, ctx.$isPending]);
|
||||
return canvasManager.stagingArea.connectToSession(ctx.$items, ctx.$selectedItemId, ctx.$progressData);
|
||||
}, [canvasManager, ctx.$progressData, ctx.$selectedItemId, ctx.$items]);
|
||||
|
||||
useEffect(() => {
|
||||
return ctx.$selectedItemIndex.listen((index) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import {
|
||||
buildSelectSessionQueueItems,
|
||||
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';
|
||||
@@ -86,11 +87,9 @@ const setProgress = ($progressData: ProgressDataMap, data: S['InvocationProgress
|
||||
export type ProgressDataMap = MapStore<Record<number, ProgressData | undefined>>;
|
||||
|
||||
type CanvasSessionContextValue = {
|
||||
session: { id: string; type: 'simple' | 'advanced' };
|
||||
$items: Atom<S['SessionQueueItem'][]>;
|
||||
$itemCount: Atom<number>;
|
||||
$hasItems: Atom<boolean>;
|
||||
$isPending: Atom<boolean>;
|
||||
$progressData: ProgressDataMap;
|
||||
$selectedItemId: WritableAtom<number | null>;
|
||||
$selectedItem: Atom<S['SessionQueueItem'] | null>;
|
||||
@@ -107,465 +106,446 @@ type CanvasSessionContextValue = {
|
||||
|
||||
const CanvasSessionContext = createContext<CanvasSessionContextValue | null>(null);
|
||||
|
||||
export const CanvasSessionContextProvider = memo(
|
||||
({ id, type, children }: PropsWithChildren<{ id: string; type: 'simple' | 'advanced' }>) => {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const session = useMemo(() => ({ type, id }), [type, id]);
|
||||
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();
|
||||
/**
|
||||
* App store
|
||||
*/
|
||||
const store = useAppStore();
|
||||
|
||||
const socket = useStore($socket);
|
||||
const sessionId = useAppSelector(selectCanvasSessionId);
|
||||
|
||||
/**
|
||||
* Track the last completed item. Used to implement autoswitch.
|
||||
*/
|
||||
const $lastCompletedItemId = useState(() => atom<number | null>(null))[0];
|
||||
const socket = useStore($socket);
|
||||
|
||||
/**
|
||||
* Track the last started item. Used to implement autoswitch.
|
||||
*/
|
||||
const $lastStartedItemId = useState(() => atom<number | null>(null))[0];
|
||||
/**
|
||||
* Track the last completed item. Used to implement autoswitch.
|
||||
*/
|
||||
const $lastCompletedItemId = useState(() => atom<number | null>(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<S['SessionQueueItem'][]>([]))[0];
|
||||
/**
|
||||
* Track the last started item. Used to implement autoswitch.
|
||||
*/
|
||||
const $lastStartedItemId = useState(() => atom<number | null>(null))[0];
|
||||
|
||||
/**
|
||||
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
|
||||
* output images have fully loaded.
|
||||
*/
|
||||
const $lastLoadedItemId = useState(() => atom<number | null>(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<S['SessionQueueItem'][]>([]))[0];
|
||||
|
||||
/**
|
||||
* An ephemeral store of progress events and images for all items in the current session.
|
||||
*/
|
||||
const $progressData = useState(() => map<StoreValue<ProgressDataMap>>({}))[0];
|
||||
/**
|
||||
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
|
||||
* output images have fully loaded.
|
||||
*/
|
||||
const $lastLoadedItemId = useState(() => atom<number | null>(null))[0];
|
||||
|
||||
/**
|
||||
* The currently selected queue item's ID, or null if one is not selected.
|
||||
*/
|
||||
const $selectedItemId = useState(() => atom<number | null>(null))[0];
|
||||
/**
|
||||
* An ephemeral store of progress events and images for all items in the current session.
|
||||
*/
|
||||
const $progressData = useState(() => map<StoreValue<ProgressDataMap>>({}))[0];
|
||||
|
||||
/**
|
||||
* The number of items. Computed from the queue items array.
|
||||
*/
|
||||
const $itemCount = useState(() => computed([$items], (items) => items.length))[0];
|
||||
/**
|
||||
* The currently selected queue item's ID, or null if one is not selected.
|
||||
*/
|
||||
const $selectedItemId = useState(() => atom<number | null>(null))[0];
|
||||
|
||||
/**
|
||||
* Whether there are any items. Computed from the queue items array.
|
||||
*/
|
||||
const $hasItems = useState(() => computed([$items], (items) => items.length > 0))[0];
|
||||
/**
|
||||
* The number of items. Computed from the queue items array.
|
||||
*/
|
||||
const $itemCount = useState(() => computed([$items], (items) => items.length))[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];
|
||||
/**
|
||||
* Whether there are any items. Computed from the queue items array.
|
||||
*/
|
||||
const $hasItems = useState(() => computed([$items], (items) => items.length > 0))[0];
|
||||
|
||||
/**
|
||||
* The currently selected queue item, or null if one is not selected.
|
||||
*/
|
||||
const $selectedItem = useState(() =>
|
||||
computed([$items, $selectedItemId], (items, selectedItemId) => {
|
||||
/**
|
||||
* 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]);
|
||||
|
||||
const onImageLoad = useCallback(
|
||||
(itemId: number) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[itemId];
|
||||
if (current) {
|
||||
const next = { ...current, imageLoaded: true };
|
||||
$progressData.setKey(itemId, next);
|
||||
} else {
|
||||
$progressData.setKey(itemId, {
|
||||
...getInitialProgressData(itemId),
|
||||
imageLoaded: true,
|
||||
});
|
||||
}
|
||||
if (
|
||||
$lastCompletedItemId.get() === itemId &&
|
||||
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
|
||||
) {
|
||||
$selectedItemId.set(itemId);
|
||||
$lastCompletedItemId.set(null);
|
||||
}
|
||||
},
|
||||
[$lastCompletedItemId, $progressData, $selectedItemId, store]
|
||||
);
|
||||
|
||||
// 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') {
|
||||
$lastCompletedItemId.set(data.item_id);
|
||||
}
|
||||
if (data.status === 'in_progress') {
|
||||
$lastStartedItemId.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);
|
||||
};
|
||||
}, [$lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, sessionId, socket]);
|
||||
|
||||
// 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, $lastStartedItemId],
|
||||
(items, selectedItemId, lastStartedItemId) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
// 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 (
|
||||
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start' &&
|
||||
items.findIndex(({ item_id }) => item_id === lastStartedItemId) !== -1
|
||||
) {
|
||||
$selectedItemId.set(lastStartedItemId);
|
||||
$lastStartedItemId.set(null);
|
||||
} 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 (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 (items !== _prevItems) {
|
||||
_prevItems = items;
|
||||
}
|
||||
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(() => buildSelectSessionQueueItems(session.id), [session.id]);
|
||||
|
||||
const discard = useCallback(
|
||||
(itemId: number) => {
|
||||
store.dispatch(canvasQueueItemDiscarded({ itemId }));
|
||||
},
|
||||
[store]
|
||||
}
|
||||
);
|
||||
|
||||
const discardAll = useCallback(() => {
|
||||
store.dispatch(canvasSessionReset());
|
||||
}, [store]);
|
||||
// Clean up the progress data when a queue item is discarded.
|
||||
const unsubCleanUpProgressData = $items.subscribe(async (items) => {
|
||||
const progressData = $progressData.get();
|
||||
|
||||
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 toDelete: number[] = [];
|
||||
const toUpdate: ProgressData[] = [];
|
||||
|
||||
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]);
|
||||
|
||||
const onImageLoad = useCallback(
|
||||
(itemId: number) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[itemId];
|
||||
if (current) {
|
||||
const next = { ...current, imageLoaded: true };
|
||||
$progressData.setKey(itemId, next);
|
||||
} else {
|
||||
$progressData.setKey(itemId, {
|
||||
...getInitialProgressData(itemId),
|
||||
imageLoaded: true,
|
||||
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,
|
||||
});
|
||||
}
|
||||
if (
|
||||
$lastCompletedItemId.get() === itemId &&
|
||||
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
|
||||
) {
|
||||
$selectedItemId.set(itemId);
|
||||
$lastCompletedItemId.set(null);
|
||||
}
|
||||
},
|
||||
[$lastCompletedItemId, $progressData, $selectedItemId, store]
|
||||
);
|
||||
|
||||
// Set up socket listeners
|
||||
useEffect(() => {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onProgress = (data: S['InvocationProgressEvent']) => {
|
||||
if (data.destination !== session.id) {
|
||||
return;
|
||||
}
|
||||
setProgress($progressData, data);
|
||||
};
|
||||
for (const item of items) {
|
||||
const datum = progressData[item.item_id];
|
||||
|
||||
const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
|
||||
if (data.destination !== session.id) {
|
||||
return;
|
||||
}
|
||||
if (data.status === 'completed') {
|
||||
$lastCompletedItemId.set(data.item_id);
|
||||
}
|
||||
if (data.status === 'in_progress') {
|
||||
$lastStartedItemId.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);
|
||||
};
|
||||
}, [$lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, session.id, socket]);
|
||||
|
||||
// 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, $lastStartedItemId],
|
||||
(items, selectedItemId, lastStartedItemId) => {
|
||||
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 (
|
||||
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start' &&
|
||||
items.findIndex(({ item_id }) => item_id === lastStartedItemId) !== -1
|
||||
) {
|
||||
$selectedItemId.set(lastStartedItemId);
|
||||
$lastStartedItemId.set(null);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up the progress data when a queue item is discarded.
|
||||
const unsubCleanUpProgressData = $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);
|
||||
if (datum) {
|
||||
if (datum.imageDTO) {
|
||||
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,
|
||||
});
|
||||
const outputImageName = getOutputImageName(item);
|
||||
if (!outputImageName) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const datum = progressData[item.item_id];
|
||||
|
||||
if (datum) {
|
||||
if (datum.imageDTO) {
|
||||
continue;
|
||||
}
|
||||
const outputImageName = getOutputImageName(item);
|
||||
if (!outputImageName) {
|
||||
continue;
|
||||
}
|
||||
const imageDTO = await getImageDTOSafe(outputImageName);
|
||||
if (!imageDTO) {
|
||||
continue;
|
||||
}
|
||||
toUpdate.push({
|
||||
...datum,
|
||||
imageDTO,
|
||||
});
|
||||
} else {
|
||||
const outputImageName = getOutputImageName(item);
|
||||
if (!outputImageName) {
|
||||
continue;
|
||||
}
|
||||
const imageDTO = await getImageDTOSafe(outputImageName);
|
||||
if (!imageDTO) {
|
||||
continue;
|
||||
}
|
||||
toUpdate.push({
|
||||
...getInitialProgressData(item.item_id),
|
||||
imageDTO,
|
||||
});
|
||||
const imageDTO = await getImageDTOSafe(outputImageName);
|
||||
if (!imageDTO) {
|
||||
continue;
|
||||
}
|
||||
toUpdate.push({
|
||||
...datum,
|
||||
imageDTO,
|
||||
});
|
||||
} else {
|
||||
const outputImageName = getOutputImageName(item);
|
||||
if (!outputImageName) {
|
||||
continue;
|
||||
}
|
||||
const imageDTO = await getImageDTOSafe(outputImageName);
|
||||
if (!imageDTO) {
|
||||
continue;
|
||||
}
|
||||
toUpdate.push({
|
||||
...getInitialProgressData(item.item_id),
|
||||
imageDTO,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const itemId of toDelete) {
|
||||
$progressData.setKey(itemId, undefined);
|
||||
}
|
||||
for (const itemId of toDelete) {
|
||||
$progressData.setKey(itemId, undefined);
|
||||
}
|
||||
|
||||
for (const datum of toUpdate) {
|
||||
$progressData.setKey(datum.itemId, datum);
|
||||
}
|
||||
});
|
||||
for (const datum of toUpdate) {
|
||||
$progressData.setKey(datum.itemId, datum);
|
||||
}
|
||||
});
|
||||
|
||||
// We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
|
||||
// of fallback content and/or progress images. The only surefire way to determine when images have fully loaded
|
||||
// is via the image elements' `onLoad` callback. Images set `$lastLoadedItemId` to their queue item ID in their
|
||||
// `onLoad` handler, and we listen for that here. If auto-switch is enabled, we then switch the to the item.
|
||||
//
|
||||
// TODO: This isn't perfect... we set $lastLoadedItemId in the mini preview component, but the full view
|
||||
// component still needs to retrieve the image from the browser cache... can result in a flash of the progress
|
||||
// image...
|
||||
const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
|
||||
if (lastLoadedItemId === null) {
|
||||
return;
|
||||
}
|
||||
if (selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish') {
|
||||
$selectedItemId.set(lastLoadedItemId);
|
||||
}
|
||||
$lastLoadedItemId.set(null);
|
||||
});
|
||||
// We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
|
||||
// of fallback content and/or progress images. The only surefire way to determine when images have fully loaded
|
||||
// is via the image elements' `onLoad` callback. Images set `$lastLoadedItemId` to their queue item ID in their
|
||||
// `onLoad` handler, and we listen for that here. If auto-switch is enabled, we then switch the to the item.
|
||||
//
|
||||
// TODO: This isn't perfect... we set $lastLoadedItemId in the mini preview component, but the full view
|
||||
// component still needs to retrieve the image from the browser cache... can result in a flash of the progress
|
||||
// image...
|
||||
const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
|
||||
if (lastLoadedItemId === null) {
|
||||
return;
|
||||
}
|
||||
if (selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish') {
|
||||
$selectedItemId.set(lastLoadedItemId);
|
||||
}
|
||||
$lastLoadedItemId.set(null);
|
||||
});
|
||||
|
||||
// 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: session.id })
|
||||
);
|
||||
|
||||
// const unsubListener = store.dispatch(
|
||||
// addAppListener({
|
||||
// matcher: queueApi.endpoints.cancelQueueItem.matchFulfilled,
|
||||
// effect: ({ payload }, { getState }) => {
|
||||
// const { item_id } = payload;
|
||||
|
||||
// const items = selectQueueItems(getState());
|
||||
// if (items.length === 0) {
|
||||
// $selectedItemId.set(null);
|
||||
// } else if ($selectedItemId.get() === null) {
|
||||
// $selectedItemId.set(items[0].item_id);
|
||||
// }
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
|
||||
return () => {
|
||||
unsubHandleAutoSwitch();
|
||||
unsubQueueItemsQuery();
|
||||
unsubReduxSyncToItemsAtom();
|
||||
unsubEnsureSelectedItemIdExists();
|
||||
unsubCleanUpProgressData();
|
||||
$items.set([]);
|
||||
$progressData.set({});
|
||||
$selectedItemId.set(null);
|
||||
};
|
||||
}, [
|
||||
$items,
|
||||
$lastLoadedItemId,
|
||||
$lastStartedItemId,
|
||||
$progressData,
|
||||
$selectedItemId,
|
||||
selectQueueItems,
|
||||
session.id,
|
||||
store,
|
||||
]);
|
||||
|
||||
const value = useMemo<CanvasSessionContextValue>(
|
||||
() => ({
|
||||
session,
|
||||
$items,
|
||||
$hasItems,
|
||||
$isPending,
|
||||
$progressData,
|
||||
$selectedItemId,
|
||||
$selectedItem,
|
||||
$selectedItemIndex,
|
||||
$selectedItemOutputImageDTO,
|
||||
$itemCount,
|
||||
selectNext,
|
||||
selectPrev,
|
||||
selectFirst,
|
||||
selectLast,
|
||||
onImageLoad,
|
||||
discard,
|
||||
discardAll,
|
||||
}),
|
||||
[
|
||||
$items,
|
||||
$hasItems,
|
||||
$isPending,
|
||||
$progressData,
|
||||
$selectedItem,
|
||||
$selectedItemId,
|
||||
$selectedItemIndex,
|
||||
session,
|
||||
$selectedItemOutputImageDTO,
|
||||
$itemCount,
|
||||
selectNext,
|
||||
selectPrev,
|
||||
selectFirst,
|
||||
selectLast,
|
||||
onImageLoad,
|
||||
discard,
|
||||
discardAll,
|
||||
]
|
||||
// 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 })
|
||||
);
|
||||
|
||||
return <CanvasSessionContext.Provider value={value}>{children}</CanvasSessionContext.Provider>;
|
||||
}
|
||||
);
|
||||
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
|
||||
return () => {
|
||||
unsubHandleAutoSwitch();
|
||||
unsubQueueItemsQuery();
|
||||
unsubReduxSyncToItemsAtom();
|
||||
unsubEnsureSelectedItemIdExists();
|
||||
unsubCleanUpProgressData();
|
||||
$items.set([]);
|
||||
$progressData.set({});
|
||||
$selectedItemId.set(null);
|
||||
};
|
||||
}, [
|
||||
$items,
|
||||
$lastLoadedItemId,
|
||||
$lastStartedItemId,
|
||||
$progressData,
|
||||
$selectedItemId,
|
||||
selectQueueItems,
|
||||
sessionId,
|
||||
store,
|
||||
]);
|
||||
|
||||
const value = useMemo<CanvasSessionContextValue>(
|
||||
() => ({
|
||||
$items,
|
||||
$hasItems,
|
||||
$isPending,
|
||||
$progressData,
|
||||
$selectedItemId,
|
||||
$selectedItem,
|
||||
$selectedItemIndex,
|
||||
$selectedItemOutputImageDTO,
|
||||
$itemCount,
|
||||
selectNext,
|
||||
selectPrev,
|
||||
selectFirst,
|
||||
selectLast,
|
||||
onImageLoad,
|
||||
discard,
|
||||
discardAll,
|
||||
}),
|
||||
[
|
||||
$items,
|
||||
$hasItems,
|
||||
$isPending,
|
||||
$progressData,
|
||||
$selectedItem,
|
||||
$selectedItemId,
|
||||
$selectedItemIndex,
|
||||
$selectedItemOutputImageDTO,
|
||||
$itemCount,
|
||||
selectNext,
|
||||
selectPrev,
|
||||
selectFirst,
|
||||
selectLast,
|
||||
onImageLoad,
|
||||
discard,
|
||||
discardAll,
|
||||
]
|
||||
);
|
||||
|
||||
return <CanvasSessionContext.Provider value={value}>{children}</CanvasSessionContext.Provider>;
|
||||
});
|
||||
CanvasSessionContextProvider.displayName = 'CanvasSessionContextProvider';
|
||||
|
||||
export const useCanvasSessionContext = () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { canvasSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
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';
|
||||
@@ -19,6 +19,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const canvasSessionId = useAppSelector(selectCanvasSessionId);
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
@@ -41,14 +42,14 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
|
||||
dispatch(canvasSessionReset());
|
||||
cancelQueueItemsByDestination.trigger(ctx.session.id, { withToast: false });
|
||||
cancelQueueItemsByDestination.trigger(canvasSessionId, { withToast: false });
|
||||
}, [
|
||||
selectedItemImageDTO,
|
||||
bboxRect,
|
||||
dispatch,
|
||||
selectedEntityIdentifier?.type,
|
||||
cancelQueueItemsByDestination,
|
||||
ctx.session.id,
|
||||
canvasSessionId,
|
||||
]);
|
||||
|
||||
useHotkeys(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -9,11 +11,12 @@ export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisa
|
||||
const ctx = useCanvasSessionContext();
|
||||
const { t } = useTranslation();
|
||||
const cancelQueueItemsByDestination = useCancelQueueItemsByDestination();
|
||||
const canvasSessionId = useAppSelector(selectCanvasSessionId);
|
||||
|
||||
const discardAll = useCallback(() => {
|
||||
ctx.discardAll();
|
||||
cancelQueueItemsByDestination.trigger(ctx.session.id, { withToast: false });
|
||||
}, [cancelQueueItemsByDestination, ctx]);
|
||||
cancelQueueItemsByDestination.trigger(canvasSessionId, { withToast: false });
|
||||
}, [cancelQueueItemsByDestination, ctx, canvasSessionId]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { getFocusedRegion } from 'common/hooks/focus';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export function useCanvasDeleteLayerHotkey() {
|
||||
@@ -12,14 +12,13 @@ export function useCanvasDeleteLayerHotkey() {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const canvasRightPanelTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||
|
||||
const deleteSelectedLayer = useCallback(() => {
|
||||
if (selectedEntityIdentifier === null || isBusy || canvasRightPanelTab !== 'layers') {
|
||||
if (selectedEntityIdentifier === null || isBusy || getFocusedRegion() !== 'layers') {
|
||||
return;
|
||||
}
|
||||
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
|
||||
}, [canvasRightPanelTab, dispatch, isBusy, selectedEntityIdentifier]);
|
||||
}, [dispatch, isBusy, selectedEntityIdentifier]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'deleteSelected',
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
selectIsolatedLayerPreview,
|
||||
selectIsolatedStagingPreview,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import {
|
||||
buildSelectIsSelected,
|
||||
getSelectIsTypeHidden,
|
||||
@@ -283,7 +282,7 @@ export abstract class CanvasEntityAdapterBase<T extends CanvasEntityState, U ext
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(selectIsolatedStagingPreview, this.syncVisibility)
|
||||
);
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectIsStaging, this.syncVisibility));
|
||||
this.subscriptions.add(this.manager.stagingArea.$isStaging.listen(this.syncVisibility));
|
||||
this.subscriptions.add(this.manager.stateApi.$filteringAdapter.listen(this.syncVisibility));
|
||||
this.subscriptions.add(this.manager.stateApi.$transformingAdapter.listen(this.syncVisibility));
|
||||
this.subscriptions.add(this.manager.stateApi.$segmentingAdapter.listen(this.syncVisibility));
|
||||
@@ -462,7 +461,7 @@ export abstract class CanvasEntityAdapterBase<T extends CanvasEntityState, U ext
|
||||
* This allows the user to easily see how the new generation fits in with the rest of the canvas without the
|
||||
* other layer types getting in the way.
|
||||
*/
|
||||
const isStaging = this.manager.stateApi.runSelector(selectIsStaging);
|
||||
const isStaging = this.manager.stagingArea.$isStaging.get();
|
||||
const isRasterLayer = isRasterLayerEntityIdentifier(this.entityIdentifier);
|
||||
if (isStaging && !isRasterLayer) {
|
||||
this.setVisibility(false);
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom, effect } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
// To get pixel sizes corresponding to our theme tokens, first find the theme token CSS var in browser dev tools.
|
||||
// For example `var(--invoke-space-8)` is equivalent to using `8` as a space prop in a component.
|
||||
@@ -121,14 +121,12 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
this.image = 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));
|
||||
|
||||
/**
|
||||
* Rerender when the image source changes.
|
||||
* Rerender when the anything important changes.
|
||||
*/
|
||||
this.subscriptions.add(this.$imageSrc.listen(this.render));
|
||||
this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render));
|
||||
this.subscriptions.add(this.$isPending.listen(this.render));
|
||||
this.subscriptions.add(this.$isStaging.listen(this.render));
|
||||
|
||||
/**
|
||||
* Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy
|
||||
@@ -138,8 +136,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
* 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);
|
||||
this.$isStaging.listen((isStaging, oldIsStaging) => {
|
||||
if (isStaging && !oldIsStaging) {
|
||||
this.$shouldShowStagedImage.set(true);
|
||||
}
|
||||
@@ -150,15 +147,17 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render();
|
||||
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
|
||||
};
|
||||
|
||||
connectToSession = (
|
||||
$items: Atom<S['SessionQueueItem'][]>,
|
||||
$selectedItemId: Atom<number | null>,
|
||||
$progressData: ProgressDataMap,
|
||||
$isPending: Atom<boolean>
|
||||
$progressData: ProgressDataMap
|
||||
) => {
|
||||
const cb = (selectedItemId: number | null, progressData: Record<number, ProgressData | undefined>) => {
|
||||
const imageSrcListener = (
|
||||
selectedItemId: number | null,
|
||||
progressData: Record<number, ProgressData | undefined>
|
||||
) => {
|
||||
if (!selectedItemId) {
|
||||
this.$imageSrc.set(null);
|
||||
return;
|
||||
@@ -176,20 +175,30 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
this.$imageSrc.set(null);
|
||||
}
|
||||
};
|
||||
const unsubImageSrc = effect([$selectedItemId, $progressData], imageSrcListener);
|
||||
|
||||
// Run the effect & forcibly render once to initialize
|
||||
cb($selectedItemId.get(), $progressData.get());
|
||||
const isPendingListener = (items: S['SessionQueueItem'][]) => {
|
||||
this.$isPending.set(items.some((item) => item.status === 'pending' || item.status === 'in_progress'));
|
||||
};
|
||||
const unsubIsPending = effect([$items], isPendingListener);
|
||||
|
||||
const isStagingListener = (items: S['SessionQueueItem'][]) => {
|
||||
this.$isStaging.set(items.length > 0);
|
||||
};
|
||||
const unsubIsStaging = effect([$items], isStagingListener);
|
||||
|
||||
// Run the effects & forcibly render once to initialize
|
||||
isStagingListener($items.get());
|
||||
isPendingListener($items.get());
|
||||
imageSrcListener($selectedItemId.get(), $progressData.get());
|
||||
this.render();
|
||||
|
||||
// Sync the $isPending flag with the computed
|
||||
const unsubIsPending = effect([$isPending], (isPending) => {
|
||||
this.$isPending.set(isPending);
|
||||
});
|
||||
|
||||
const unsubImageSrc = effect([$selectedItemId, $progressData], cb);
|
||||
|
||||
return () => {
|
||||
this.$isStaging.set(false);
|
||||
unsubIsStaging();
|
||||
this.$isPending.set(false);
|
||||
unsubIsPending();
|
||||
this.$imageSrc.set(null);
|
||||
unsubImageSrc();
|
||||
};
|
||||
};
|
||||
@@ -247,6 +256,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
this.konva.group.visible(shouldShowStagedImage && this.$isStaging.get());
|
||||
console.log({ isPending, isStaging: this.$isStaging.get(), shouldShowStagedImage, imageSrc });
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { useMemo } from 'react';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
generateSessionId: string | null;
|
||||
canvasSessionId: string | null;
|
||||
_version: 1;
|
||||
canvasSessionId: string;
|
||||
canvasDiscardedQueueItems: number[];
|
||||
};
|
||||
|
||||
const INITIAL_STATE: CanvasStagingAreaState = {
|
||||
generateSessionId: null,
|
||||
canvasSessionId: null,
|
||||
_version: 1,
|
||||
canvasSessionId: getPrefixedId('canvas'),
|
||||
canvasDiscardedQueueItems: [],
|
||||
};
|
||||
|
||||
@@ -23,46 +25,38 @@ export const canvasSessionSlice = createSlice({
|
||||
name: 'canvasSession',
|
||||
initialState: getInitialState(),
|
||||
reducers: {
|
||||
generateSessionIdChanged: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
state.generateSessionId = id;
|
||||
},
|
||||
generateSessionReset: (state) => {
|
||||
state.generateSessionId = null;
|
||||
},
|
||||
canvasQueueItemDiscarded: (state, action: PayloadAction<{ itemId: number }>) => {
|
||||
const { itemId } = action.payload;
|
||||
if (!state.canvasDiscardedQueueItems.includes(itemId)) {
|
||||
state.canvasDiscardedQueueItems.push(itemId);
|
||||
}
|
||||
},
|
||||
canvasSessionIdChanged: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
state.canvasSessionId = id;
|
||||
state.canvasDiscardedQueueItems = [];
|
||||
canvasSessionReset: {
|
||||
reducer: (state, action: PayloadAction<{ canvasSessionId: string }>) => {
|
||||
const { canvasSessionId } = action.payload;
|
||||
state.canvasSessionId = canvasSessionId;
|
||||
state.canvasDiscardedQueueItems = [];
|
||||
},
|
||||
prepare: () => {
|
||||
return {
|
||||
payload: {
|
||||
canvasSessionId: getPrefixedId('canvas'),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
canvasSessionReset: (state) => {
|
||||
state.canvasSessionId = null;
|
||||
state.canvasDiscardedQueueItems = [];
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(canvasReset, (state) => {
|
||||
state.canvasSessionId = null;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
generateSessionIdChanged,
|
||||
generateSessionReset,
|
||||
canvasSessionIdChanged,
|
||||
canvasSessionReset,
|
||||
canvasQueueItemDiscarded,
|
||||
} = canvasSessionSlice.actions;
|
||||
export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasSessionSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrate = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas');
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -74,13 +68,14 @@ export const canvasStagingAreaPersistConfig: PersistConfig<CanvasStagingAreaStat
|
||||
};
|
||||
|
||||
export const selectCanvasSessionSlice = (s: RootState) => s[canvasSessionSlice.name];
|
||||
|
||||
export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId);
|
||||
export const selectGenerateSessionId = createSelector(
|
||||
|
||||
const selectDiscardedItems = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
({ generateSessionId }) => generateSessionId
|
||||
({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems
|
||||
);
|
||||
export const buildSelectSessionQueueItems = (sessionId: string) =>
|
||||
|
||||
export const buildSelectCanvasQueueItems = (sessionId: string) =>
|
||||
createSelector(
|
||||
[queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems],
|
||||
({ data }, discardedItems) => {
|
||||
@@ -92,22 +87,18 @@ export const buildSelectSessionQueueItems = (sessionId: string) =>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const selectIsStaging = (state: RootState) => {
|
||||
const sessionId = selectCanvasSessionId(state);
|
||||
if (!sessionId) {
|
||||
return false;
|
||||
}
|
||||
const { data } = queueApi.endpoints.listAllQueueItems.select({ destination: sessionId })(state);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const discardedItems = selectDiscardedItems(state);
|
||||
return data.some(
|
||||
({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
|
||||
);
|
||||
export const useCanvasQueueItems = () => {
|
||||
const sessionId = useAppSelector(selectCanvasSessionId);
|
||||
const selector = useMemo(() => buildSelectCanvasQueueItems(sessionId), [sessionId]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
export const buildSelectIsStaging = (sessionId: string) =>
|
||||
createSelector([buildSelectCanvasQueueItems(sessionId)], (queueItems) => {
|
||||
return queueItems.length > 0;
|
||||
});
|
||||
export const useCanvasIsStaging = () => {
|
||||
const sessionId = useAppSelector(selectCanvasSessionId);
|
||||
const selector = useMemo(() => buildSelectIsStaging(sessionId), [sessionId]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
const selectDiscardedItems = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -12,7 +12,7 @@ export const useRecallAll = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const clearStylePreset = useClearStylePresetWithToast();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -8,7 +8,7 @@ import type { ImageDTO } from 'services/api/types';
|
||||
export const useRecallDimensions = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -11,7 +11,7 @@ import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
|
||||
export const useRecallRemix = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const clearStylePreset = useClearStylePresetWithToast();
|
||||
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import {
|
||||
selectIsChatGPT4o,
|
||||
selectIsFluxKontext,
|
||||
@@ -26,7 +26,7 @@ export const BboxAspectRatioSelect = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const id = useAppSelector(selectAspectRatioID);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const isImagen3 = useAppSelector(selectIsImagen3);
|
||||
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
|
||||
const isImagen4 = useAppSelector(selectIsImagen4);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsDownUpBold } from 'react-icons/pi';
|
||||
@@ -9,7 +9,7 @@ import { PiArrowsDownUpBold } from 'react-icons/pi';
|
||||
export const BboxSwapDimensionsButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(bboxDimensionsSwapped());
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
|
||||
export const useIsBboxSizeLocked = () => {
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const isApiModel = useIsApiModel();
|
||||
|
||||
return isApiModel || isStaging;
|
||||
|
||||
@@ -7,8 +7,7 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom
|
||||
import { withResult, withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { canvasSessionIdChanged, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
|
||||
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
|
||||
@@ -37,11 +36,7 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
|
||||
|
||||
const state = getState();
|
||||
|
||||
let destination = selectCanvasSessionId(state);
|
||||
if (destination === null) {
|
||||
destination = getPrefixedId('canvas');
|
||||
dispatch(canvasSessionIdChanged({ id: destination }));
|
||||
}
|
||||
const destination = selectCanvasSessionId(state);
|
||||
|
||||
const buildGraphResult = await withResultAsync(async () => {
|
||||
const model = state.params.model;
|
||||
|
||||
@@ -5,8 +5,6 @@ import type { AppStore } from 'app/store/store';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
|
||||
import { withResult, withResultAsync } from 'common/util/result';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { generateSessionIdChanged, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
|
||||
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
|
||||
@@ -36,11 +34,7 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
|
||||
|
||||
const state = getState();
|
||||
|
||||
let destination = selectGenerateSessionId(state);
|
||||
if (destination === null) {
|
||||
destination = getPrefixedId('generate');
|
||||
dispatch(generateSessionIdChanged({ id: destination }));
|
||||
}
|
||||
const destination = 'generate';
|
||||
|
||||
const buildGraphResult = await withResultAsync(async () => {
|
||||
const model = state.params.model;
|
||||
|
||||
@@ -17,7 +17,6 @@ import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasT
|
||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
|
||||
@@ -50,7 +49,6 @@ const canvasBgSx = {
|
||||
export const CanvasWorkspacePanel = memo(() => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
const canvasId = useAppSelector(selectCanvasSessionId);
|
||||
|
||||
const renderMenu = useCallback(() => {
|
||||
return <MenuContent />;
|
||||
@@ -103,13 +101,11 @@ export const CanvasWorkspacePanel = memo(() => {
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
{canvasId !== null && (
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasSessionContextProvider type="advanced" id={canvasId}>
|
||||
<StagingArea />
|
||||
</CanvasSessionContextProvider>
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasSessionContextProvider>
|
||||
<StagingArea />
|
||||
</CanvasSessionContextProvider>
|
||||
</CanvasManagerProviderGate>
|
||||
<Flex position="absolute" bottom={4}>
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StagingArea = memo(() => {
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
if (!isStaging) {
|
||||
return null;
|
||||
|
||||
@@ -4,4 +4,3 @@ import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
|
||||
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
|
||||
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
|
||||
export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);
|
||||
|
||||
@@ -12,9 +12,6 @@ export const uiSlice = createSlice({
|
||||
setActiveTab: (state, action: PayloadAction<UIState['activeTab']>) => {
|
||||
state.activeTab = action.payload;
|
||||
},
|
||||
activeTabCanvasRightPanelChanged: (state, action: PayloadAction<UIState['activeTabCanvasRightPanel']>) => {
|
||||
state.activeTabCanvasRightPanel = action.payload;
|
||||
},
|
||||
setShouldShowImageDetails: (state, action: PayloadAction<UIState['shouldShowImageDetails']>) => {
|
||||
state.shouldShowImageDetails = action.payload;
|
||||
},
|
||||
@@ -73,7 +70,6 @@ export const uiSlice = createSlice({
|
||||
|
||||
export const {
|
||||
setActiveTab,
|
||||
activeTabCanvasRightPanelChanged,
|
||||
setShouldShowImageDetails,
|
||||
setShouldShowProgressInViewer,
|
||||
accordionStateChanged,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { z } from 'zod/v4';
|
||||
|
||||
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
|
||||
export type TabName = z.infer<typeof zTabName>;
|
||||
const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']);
|
||||
|
||||
const zPartialDimensions = z.object({
|
||||
width: z.number().optional(),
|
||||
@@ -17,7 +16,6 @@ export type Serializable = z.infer<typeof zSerializable>;
|
||||
const zUIState = z.object({
|
||||
_version: z.literal(3).default(3),
|
||||
activeTab: zTabName.default('generate'),
|
||||
activeTabCanvasRightPanel: zCanvasRightPanelTabName.default('gallery'),
|
||||
shouldShowImageDetails: z.boolean().default(false),
|
||||
shouldShowProgressInViewer: z.boolean().default(true),
|
||||
accordions: z.record(z.string(), z.boolean()).default(() => ({})),
|
||||
|
||||
Reference in New Issue
Block a user