mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-01 23:35:10 -05:00
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:
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user