mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): consolidate imagecontextmenu and send to menu
Both support the same actions: - Open in new tab - Copy image (if supported by browser) - Use prompt - Use seed - Use all - Send to img2img - Send to canvas - Change board - Download image - Delete
This commit is contained in:
parent
380aa1d7b5
commit
c82ae74610
@ -1,7 +1,16 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react';
|
import {
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
FlexProps,
|
||||||
|
Link,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
@ -49,6 +58,8 @@ import {
|
|||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
|
import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems';
|
||||||
|
|
||||||
const currentImageButtonsSelector = createSelector(
|
const currentImageButtonsSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
@ -345,65 +356,18 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||||
<IAIPopover
|
<Menu>
|
||||||
triggerComponent={
|
<MenuButton
|
||||||
<IAIIconButton
|
as={IAIIconButton}
|
||||||
aria-label={`${t('parameters.sendTo')}...`}
|
aria-label={`${t('parameters.sendTo')}...`}
|
||||||
tooltip={`${t('parameters.sendTo')}...`}
|
tooltip={`${t('parameters.sendTo')}...`}
|
||||||
isDisabled={!imageDTO}
|
isDisabled={!imageDTO}
|
||||||
icon={<FaShareAlt />}
|
icon={<FaShareAlt />}
|
||||||
/>
|
/>
|
||||||
}
|
<MenuList motionProps={menuListMotionProps}>
|
||||||
>
|
{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}
|
||||||
<Flex
|
</MenuList>
|
||||||
sx={{
|
</Menu>
|
||||||
flexDirection: 'column',
|
|
||||||
rowGap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSendToImageToImage}
|
|
||||||
leftIcon={<FaShare />}
|
|
||||||
id="send-to-img2img"
|
|
||||||
>
|
|
||||||
{t('parameters.sendToImg2Img')}
|
|
||||||
</IAIButton>
|
|
||||||
{isCanvasEnabled && (
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSendToCanvas}
|
|
||||||
leftIcon={<FaShare />}
|
|
||||||
id="send-to-canvas"
|
|
||||||
>
|
|
||||||
{t('parameters.sendToUnifiedCanvas')}
|
|
||||||
</IAIButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isClipboardAPIAvailable && (
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCopyImage}
|
|
||||||
leftIcon={<FaCopy />}
|
|
||||||
>
|
|
||||||
{t('parameters.copyImage')}
|
|
||||||
</IAIButton>
|
|
||||||
)}
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCopyImageLink}
|
|
||||||
leftIcon={<FaCopy />}
|
|
||||||
>
|
|
||||||
{t('parameters.copyImageToLink')}
|
|
||||||
</IAIButton>
|
|
||||||
|
|
||||||
<Link download={true} href={imageDTO?.image_url} target="_blank">
|
|
||||||
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
|
||||||
{t('parameters.downloadImage')}
|
|
||||||
</IAIButton>
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
|
||||||
</IAIPopover>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||||
|
@ -6,40 +6,15 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|||||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
||||||
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
||||||
import { MotionProps } from 'framer-motion';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const motionProps: MotionProps = {
|
|
||||||
variants: {
|
|
||||||
enter: {
|
|
||||||
visibility: 'visible',
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
transition: {
|
|
||||||
duration: 0.07,
|
|
||||||
ease: [0.4, 0, 0.2, 1],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exit: {
|
|
||||||
transitionEnd: {
|
|
||||||
visibility: 'hidden',
|
|
||||||
},
|
|
||||||
opacity: 0,
|
|
||||||
scale: 0.8,
|
|
||||||
transition: {
|
|
||||||
duration: 0.07,
|
|
||||||
easings: 'easeOut',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -72,7 +47,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
|||||||
imageDTO ? (
|
imageDTO ? (
|
||||||
<MenuList
|
<MenuList
|
||||||
sx={{ visibility: 'visible !important' }}
|
sx={{ visibility: 'visible !important' }}
|
||||||
motionProps={motionProps}
|
motionProps={menuListMotionProps}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
{selectionCount === 1 ? (
|
{selectionCount === 1 ? (
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { Link, MenuItem } from '@chakra-ui/react';
|
||||||
import { MenuItem } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
@ -18,8 +17,17 @@ import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboa
|
|||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useCallback, useContext, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaCopy, FaFolder, FaShare, FaTrash } from 'react-icons/fa';
|
import {
|
||||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
FaAsterisk,
|
||||||
|
FaCopy,
|
||||||
|
FaDownload,
|
||||||
|
FaExternalLinkAlt,
|
||||||
|
FaFolder,
|
||||||
|
FaQuoteRight,
|
||||||
|
FaSeedling,
|
||||||
|
FaShare,
|
||||||
|
FaTrash,
|
||||||
|
} from 'react-icons/fa';
|
||||||
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
||||||
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
@ -140,16 +148,21 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
|
<Link href={imageDTO.image_url} target="_blank">
|
||||||
{t('common.openInNewTab')}
|
<MenuItem
|
||||||
</MenuItem>
|
icon={<FaExternalLinkAlt />}
|
||||||
|
onClickCapture={handleOpenInNewTab}
|
||||||
|
>
|
||||||
|
{t('common.openInNewTab')}
|
||||||
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
{isClipboardAPIAvailable && (
|
{isClipboardAPIAvailable && (
|
||||||
<MenuItem icon={<FaCopy />} onClickCapture={handleCopyImage}>
|
<MenuItem icon={<FaCopy />} onClickCapture={handleCopyImage}>
|
||||||
{t('parameters.copyImage')}
|
{t('parameters.copyImage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<FaQuoteRight />}
|
||||||
onClickCapture={handleRecallPrompt}
|
onClickCapture={handleRecallPrompt}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
metadata?.positive_prompt === undefined &&
|
metadata?.positive_prompt === undefined &&
|
||||||
@ -160,14 +173,14 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<FaSeedling />}
|
||||||
onClickCapture={handleRecallSeed}
|
onClickCapture={handleRecallSeed}
|
||||||
isDisabled={metadata?.seed === undefined}
|
isDisabled={metadata?.seed === undefined}
|
||||||
>
|
>
|
||||||
{t('parameters.useSeed')}
|
{t('parameters.useSeed')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<FaAsterisk />}
|
||||||
onClickCapture={handleUseAllParameters}
|
onClickCapture={handleUseAllParameters}
|
||||||
isDisabled={!metadata}
|
isDisabled={!metadata}
|
||||||
>
|
>
|
||||||
@ -206,6 +219,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
Remove from Board
|
Remove from Board
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<Link download={true} href={imageDTO.image_url} target="_blank">
|
||||||
|
<MenuItem icon={<FaDownload />} w="100%">
|
||||||
|
{t('parameters.downloadImage')}
|
||||||
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { menuAnatomy } from '@chakra-ui/anatomy';
|
import { menuAnatomy } from '@chakra-ui/anatomy';
|
||||||
import { createMultiStyleConfigHelpers } from '@chakra-ui/react';
|
import { createMultiStyleConfigHelpers } from '@chakra-ui/react';
|
||||||
import { mode } from '@chakra-ui/theme-tools';
|
import { mode } from '@chakra-ui/theme-tools';
|
||||||
|
import { MotionProps } from 'framer-motion';
|
||||||
|
|
||||||
const { definePartsStyle, defineMultiStyleConfig } =
|
const { definePartsStyle, defineMultiStyleConfig } =
|
||||||
createMultiStyleConfigHelpers(menuAnatomy.keys);
|
createMultiStyleConfigHelpers(menuAnatomy.keys);
|
||||||
@ -21,6 +22,7 @@ const invokeAI = definePartsStyle((props) => ({
|
|||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
|
color: mode('base.900', 'base.150')(props),
|
||||||
bg: mode('base.200', 'base.800')(props),
|
bg: mode('base.200', 'base.800')(props),
|
||||||
shadow: 'dark-lg',
|
shadow: 'dark-lg',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@ -35,6 +37,9 @@ const invokeAI = definePartsStyle((props) => ({
|
|||||||
_focus: {
|
_focus: {
|
||||||
bg: mode('base.400', 'base.600')(props),
|
bg: mode('base.400', 'base.600')(props),
|
||||||
},
|
},
|
||||||
|
svg: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -46,3 +51,28 @@ export const menuTheme = defineMultiStyleConfig({
|
|||||||
variant: 'invokeAI',
|
variant: 'invokeAI',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const menuListMotionProps: MotionProps = {
|
||||||
|
variants: {
|
||||||
|
enter: {
|
||||||
|
visibility: 'visible',
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.07,
|
||||||
|
ease: [0.4, 0, 0.2, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
transitionEnd: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.8,
|
||||||
|
transition: {
|
||||||
|
duration: 0.07,
|
||||||
|
easings: 'easeOut',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user