feat(ui): reworked image context menu

- Add `Open in Viewer`
- Remove `Send to Image to Image`
- Fix `Send to Canvas`
- Split out logic for composability
This commit is contained in:
psychedelicious
2024-09-06 18:42:40 +10:00
parent ce9f17726f
commit c64693fffd
20 changed files with 474 additions and 243 deletions

View File

@@ -134,6 +134,7 @@
"nodes": "Workflows",
"notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
"openInViewer": "Open in Viewer",
"orderBy": "Order By",
"outpaint": "outpaint",
"outputs": "Outputs",
@@ -1049,8 +1050,7 @@
"seamlessYAxis": "Seamless Tiling Y Axis",
"seed": "Seed",
"imageActions": "Image Actions",
"sendToImg2Img": "Send to Image to Image",
"sendToUnifiedCanvas": "Send To Unified Canvas",
"sendToCanvas": "Send To Canvas",
"sendToUpscale": "Send To Upscale",
"showOptionsPanel": "Show Side Panel (O or T)",
"shuffle": "Shuffle Seed",
@@ -1191,8 +1191,8 @@
"problemSavingMaskDesc": "Unable to export mask",
"prunedQueue": "Pruned Queue",
"resetInitialImage": "Reset Initial Image",
"sentToImageToImage": "Sent To Image To Image",
"sentToUnifiedCanvas": "Sent to Unified Canvas",
"sentToCanvas": "Sent to Canvas",
"sentToUpscale": "Sent to Upscale",
"serverError": "Server Error",
"sessionRef": "Session: {{sessionId}}",
"setAsCanvasInitialImage": "Set as canvas initial image",

View File

@@ -0,0 +1,26 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFoldersBold } from 'react-icons/pi';
export const ImageMenuItemChangeBoard = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const onClick = useCallback(() => {
dispatch(imagesToChangeSelected([imageDTO]));
dispatch(isModalOpenChanged(true));
}, [dispatch, imageDTO]);
return (
<MenuItem icon={<PiFoldersBold />} onClickCapture={onClick}>
{t('boards.changeBoard')}
</MenuItem>
);
});
ImageMenuItemChangeBoard.displayName = 'ImageMenuItemChangeBoard';

View File

@@ -0,0 +1,28 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
export const ImageMenuItemCopy = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
const onClick = useCallback(() => {
copyImageToClipboard(imageDTO.image_url);
}, [copyImageToClipboard, imageDTO.image_url]);
if (!isClipboardAPIAvailable) {
return null;
}
return (
<MenuItem icon={<PiCopyBold />} onClickCapture={onClick}>
{t('parameters.copyImage')}
</MenuItem>
);
});
ImageMenuItemCopy.displayName = 'ImageMenuItemCopy';

View File

@@ -0,0 +1,25 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
export const ImageMenuItemDelete = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const onClick = useCallback(() => {
dispatch(imagesToDeleteSelected([imageDTO]));
}, [dispatch, imageDTO]);
return (
<MenuItem isDestructive icon={<PiTrashSimpleBold />} onClickCapture={onClick}>
{t('gallery.deleteImage', { count: 1 })}
</MenuItem>
);
});
ImageMenuItemDelete.displayName = 'ImageMenuItemDelete';

View File

@@ -0,0 +1,24 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold } from 'react-icons/pi';
export const ImageMenuItemDownload = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { downloadImage } = useDownloadImage();
const onClick = useCallback(() => {
downloadImage(imageDTO.image_url, imageDTO.image_name);
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
return (
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={onClick}>
{t('parameters.downloadImage')}
</MenuItem>
);
});
ImageMenuItemDownload.displayName = 'ImageMenuItemDownload';

View File

@@ -0,0 +1,32 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi';
export const ImageMenuItemLoadWorkflow = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
const hasTemplates = useStore($hasTemplates);
const onClick = useCallback(() => {
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
return (
<MenuItem
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
onClickCapture={onClick}
isDisabled={!imageDTO.has_workflow || !hasTemplates}
>
{t('nodes.loadWorkflow')}
</MenuItem>
);
});
ImageMenuItemLoadWorkflow.displayName = 'ImageMenuItemLoadWorkflow';

View File

@@ -0,0 +1,72 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiPaintBrushBold,
PiPlantBold,
PiQuotesBold,
} from 'react-icons/pi';
export const ImageMenuItemMetadataRecallActions = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
} = useImageActions(imageDTO?.image_name);
return (
<>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
onClickCapture={remix}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
onClickCapture={recallPrompts}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
onClickCapture={recallSeed}
isDisabled={isLoadingMetadata || !hasSeed}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
onClickCapture={recallAll}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={createAsPreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('stylePresets.useForTemplate')}
</MenuItem>
</>
);
});
ImageMenuItemMetadataRecallActions.displayName = 'ImageMenuItemMetadataRecallActions';

View File

@@ -0,0 +1,18 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowSquareOutBold } from 'react-icons/pi';
export const ImageMenuItemOpenInNewTab = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
return (
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiArrowSquareOutBold />}>
{t('common.openInNewTab')}
</MenuItem>
);
});
ImageMenuItemOpenInNewTab.displayName = 'ImageMenuItemOpenInNewTab';

View File

@@ -0,0 +1,29 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold } from 'react-icons/pi';
export const ImageMenuItemOpenInViewer = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
dispatch(imageToCompareChanged(null));
dispatch(imageSelected(imageDTO));
imageViewer.onOpen();
}, [dispatch, imageDTO, imageViewer]);
return (
<MenuItem icon={<PiEyeBold />} onClick={onClick}>
{t('gallery.openInViewer')}
</MenuItem>
);
});
ImageMenuItemOpenInViewer.displayName = 'ImageMenuItemOpenInViewer';

View File

@@ -0,0 +1,31 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImagesBold } from 'react-icons/pi';
export const ImageMenuItemSelectForCompare = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const selectMaySelectForCompare = useMemo(
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
[imageDTO.image_name]
);
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
const onClick = useCallback(() => {
dispatch(imageToCompareChanged(imageDTO));
}, [dispatch, imageDTO]);
return (
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={onClick}>
{t('gallery.selectForCompare')}
</MenuItem>
);
});
ImageMenuItemSelectForCompare.displayName = 'ImageMenuItemSelectForCompare';

View File

@@ -0,0 +1,50 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShareFatBold } from 'react-icons/pi';
const selectBboxRect = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect);
export const ImageMenuItemSendToCanvas = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const bboxRect = useAppSelector(selectBboxRect);
const imageViewer = useImageViewer();
const handleSendToCanvas = useCallback(() => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasRasterLayerState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(sentImageToCanvas());
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
dispatch(setActiveTab('generation'));
imageViewer.onClose();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, imageViewer, t]);
return (
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
{t('parameters.sendToCanvas')}
</MenuItem>
);
});
ImageMenuItemSendToCanvas.displayName = 'ImageMenuItemSendToCanvas';

View File

@@ -0,0 +1,33 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShareFatBold } from 'react-icons/pi';
export const ImageMenuItemSendToUpscale = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const handleSendToCanvas = useCallback(() => {
dispatch(upscaleInitialImageChanged(imageDTO));
dispatch(setActiveTab('upscaling'));
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToUpscale'),
status: 'success',
});
}, [dispatch, imageDTO, t]);
return (
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-upscale">
{t('parameters.sendToUpscale')}
</MenuItem>
);
});
ImageMenuItemSendToUpscale.displayName = 'ImageMenuItemSendToUpscale';

View File

@@ -0,0 +1,44 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStarBold, PiStarFill } from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
export const ImageMenuItemStarUnstar = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const customStarUi = useStore($customStarUI);
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
const starImage = useCallback(() => {
if (imageDTO) {
starImages({ imageDTOs: [imageDTO] });
}
}, [starImages, imageDTO]);
const unstarImage = useCallback(() => {
if (imageDTO) {
unstarImages({ imageDTOs: [imageDTO] });
}
}, [unstarImages, imageDTO]);
if (imageDTO.starred) {
return (
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={unstarImage}>
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
</MenuItem>
);
}
return (
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={starImage}>
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
</MenuItem>
);
});
ImageMenuItemStarUnstar.displayName = 'ImageMenuItemStarUnstar';

View File

@@ -1,234 +1,45 @@
import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { $templates } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { size } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiCopyBold,
PiDownloadSimpleBold,
PiFlowArrowBold,
PiFoldersBold,
PiImagesBold,
PiPaintBrushBold,
PiPlantBold,
PiQuotesBold,
PiShareFatBold,
PiStarBold,
PiStarFill,
PiTrashSimpleBold,
} from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
import { MenuDivider } from '@invoke-ai/ui-library';
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
import { ImageMenuItemMetadataRecallActions } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions';
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer';
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
import { ImageMenuItemSendToCanvas } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas';
import { ImageMenuItemSendToUpscale } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale';
import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar';
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
import { memo } from 'react';
import type { ImageDTO } from 'services/api/types';
type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO;
};
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { imageDTO } = props;
const selectMaySelectForCompare = useMemo(
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
[imageDTO.image_name]
);
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const customStarUi = useStore($customStarUI);
const { downloadImage } = useDownloadImage();
const templates = useStore($templates);
const {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
} = useImageActions(imageDTO?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
const handleLoadWorkflow = useCallback(() => {
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
const handleDelete = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(imagesToDeleteSelected([imageDTO]));
}, [dispatch, imageDTO]);
const handleSendToImageToImage = useCallback(() => {
// TODO(psyche): restore send to img2img functionality
dispatch(sentImageToImg2Img());
dispatch(setActiveTab('generation'));
}, [dispatch]);
const handleSendToCanvas = useCallback(() => {
// TODO(psyche): restore send to canvas functionality
dispatch(sentImageToCanvas());
dispatch(setActiveTab('generation'));
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToUnifiedCanvas'),
status: 'success',
});
}, [dispatch, t]);
const handleChangeBoard = useCallback(() => {
dispatch(imagesToChangeSelected([imageDTO]));
dispatch(isModalOpenChanged(true));
}, [dispatch, imageDTO]);
const handleCopyImage = useCallback(() => {
copyImageToClipboard(imageDTO.image_url);
}, [copyImageToClipboard, imageDTO.image_url]);
const handleStarImage = useCallback(() => {
if (imageDTO) {
starImages({ imageDTOs: [imageDTO] });
}
}, [starImages, imageDTO]);
const handleUnstarImage = useCallback(() => {
if (imageDTO) {
unstarImages({ imageDTOs: [imageDTO] });
}
}, [unstarImages, imageDTO]);
const handleDownloadImage = useCallback(() => {
downloadImage(imageDTO.image_url, imageDTO.image_name);
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
const handleSelectImageForCompare = useCallback(() => {
dispatch(imageToCompareChanged(imageDTO));
}, [dispatch, imageDTO]);
const handleSendToUpscale = useCallback(() => {
dispatch(upscaleInitialImageChanged(imageDTO));
dispatch(setActiveTab('upscaling'));
}, [dispatch, imageDTO]);
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
return (
<>
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
{t('common.openInNewTab')}
</MenuItem>
{isClipboardAPIAvailable && (
<MenuItem icon={<PiCopyBold />} onClickCapture={handleCopyImage}>
{t('parameters.copyImage')}
</MenuItem>
)}
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleDownloadImage}>
{t('parameters.downloadImage')}
</MenuItem>
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={handleSelectImageForCompare}>
{t('gallery.selectForCompare')}
</MenuItem>
<ImageDTOContextProvider value={imageDTO}>
<ImageMenuItemOpenInNewTab />
<ImageMenuItemCopy />
<ImageMenuItemDownload />
<ImageMenuItemOpenInViewer />
<ImageMenuItemSelectForCompare />
<MenuDivider />
<MenuItem
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
onClickCapture={handleLoadWorkflow}
isDisabled={!imageDTO.has_workflow || !size(templates)}
>
{t('nodes.loadWorkflow')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
onClickCapture={remix}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
onClickCapture={recallPrompts}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
onClickCapture={recallSeed}
isDisabled={isLoadingMetadata || !hasSeed}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
onClickCapture={recallAll}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={createAsPreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('stylePresets.useForTemplate')}
</MenuItem>
<ImageMenuItemLoadWorkflow />
<ImageMenuItemMetadataRecallActions />
<MenuDivider />
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToUpscale} id="send-to-upscale">
{t('parameters.sendToUpscale')}
</MenuItem>
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<ImageMenuItemSendToUpscale />
<ImageMenuItemSendToCanvas />
<MenuDivider />
<MenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
{t('boards.changeBoard')}
</MenuItem>
{imageDTO.starred ? (
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={handleUnstarImage}>
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
</MenuItem>
) : (
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleStarImage}>
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
</MenuItem>
)}
<ImageMenuItemChangeBoard />
<ImageMenuItemStarUnstar />
<MenuDivider />
<MenuItem color="error.300" icon={<PiTrashSimpleBold />} onClickCapture={handleDelete}>
{t('gallery.deleteImage', { count: 1 })}
</MenuItem>
</>
<ImageMenuItemDelete />
</ImageDTOContextProvider>
);
};
export default memo(SingleSelectionMenuItems);
const SpinnerIcon = () => (
<Flex w="14px" alignItems="center" justifyContent="center">
<Spinner size="xs" />
</Flex>
);

View File

@@ -0,0 +1,7 @@
import { Flex, Spinner } from '@invoke-ai/ui-library';
export const SpinnerIcon = () => (
<Flex w="14px" alignItems="center" justifyContent="center">
<Spinner size="xs" />
</Flex>
);

View File

@@ -9,14 +9,12 @@ import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteIm
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToImg2Img } from 'features/gallery/store/actions';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
import { $templates } from 'features/nodes/store/nodesSlice';
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { size } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
@@ -65,14 +63,6 @@ const CurrentImageButtons = () => {
const handleUseSize = useCallback(() => {
parseAndRecallImageDimensions(lastSelectedImage);
}, [lastSelectedImage]);
const handleSendToImageToImage = useCallback(() => {
if (!imageDTO) {
return;
}
// TODO(psyche): restore send to img2img functionality
dispatch(sentImageToImg2Img());
dispatch(setActiveTab('generation'));
}, [dispatch, imageDTO]);
const handleClickUpscale = useCallback(() => {
if (!imageDTO) {
return;
@@ -93,9 +83,8 @@ const CurrentImageButtons = () => {
useHotkeys('p', recallPrompts, { enabled: isImageViewerActive }, [recallPrompts, isImageViewerActive]);
useHotkeys('r', remix, { enabled: isImageViewerActive }, [remix, isImageViewerActive]);
useHotkeys('d', handleUseSize, { enabled: isImageViewerActive }, [handleUseSize, isImageViewerActive]);
useHotkeys('shift+i', handleSendToImageToImage, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]);
useHotkeys(
'Shift+U',
'shift+u',
handleClickUpscale,
{ enabled: Boolean(isUpscalingEnabled && isImageViewerActive && isConnected) },
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive]

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from 'react';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
const ImageDTOContext = createContext<ImageDTO | null>(null);
export const ImageDTOContextProvider = ImageDTOContext.Provider;
export const useImageDTOContext = () => {
const imageDTO = useContext(ImageDTOContext);
assert(imageDTO !== null, 'useImageDTOContext must be used within ImageDTOContextProvider');
return imageDTO;
};

View File

@@ -2,6 +2,4 @@ import { createAction } from '@reduxjs/toolkit';
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');
export const imageDownloaded = createAction('gallery/imageDownloaded');

View File

@@ -55,7 +55,7 @@ import {
} from 'features/nodes/types/field';
import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import { atom } from 'nanostores';
import { atom, computed } from 'nanostores';
import type { MouseEvent } from 'react';
import type { Edge, EdgeChange, NodeChange, Viewport, XYPosition } from 'reactflow';
import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
@@ -435,6 +435,7 @@ export const {
export const $cursorPos = atom<XYPosition | null>(null);
export const $templates = atom<Templates>({});
export const $hasTemplates = computed($templates, (templates) => Object.keys(templates).length > 0);
export const $copiedNodes = atom<AnyNode[]>([]);
export const $copiedEdges = atom<InvocationNodeEdge[]>([]);
export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]);

View File

@@ -16,10 +16,10 @@ type UseGetAndLoadEmbeddedWorkflowReturn = {
};
type UseGetAndLoadEmbeddedWorkflow = (
options: UseGetAndLoadEmbeddedWorkflowOptions
options?: UseGetAndLoadEmbeddedWorkflowOptions
) => UseGetAndLoadEmbeddedWorkflowReturn;
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ onSuccess, onError }) => {
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = (options) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [_getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult] = useLazyGetImageWorkflowQuery();
@@ -30,7 +30,7 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o
if (data) {
dispatch(workflowLoadRequested({ data, asCopy: true }));
// No toast - the listener for this action does that after the workflow is loaded
onSuccess && onSuccess();
options?.onSuccess && options?.onSuccess();
} else {
toast({
id: 'PROBLEM_RETRIEVING_WORKFLOW',
@@ -44,10 +44,10 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o
title: t('toast.problemRetrievingWorkflow'),
status: 'error',
});
onError && onError();
options?.onError && options?.onError();
}
},
[_getAndLoadEmbeddedWorkflow, dispatch, onSuccess, t, onError]
[_getAndLoadEmbeddedWorkflow, dispatch, options, t]
);
return { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult };