mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-14 22:38:02 -05:00
refactor(ui): metadata recall buttons & hotkeys (WIP)
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
115
invokeai/frontend/web/src/common/hooks/useAsyncState.ts
Normal file
115
invokeai/frontend/web/src/common/hooks/useAsyncState.ts
Normal 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 };
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user