refactor(ui): metadata recall buttons & hotkeys (WIP)

This commit is contained in:
psychedelicious
2025-07-09 17:09:54 +10:00
parent bb50f4b8a2
commit 7f2dd22d47
22 changed files with 950 additions and 173 deletions

View File

@@ -1,13 +1,14 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { useImageDTO } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
@@ -26,64 +27,61 @@ export const GlobalImageHotkeys = memo(() => {
GlobalImageHotkeys.displayName = 'GlobalImageHotkeys';
const GlobalImageHotkeysInternal = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const imageActions = useImageActions(imageDTO);
const isStaging = useAppSelector(selectIsStaging);
const activeTab = useAppSelector(selectActiveTab);
const isUpscalingEnabled = useFeatureStatus('upscaling');
const isCanvasTabAndStaging = useMemo(() => activeTab === 'canvas' && isStaging, [activeTab, isStaging]);
const recallAll = useRecallAll(imageDTO);
const recallRemix = useRecallRemix(imageDTO);
const recallPrompts = useRecallPrompts(imageDTO);
const recallSeed = useRecallSeed(imageDTO);
const recallDimensions = useRecallDimensions(imageDTO);
const loadWorkflow = useLoadWorkflow(imageDTO);
useRegisteredHotkeys({
id: 'loadWorkflow',
category: 'viewer',
callback: imageActions.loadWorkflow,
options: { enabled: isGalleryFocused || isViewerFocused },
dependencies: [imageActions.loadWorkflow, isGalleryFocused, isViewerFocused],
callback: loadWorkflow.load,
options: { enabled: loadWorkflow.isEnabled },
dependencies: [loadWorkflow],
});
useRegisteredHotkeys({
id: 'recallAll',
category: 'viewer',
callback: imageActions.recallAll,
options: { enabled: !isCanvasTabAndStaging && (isGalleryFocused || isViewerFocused) },
dependencies: [imageActions.recallAll, isCanvasTabAndStaging, isGalleryFocused, isViewerFocused],
callback: recallAll.recall,
options: { enabled: recallAll.isEnabled },
dependencies: [recallAll],
});
useRegisteredHotkeys({
id: 'recallSeed',
category: 'viewer',
callback: imageActions.recallSeed,
options: { enabled: isGalleryFocused || isViewerFocused },
dependencies: [imageActions.recallSeed, isGalleryFocused, isViewerFocused],
callback: recallSeed.recall,
options: { enabled: recallSeed.isEnabled },
dependencies: [recallSeed],
});
useRegisteredHotkeys({
id: 'recallPrompts',
category: 'viewer',
callback: imageActions.recallPrompts,
options: { enabled: isGalleryFocused || isViewerFocused },
dependencies: [imageActions.recallPrompts, isGalleryFocused, isViewerFocused],
callback: recallPrompts.recall,
options: { enabled: recallPrompts.isEnabled },
dependencies: [recallPrompts],
});
useRegisteredHotkeys({
id: 'remix',
category: 'viewer',
callback: imageActions.remix,
options: { enabled: isGalleryFocused || isViewerFocused },
dependencies: [imageActions.remix, isGalleryFocused, isViewerFocused],
callback: recallRemix.recall,
options: { enabled: recallRemix.isEnabled },
dependencies: [recallRemix],
});
useRegisteredHotkeys({
id: 'useSize',
category: 'viewer',
callback: imageActions.recallSize,
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
dependencies: [imageActions.recallSize, isStaging, isGalleryFocused, isViewerFocused],
});
useRegisteredHotkeys({
id: 'runPostprocessing',
category: 'viewer',
callback: imageActions.upscale,
options: { enabled: isUpscalingEnabled && isViewerFocused },
dependencies: [isUpscalingEnabled, imageDTO, isViewerFocused],
callback: recallDimensions.recall,
options: { enabled: recallDimensions.isEnabled },
dependencies: [recallDimensions],
});
return null;
});

View File

@@ -0,0 +1,115 @@
import { useStore } from '@nanostores/react';
import { WrappedError } from 'common/util/result';
import type { Atom } from 'nanostores';
import { atom } from 'nanostores';
import { useCallback, useEffect, useMemo, useState } from 'react';
type SuccessState<T> = {
status: 'success';
value: T;
error: null;
};
type ErrorState = {
status: 'error';
value: null;
error: Error;
};
type PendingState = {
status: 'pending';
value: null;
error: null;
};
type IdleState = {
status: 'idle';
value: null;
error: null;
};
export type State<T> = IdleState | PendingState | SuccessState<T> | ErrorState;
type UseAsyncStateOptions = {
immediate?: boolean;
};
type UseAsyncReturn<T> = {
$state: Atom<State<T>>;
trigger: () => Promise<void>;
reset: () => void;
};
export const useAsyncState = <T>(execute: () => Promise<T>, options?: UseAsyncStateOptions): UseAsyncReturn<T> => {
const $state = useState(() =>
atom<State<T>>({
status: 'idle',
value: null,
error: null,
})
)[0];
const trigger = useCallback(async () => {
$state.set({
status: 'pending',
value: null,
error: null,
});
try {
const value = await execute();
$state.set({
status: 'success',
value,
error: null,
});
} catch (error) {
$state.set({
status: 'error',
value: null,
error: WrappedError.wrap(error),
});
}
}, [$state, execute]);
const reset = useCallback(() => {
$state.set({
status: 'idle',
value: null,
error: null,
});
}, [$state]);
useEffect(() => {
if (options?.immediate) {
trigger();
}
}, [options?.immediate, trigger]);
const api = useMemo(
() =>
({
$state,
trigger,
reset,
}) satisfies UseAsyncReturn<T>,
[$state, trigger, reset]
);
return api;
};
type UseAsyncReturnReactive<T> = {
state: State<T>;
trigger: () => Promise<void>;
reset: () => void;
};
export const useAsyncStateReactive = <T>(
execute: () => Promise<T>,
options?: UseAsyncStateOptions
): UseAsyncReturnReactive<T> => {
const { $state, trigger, reset } = useAsyncState(execute, options);
const state = useStore($state);
return { state, trigger, reset };
};

View File

@@ -1,10 +1,11 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppStore } from 'app/store/storeHooks';
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasQueueItemDiscarded, selectDiscardedItems } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
buildSelectSessionQueueItems,
canvasQueueItemDiscarded,
} 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';
@@ -217,25 +218,9 @@ export const CanvasSessionContextProvider = memo(
)[0];
/**
* A redux selector to select all queue items from the RTK Query cache. It's important that this returns stable
* references if possible to reduce re-renders. All derivations of the queue items (e.g. filtering out canceled
* items) should be done in a nanostores computed.
* A redux selector to select all queue items from the RTK Query cache.
*/
const selectQueueItems = useMemo(
() =>
createSelector(
[queueApi.endpoints.listAllQueueItems.select({ destination: session.id }), selectDiscardedItems],
({ data }, discardedItems) => {
if (!data) {
return EMPTY_ARRAY;
}
return data.filter(
({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
);
}
),
[session.id]
);
const selectQueueItems = useMemo(() => buildSelectSessionQueueItems(session.id), [session.id]);
const discard = useCallback(
(itemId: number) => {

View File

@@ -1,7 +1,9 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { EMPTY_ARRAY } from 'app/store/constants';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { canvasReset } from 'features/controlLayers/store/actions';
import { queueApi } from 'services/api/endpoints/queue';
type CanvasStagingAreaState = {
generateSessionId: string | null;
@@ -78,7 +80,30 @@ export const selectGenerateSessionId = createSelector(
selectCanvasSessionSlice,
({ generateSessionId }) => generateSessionId
);
export const selectIsStaging = createSelector(selectCanvasSessionId, (canvasSessionId) => canvasSessionId !== null);
export const buildSelectSessionQueueItems = (sessionId: string) =>
createSelector(
[queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems],
({ data }, discardedItems) => {
if (!data) {
return EMPTY_ARRAY;
}
return data.filter(
({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
);
}
);
export const selectIsStaging = (state: RootState) => {
const sessionId = selectCanvasSessionId(state);
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 selectDiscardedItems = createSelector(
selectCanvasSessionSlice,
({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems

View File

@@ -1,7 +1,11 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { useCreateStylePresetFromMetadata } from 'features/gallery/hooks/useCreateStylePresetFromMetadata';
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -15,11 +19,15 @@ import {
export const ImageMenuItemMetadataRecallActions = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const subMenu = useSubMenu();
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } =
useImageActions(imageDTO);
const imageDTO = useImageDTOContext();
const recallAll = useRecallAll(imageDTO);
const recallRemix = useRecallRemix(imageDTO);
const recallPrompts = useRecallPrompts(imageDTO);
const recallSeed = useRecallSeed(imageDTO);
const stylePreset = useCreateStylePresetFromMetadata(imageDTO);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
@@ -28,19 +36,23 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
<SubMenuButtonContent label={t('parameters.recallMetadata')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={remix} isDisabled={!hasMetadata}>
<MenuItem
icon={<PiArrowsCounterClockwiseBold />}
onClick={recallRemix.recall}
isDisabled={!recallRemix.isEnabled}
>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem icon={<PiQuotesBold />} onClick={recallPrompts} isDisabled={!hasPrompts}>
<MenuItem icon={<PiQuotesBold />} onClick={recallPrompts.recall} isDisabled={!recallPrompts.isEnabled}>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem icon={<PiPlantBold />} onClick={recallSeed} isDisabled={!hasSeed}>
<MenuItem icon={<PiPlantBold />} onClick={recallSeed.recall} isDisabled={!recallSeed.isEnabled}>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem icon={<PiAsteriskBold />} onClick={recallAll} isDisabled={!hasMetadata}>
<MenuItem icon={<PiAsteriskBold />} onClick={recallAll.recall} isDisabled={!recallAll.isEnabled}>
{t('parameters.useAll')}
</MenuItem>
<MenuItem icon={<PiPaintBrushBold />} onClick={createAsPreset} isDisabled={!hasPrompts}>
<MenuItem icon={<PiPaintBrushBold />} onClick={stylePreset.create} isDisabled={!stylePreset.isEnabled}>
{t('stylePresets.useForTemplate')}
</MenuItem>
</MenuList>

View File

@@ -1,5 +1,5 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useAppStore } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
@@ -9,7 +9,6 @@ import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
@@ -20,7 +19,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
const store = useAppStore();
const imageDTO = useImageDTOContext();
const isBusy = useCanvasIsBusySafe();
const activeTab = useAppSelector(selectActiveTab);
const onClickNewRasterLayerFromImage = useCallback(async () => {
const { dispatch, getState } = store;
@@ -82,10 +80,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
});
}, [imageDTO, store, t]);
if (activeTab === 'generate') {
return null;
}
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
<Menu {...subMenu.menuProps}>

View File

@@ -1,4 +1,5 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
@@ -16,6 +17,7 @@ import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContex
import { ImageMenuItemUseAsRefImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage';
import { ImageMenuItemUseForPromptGeneration } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseForPromptGeneration';
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -24,6 +26,8 @@ type SingleSelectionMenuItemsProps = {
};
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
const tab = useAppSelector(selectActiveTab);
return (
<ImageDTOContextProvider value={imageDTO}>
<IconMenuItemGroup>
@@ -36,13 +40,13 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
</IconMenuItemGroup>
<MenuDivider />
<ImageMenuItemLoadWorkflow />
<ImageMenuItemMetadataRecallActions />
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemMetadataRecallActions />}
<MenuDivider />
<ImageMenuItemSendToUpscale />
<ImageMenuItemUseForPromptGeneration />
<ImageMenuItemUseAsRefImage />
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemUseAsRefImage />}
<ImageMenuItemNewCanvasFromImageSubMenu />
<ImageMenuItemNewLayerFromImageSubMenu />
{tab === 'canvas' && <ImageMenuItemNewLayerFromImageSubMenu />}
<MenuDivider />
<ImageMenuItemChangeBoard />
<ImageMenuItemStarUnstar />

View File

@@ -1,21 +1,21 @@
import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useAppSelector } from 'app/store/storeHooks';
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { useDeleteImage } from 'features/gallery/hooks/useDeleteImage';
import { useEditImage } from 'features/gallery/hooks/useEditImage';
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { toast } from 'features/toast/toast';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
import { selectActiveTab, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
@@ -40,41 +40,19 @@ export const CurrentImageButtons = memo(() => {
const imageName = useAppSelector(selectLastSelectedImage);
const imageDTO = useImageDTO(imageName);
const hasTemplates = useStore($hasTemplates);
const imageActions = useImageActions(imageDTO);
const isStaging = useAppSelector(selectIsStaging);
const activeTab = useAppSelector(selectActiveTab);
const isUpscalingEnabled = useFeatureStatus('upscaling');
const { getState, dispatch } = useAppStore();
const canvasManager = useCanvasManagerSafe();
const isCanvasTabAndStaging = useMemo(() => activeTab === 'canvas' && isStaging, [activeTab, isStaging]);
const recallAll = useRecallAll(imageDTO);
const recallRemix = useRecallRemix(imageDTO);
const recallPrompts = useRecallPrompts(imageDTO);
const recallSeed = useRecallSeed(imageDTO);
const recallDimensions = useRecallDimensions(imageDTO);
const loadWorkflow = useLoadWorkflow(imageDTO);
const editImage = useEditImage(imageDTO);
const deleteImage = useDeleteImage(imageDTO);
const handleEdit = useCallback(async () => {
if (!imageDTO) {
return;
}
await newCanvasFromImage({
imageDTO,
type: 'raster_layer',
withInpaintMask: true,
getState,
dispatch,
});
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
// Automatically select the brush tool when editing an image
if (canvasManager) {
canvasManager.tool.$tool.set('brush');
}
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, getState, dispatch, t, canvasManager]);
console.log(isDisabledOverride, recallSeed.isEnabled);
return (
<>
@@ -95,8 +73,8 @@ export const CurrentImageButtons = memo(() => {
<Button
leftIcon={<PiPencilBold />}
onClick={handleEdit}
isDisabled={isDisabledOverride || !imageDTO}
onClick={editImage.edit}
isDisabled={isDisabledOverride || !editImage.isEnabled}
variant="link"
size="sm"
alignSelf="stretch"
@@ -111,37 +89,37 @@ export const CurrentImageButtons = memo(() => {
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasWorkflow || !hasTemplates}
isDisabled={isDisabledOverride || !loadWorkflow.isEnabled}
variant="link"
alignSelf="stretch"
onClick={imageActions.loadWorkflow}
onClick={loadWorkflow.load}
/>
<IconButton
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasMetadata}
isDisabled={isDisabledOverride || !recallRemix.isEnabled}
variant="link"
alignSelf="stretch"
onClick={imageActions.remix}
onClick={recallRemix.recall}
/>
<IconButton
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasPrompts}
isDisabled={isDisabledOverride || !recallPrompts.isEnabled}
variant="link"
alignSelf="stretch"
onClick={imageActions.recallPrompts}
onClick={recallPrompts.recall}
/>
<IconButton
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasSeed}
isDisabled={isDisabledOverride || !recallSeed.isEnabled}
variant="link"
alignSelf="stretch"
onClick={imageActions.recallSeed}
onClick={recallSeed.recall}
/>
<IconButton
icon={<PiRulerBold />}
@@ -149,24 +127,24 @@ export const CurrentImageButtons = memo(() => {
aria-label={`${t('parameters.useSize')} (D)`}
variant="link"
alignSelf="stretch"
onClick={imageActions.recallSize}
isDisabled={isDisabledOverride || !imageDTO || isCanvasTabAndStaging}
onClick={recallDimensions.recall}
isDisabled={isDisabledOverride || !recallDimensions.isEnabled}
/>
<IconButton
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasMetadata}
isDisabled={isDisabledOverride || !recallAll.isEnabled}
variant="link"
alignSelf="stretch"
onClick={imageActions.recallAll}
onClick={recallAll.recall}
/>
{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} isDisabled={isDisabledOverride} />}
<Divider orientation="vertical" h={8} mx={2} />
<DeleteImageButton onClick={imageActions.delete} isDisabled={isDisabledOverride || !imageDTO} />
<DeleteImageButton onClick={deleteImage.delete} isDisabled={isDisabledOverride || !deleteImage.isEnabled} />
</>
);
});

View File

@@ -0,0 +1,26 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import {
activeStylePresetIdChanged,
selectStylePresetActivePresetId,
} from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const useClearStylePresetWithToast = () => {
const store = useAppStore();
const { t } = useTranslation();
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const clearStylePreset = useCallback(() => {
if (activeStylePresetId) {
store.dispatch(activeStylePresetIdChanged(null));
toast({
status: 'info',
title: t('stylePresets.promptTemplateCleared'),
});
}
}, [activeStylePresetId, store, t]);
return clearStylePreset;
};

View File

@@ -0,0 +1,81 @@
import { useAppStore } from 'app/store/storeHooks';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
export const useCreateStylePresetFromMetadata = (imageDTO?: ImageDTO | null) => {
const store = useAppStore();
const [hasPrompts, setHasPrompts] = useState(false);
const { metadata } = useDebouncedMetadata(imageDTO?.image_name);
useEffect(() => {
MetadataUtils.hasMetadataByHandlers({
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
metadata,
store,
require: 'some',
})
.then((result) => {
setHasPrompts(result);
})
.catch(() => {
setHasPrompts(false);
});
}, [metadata, store]);
const isEnabled = useMemo(() => {
if (!imageDTO) {
return false;
}
if (!hasPrompts) {
return false;
}
return true;
}, [hasPrompts, imageDTO]);
const create = useCallback(async () => {
if (!imageDTO) {
return;
}
if (!metadata) {
return;
}
if (!isEnabled) {
return;
}
let positivePrompt: string;
let negativePrompt: string;
try {
positivePrompt = await MetadataHandlers.PositivePrompt.parse(metadata, store);
} catch (error) {
positivePrompt = '';
}
try {
negativePrompt = (await MetadataHandlers.NegativePrompt.parse(metadata, store)) ?? '';
} catch (error) {
negativePrompt = '';
}
$stylePresetModalState.set({
prefilledFormData: {
name: '',
positivePrompt,
negativePrompt,
imageUrl: imageDTO.image_url,
type: 'user',
},
updatingStylePresetId: null,
isModalOpen: true,
});
}, [imageDTO, isEnabled, metadata, store]);
return {
create,
isEnabled,
};
};

View File

@@ -0,0 +1,34 @@
import { useIsRegionFocused } from 'common/hooks/focus';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useCallback, useMemo } from 'react';
import type { ImageDTO } from 'services/api/types';
export const useDeleteImage = (imageDTO?: ImageDTO | null) => {
const deleteImageModal = useDeleteImageModalApi();
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const isEnabled = useMemo(() => {
if (!imageDTO) {
return;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [imageDTO, isGalleryFocused, isViewerFocused]);
const _delete = useCallback(() => {
if (!imageDTO) {
return;
}
if (!isEnabled) {
return;
}
deleteImageModal.delete([imageDTO.image_name]);
}, [deleteImageModal, imageDTO, isEnabled]);
return {
delete: _delete,
isEnabled,
};
};

View File

@@ -0,0 +1,63 @@
import { useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
export const useEditImage = (imageDTO?: ImageDTO | null) => {
const { t } = useTranslation();
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const { getState, dispatch } = useAppStore();
const canvasManager = useCanvasManagerSafe();
const isEnabled = useMemo(() => {
if (!imageDTO) {
return false;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [imageDTO, isGalleryFocused, isViewerFocused]);
const edit = useCallback(async () => {
if (!imageDTO) {
return;
}
if (!isEnabled) {
return;
}
await newCanvasFromImage({
imageDTO,
type: 'raster_layer',
withInpaintMask: true,
getState,
dispatch,
});
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
if (canvasManager) {
canvasManager.tool.$tool.set('brush');
}
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, isEnabled, getState, dispatch, canvasManager, t]);
return {
edit,
isEnabled,
};
};

View File

@@ -1,6 +1,7 @@
import { useStore } from '@nanostores/react';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
@@ -10,6 +11,7 @@ import {
activeStylePresetIdChanged,
selectStylePresetActivePresetId,
} from 'features/stylePresets/store/stylePresetSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { toast } from 'features/toast/toast';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
@@ -30,6 +32,9 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
const [hasPrompts, setHasPrompts] = useState(false);
const hasTemplates = useStore($hasTemplates);
const deleteImageModal = useDeleteImageModalApi();
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const isUpscalingEnabled = useFeatureStatus('upscaling');
const isCanvasTabAndStaging = useMemo(() => activeTab === 'canvas' && isStaging, [activeTab, isStaging]);
@@ -87,13 +92,12 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
if (!metadata) {
return;
}
MetadataUtils.recallAll(
metadata,
store,
isCanvasTabAndStaging ? [MetadataHandlers.Width, MetadataHandlers.Height] : []
);
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
const skip = activeTab === 'canvas' && isStaging ? [MetadataHandlers.Width, MetadataHandlers.Height] : undefined;
MetadataUtils.recallAll(metadata, store, skip);
clearStylePreset();
}, [imageDTO, metadata, store, isCanvasTabAndStaging, clearStylePreset]);
}, [imageDTO, metadata, activeTab, isStaging, store, clearStylePreset]);
const remix = useCallback(() => {
if (!imageDTO) {
@@ -162,56 +166,137 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
});
}, [imageDTO, metadata, store]);
const isEnabledLoadWorkflow = useMemo(() => {
if (!imageDTO) {
return false;
}
if (!imageDTO.has_workflow) {
return false;
}
if (!hasTemplates) {
return false;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [hasTemplates, imageDTO, isGalleryFocused, isViewerFocused]);
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const loadWorkflowFromImage = useCallback(() => {
const loadWorkflow = useCallback(() => {
if (!imageDTO) {
return;
}
if (!imageDTO.has_workflow || !hasTemplates) {
if (!isEnabledLoadWorkflow) {
return;
}
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
}, [hasTemplates, imageDTO, loadWorkflowWithDialog]);
}, [imageDTO, isEnabledLoadWorkflow, loadWorkflowWithDialog]);
const isEnabledRecallSize = useMemo(() => {
if (!imageDTO) {
return;
}
if (activeTab === 'canvas' && isStaging) {
return false;
}
if (activeTab !== 'canvas' && activeTab !== 'generate') {
return false;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [imageDTO, activeTab, isStaging, isGalleryFocused, isViewerFocused]);
const recallSize = useCallback(() => {
if (!imageDTO) {
return;
}
if (isCanvasTabAndStaging) {
if (!isEnabledRecallSize) {
return;
}
MetadataUtils.recallDimensions(imageDTO, store);
}, [imageDTO, isCanvasTabAndStaging, store]);
}, [imageDTO, isEnabledRecallSize, store]);
const isEnabledUpscale = useMemo(() => {
if (!imageDTO) {
return;
}
if (!isUpscalingEnabled) {
return false;
}
if (activeTab === 'canvas' && isStaging) {
return false;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [imageDTO, isUpscalingEnabled, activeTab, isStaging, isGalleryFocused, isViewerFocused]);
const upscale = useCallback(() => {
if (!imageDTO) {
return;
}
if (!isEnabledUpscale) {
return;
}
store.dispatch(adHocPostProcessingRequested({ imageDTO }));
}, [imageDTO, store]);
}, [imageDTO, isEnabledUpscale, store]);
const isEnabledDelete = useMemo(() => {
if (!imageDTO) {
return;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [imageDTO, isGalleryFocused, isViewerFocused]);
const _delete = useCallback(() => {
if (!imageDTO) {
return;
}
if (!isEnabledDelete) {
return;
}
deleteImageModal.delete([imageDTO.image_name]);
}, [deleteImageModal, imageDTO]);
}, [deleteImageModal, imageDTO, isEnabledDelete]);
return {
hasMetadata,
hasSeed,
hasPrompts,
recallAll,
remix,
recallSeed,
recallPrompts,
recallAll: {
run: recallAll,
isEnabled: hasMetadata,
},
remix: {
run: remix,
isEnabled: hasMetadata,
},
recallSeed: {
run: recallSeed,
isEnabled: hasSeed,
},
recallPrompts: {
run: recallPrompts,
isEnabled: hasPrompts,
},
createAsPreset,
loadWorkflow: loadWorkflowFromImage,
hasWorkflow: imageDTO?.has_workflow ?? false,
recallSize,
upscale,
delete: _delete,
loadWorkflow: {
run: loadWorkflow,
isEnabled: imageDTO?.has_workflow ?? false,
},
recallSize: {
run: recallSize,
isEnabled: isEnabledRecallSize,
},
upscale: {
run: upscale,
isEnabled: isEnabledUpscale,
},
delete: {
run: _delete,
isEnabled: isEnabledDelete,
},
};
};

View File

@@ -0,0 +1,43 @@
import { useStore } from '@nanostores/react';
import { useIsRegionFocused } from 'common/hooks/focus';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { useCallback, useMemo } from 'react';
import type { ImageDTO } from 'services/api/types';
export const useLoadWorkflow = (imageDTO?: ImageDTO | null) => {
const hasTemplates = useStore($hasTemplates);
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const isEnabled = useMemo(() => {
if (!imageDTO) {
return false;
}
if (!imageDTO.has_workflow) {
return false;
}
if (!hasTemplates) {
return false;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
return true;
}, [hasTemplates, imageDTO, isGalleryFocused, isViewerFocused]);
const load = useCallback(() => {
if (!imageDTO) {
return;
}
if (!isEnabled) {
return;
}
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
}, [imageDTO, isEnabled, loadWorkflowWithDialog]);
return { load, isEnabled };
};

View File

@@ -0,0 +1,60 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
export const useRecallAll = (imageDTO?: ImageDTO | null) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
const { metadata } = useDebouncedMetadata(imageDTO?.image_name);
const isStaging = useAppSelector(selectIsStaging);
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const clearStylePreset = useClearStylePresetWithToast();
const isEnabled = useMemo(() => {
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
if (tab !== 'canvas' && tab !== 'generate') {
return false;
}
if (!metadata) {
return false;
}
return true;
}, [isGalleryFocused, isViewerFocused, metadata, tab]);
const handlersToSkip = useMemo(() => {
if (tab === 'canvas' && isStaging) {
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
return [MetadataHandlers.Width, MetadataHandlers.Height];
}
return undefined;
}, [isStaging, tab]);
const recall = useCallback(() => {
if (!metadata) {
return;
}
if (!isEnabled) {
return;
}
MetadataUtils.recallAll(metadata, store, handlersToSkip);
clearStylePreset();
}, [metadata, isEnabled, store, handlersToSkip, clearStylePreset]);
return {
recall,
isEnabled,
};
};

View File

@@ -0,0 +1,50 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
import type { ImageDTO } from 'services/api/types';
export const useRecallDimensions = (imageDTO?: ImageDTO | null) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
const isStaging = useAppSelector(selectIsStaging);
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const isEnabled = useMemo(() => {
if (!imageDTO) {
return false;
}
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
if (tab !== 'canvas' && tab !== 'generate') {
return false;
}
if (tab === 'canvas' && isStaging) {
return false;
}
return true;
}, [imageDTO, isGalleryFocused, isStaging, isViewerFocused, tab]);
const recall = useCallback(() => {
if (!imageDTO) {
return;
}
if (!isEnabled) {
return;
}
MetadataUtils.recallDimensions(imageDTO, store);
}, [isEnabled, imageDTO, store]);
return {
recall,
isEnabled,
};
};

View File

@@ -0,0 +1,75 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
export const useRecallPrompts = (imageDTO?: ImageDTO | null) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const clearStylePreset = useClearStylePresetWithToast();
const [hasPrompts, setHasPrompts] = useState(false);
const { metadata } = useDebouncedMetadata(imageDTO?.image_name);
useEffect(() => {
const parse = async () => {
try {
const result = await MetadataUtils.hasMetadataByHandlers({
handlers: [
MetadataHandlers.PositivePrompt,
MetadataHandlers.NegativePrompt,
MetadataHandlers.PositiveStylePrompt,
MetadataHandlers.NegativeStylePrompt,
],
metadata,
store,
require: 'some',
});
setHasPrompts(result);
} catch {
setHasPrompts(false);
}
};
parse();
}, [metadata, store]);
const isEnabled = useMemo(() => {
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
if (tab !== 'canvas' && tab !== 'generate') {
return false;
}
if (!hasPrompts) {
return false;
}
return true;
}, [hasPrompts, isGalleryFocused, isViewerFocused, tab]);
const recall = useCallback(() => {
if (!metadata) {
return;
}
if (!isEnabled) {
return;
}
MetadataUtils.recallPrompts(metadata, store);
clearStylePreset();
}, [metadata, isEnabled, store, clearStylePreset]);
return {
recall,
isEnabled,
};
};

View File

@@ -0,0 +1,63 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
export const useRecallRemix = (imageDTO?: ImageDTO | null) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
const isStaging = useAppSelector(selectIsStaging);
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const clearStylePreset = useClearStylePresetWithToast();
const { metadata } = useDebouncedMetadata(imageDTO?.image_name);
const isEnabled = useMemo(() => {
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
if (tab !== 'canvas' && tab !== 'generate') {
return false;
}
if (!metadata) {
return false;
}
return true;
}, [isGalleryFocused, isViewerFocused, metadata, tab]);
const handlersToSkip = useMemo(() => {
// Remix always skips the seed handler
const _handlersToSkip = [MetadataHandlers.Seed];
if (tab === 'canvas' && isStaging) {
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
_handlersToSkip.push(MetadataHandlers.Width, MetadataHandlers.Height);
}
return _handlersToSkip;
}, [isStaging, tab]);
const recall = useCallback(() => {
if (!metadata) {
return;
}
if (!isEnabled) {
return;
}
MetadataUtils.recallAll(metadata, store, handlersToSkip);
clearStylePreset();
}, [metadata, isEnabled, store, handlersToSkip, clearStylePreset]);
return {
recall,
isEnabled,
};
};

View File

@@ -0,0 +1,65 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
export const useRecallSeed = (imageDTO?: ImageDTO | null) => {
const store = useAppStore();
const tab = useAppSelector(selectActiveTab);
const [hasSeed, setHasSeed] = useState(false);
const isGalleryFocused = useIsRegionFocused('gallery');
const isViewerFocused = useIsRegionFocused('viewer');
const { metadata } = useDebouncedMetadata(imageDTO?.image_name);
useEffect(() => {
const parse = async () => {
try {
await MetadataHandlers.Seed.parse(metadata, store);
setHasSeed(true);
} catch {
setHasSeed(false);
}
};
parse();
}, [metadata, store]);
const isEnabled = useMemo(() => {
if (!isGalleryFocused && !isViewerFocused) {
return false;
}
if (tab !== 'canvas' && tab !== 'generate') {
return false;
}
if (!metadata) {
return false;
}
if (!hasSeed) {
return false;
}
return true;
}, [hasSeed, isGalleryFocused, isViewerFocused, metadata, tab]);
const recall = useCallback(() => {
if (!metadata) {
return;
}
if (!isEnabled) {
return;
}
MetadataUtils.recallByHandler({ metadata, handler: MetadataHandlers.Seed, store });
}, [metadata, isEnabled, store]);
return {
recall,
isEnabled,
};
};

View File

@@ -86,7 +86,7 @@ import {
import { toast } from 'features/toast/toast';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { t } from 'i18next';
import type { ComponentType, ReactNode } from 'react';
import type { ComponentType } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { modelsApi } from 'services/api/endpoints/models';
@@ -875,7 +875,7 @@ export const MetadataHandlers = {
// ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer,
} as const;
const successToast = (parameter: ReactNode) => {
const successToast = (parameter: string) => {
toast({
id: 'PARAMETER_SET',
title: t('toast.parameterSet'),
@@ -884,7 +884,7 @@ const successToast = (parameter: ReactNode) => {
});
};
const failedToast = (parameter: ReactNode, message?: ReactNode) => {
const failedToast = (parameter: string, message?: string) => {
toast({
id: 'PARAMETER_NOT_SET',
title: t('toast.parameterNotSet'),
@@ -1019,6 +1019,28 @@ const recallPrompts = async (metadata: unknown, store: AppStore) => {
}
};
const hasMetadataByHandlers = async (arg: {
metadata: unknown;
handlers: (SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>)[];
store: AppStore;
require: 'some' | 'all';
}) => {
const { metadata, handlers, store, require } = arg;
for (const handler of handlers) {
try {
await handler.parse(metadata, store);
if (require === 'some') {
return true;
}
} catch {
if (require === 'all') {
return false;
}
}
}
return true;
};
const recallDimensions = async (metadata: unknown, store: AppStore) => {
const recalled = await recallByHandlers({
metadata,
@@ -1048,6 +1070,7 @@ const recallAll = async (
};
export const MetadataUtils = {
hasMetadataByHandlers,
recallByHandler,
recallByHandlers,
recallAll,

View File

@@ -144,7 +144,6 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('viewer', 'recallPrompts', ['p']);
addHotkey('viewer', 'remix', ['r']);
addHotkey('viewer', 'useSize', ['d']);
addHotkey('viewer', 'runPostprocessing', ['shift+u']);
addHotkey('viewer', 'toggleMetadata', ['i']);
// Gallery

View File

@@ -1,15 +1,14 @@
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
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 { memo } from 'react';
export const StagingArea = memo(() => {
const ctx = useCanvasSessionContext();
const hasItems = useStore(ctx.$hasItems);
const isStaging = useAppSelector(selectIsStaging);
if (!hasItems) {
if (!isStaging) {
return null;
}