mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): split up ViewerToolbar
And fix disabled state for buttons
This commit is contained in:
@@ -1,313 +1,20 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
spinWithPauseAnimation,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { selectProgressSlice } from 'features/progress/store/progressSlice';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { viewerModeChanged } from 'features/viewer/store/viewerSlice';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiDotsThreeOutlineFill,
|
||||
PiEyeBold,
|
||||
PiFlowArrowBold,
|
||||
PiHourglassMediumFill,
|
||||
PiInfoBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
const selectShouldDisableToolbarButtons = createSelector(
|
||||
selectProgressSlice,
|
||||
selectLastSelectedImage,
|
||||
(progress, lastSelectedImage) => {
|
||||
const hasProgressImage = Boolean(progress.linearDenoiseProgress?.progress_image);
|
||||
return hasProgressImage || !lastSelectedImage;
|
||||
}
|
||||
);
|
||||
|
||||
const loadingStyles: SystemStyleObject = {
|
||||
svg: { animation: spinWithPauseAnimation },
|
||||
};
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { ViewerToolbarImageButtons } from 'features/viewer/components/ViewerToolbarImageButtons';
|
||||
import { ViewerToolbarImageMenu } from 'features/viewer/components/ViewerToolbarImageMenu';
|
||||
import { ViewerToolbarModeButtons } from 'features/viewer/components/ViewerToolbarModeButtons';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||
const hasProgress = useAppSelector((s) => s.progress.latestDenoiseProgress !== null);
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
|
||||
const viewerMode = useAppSelector((s) => s.viewer.viewerMode);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { recallBothPrompts, recallSeed, recallWidthAndHeight, recallAllParameters } = useRecallParameters();
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(lastSelectedImage?.image_name);
|
||||
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!lastSelectedImage || !lastSelectedImage.has_workflow) {
|
||||
return;
|
||||
}
|
||||
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
|
||||
|
||||
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
|
||||
|
||||
const handleClickUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(metadata);
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('a', handleClickUseAllParameters, [metadata]);
|
||||
|
||||
const handleUseSeed = useCallback(() => {
|
||||
recallSeed(metadata?.seed);
|
||||
}, [metadata?.seed, recallSeed]);
|
||||
|
||||
useHotkeys('s', handleUseSeed, [metadata]);
|
||||
|
||||
const handleUsePrompt = useCallback(() => {
|
||||
recallBothPrompts(
|
||||
metadata?.positive_prompt,
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt
|
||||
);
|
||||
}, [
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt,
|
||||
recallBothPrompts,
|
||||
]);
|
||||
|
||||
useHotkeys('p', handleUsePrompt, [metadata]);
|
||||
|
||||
const handleRemixImage = useCallback(() => {
|
||||
// Recalls all metadata parameters except seed
|
||||
recallAllParameters({
|
||||
...metadata,
|
||||
seed: undefined,
|
||||
});
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('r', handleRemixImage, [metadata]);
|
||||
|
||||
const handleUseSize = useCallback(() => {
|
||||
recallWidthAndHeight(metadata?.width, metadata?.height);
|
||||
}, [metadata?.width, metadata?.height, recallWidthAndHeight]);
|
||||
|
||||
useHotkeys('d', handleUseSize, [metadata]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(initialImageSelected(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||
|
||||
const handleClickUpscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(upscaleRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys(
|
||||
'Shift+U',
|
||||
() => {
|
||||
handleClickUpscale();
|
||||
},
|
||||
{
|
||||
enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected),
|
||||
},
|
||||
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'i',
|
||||
() => {
|
||||
if (imageDTO) {
|
||||
handleSelectViewerInfo();
|
||||
} else {
|
||||
toaster({
|
||||
title: t('toast.metadataLoadFailed'),
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[imageDTO, toaster]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'delete',
|
||||
() => {
|
||||
handleDelete();
|
||||
},
|
||||
[dispatch, imageDTO]
|
||||
);
|
||||
|
||||
const handleSelectViewerImage = useCallback(() => {
|
||||
dispatch(viewerModeChanged('image'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSelectViewerInfo = useCallback(() => {
|
||||
dispatch(viewerModeChanged('info'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSelectViewerProgress = useCallback(() => {
|
||||
dispatch(viewerModeChanged('progress'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2} w="full">
|
||||
<Flex flexGrow={1} flexWrap="wrap" justifyContent="flex-start" alignItems="center" gap={2}>
|
||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label={t('parameters.imageActions')}
|
||||
tooltip={t('parameters.imageActions')}
|
||||
isDisabled={!imageDTO}
|
||||
icon={<PiDotsThreeOutlineFill />}
|
||||
/>
|
||||
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
|
||||
</Menu>
|
||||
</ButtonGroup>
|
||||
<ViewerToolbarImageMenu />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
|
||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<IconButton
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!imageDTO?.has_workflow}
|
||||
onClick={handleLoadWorkflow}
|
||||
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={!metadata?.positive_prompt}
|
||||
onClick={handleRemixImage}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={!metadata?.positive_prompt}
|
||||
onClick={handleUsePrompt}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={metadata?.seed === null || metadata?.seed === undefined}
|
||||
onClick={handleUseSeed}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
isDisabled={
|
||||
metadata?.height === null ||
|
||||
metadata?.height === undefined ||
|
||||
metadata?.width === null ||
|
||||
metadata?.width === undefined
|
||||
}
|
||||
onClick={handleUseSize}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={!metadata}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isUpscalingEnabled && (
|
||||
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
||||
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
<ButtonGroup>
|
||||
<DeleteImageButton onClick={handleDelete} />
|
||||
</ButtonGroup>
|
||||
<ViewerToolbarImageButtons />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexWrap="wrap" justifyContent="flex-end" alignItems="center" gap={2}>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiEyeBold />}
|
||||
tooltip={`${t('viewer.viewerModeImage')}`}
|
||||
aria-label={`${t('viewer.viewerModeImage')}`}
|
||||
isChecked={viewerMode === 'image'}
|
||||
onClick={handleSelectViewerImage}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiInfoBold />}
|
||||
tooltip={`${t('viewer.viewerModeInfo')} (I)`}
|
||||
aria-label={`${t('viewer.viewerModeInfo')} (I)`}
|
||||
isChecked={viewerMode === 'info'}
|
||||
onClick={handleSelectViewerInfo}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={`${t('viewer.viewerModeProgress')}`}
|
||||
tooltip={`${t('viewer.viewerModeProgress')}`}
|
||||
icon={<PiHourglassMediumFill />}
|
||||
isChecked={viewerMode === 'progress'}
|
||||
onClick={handleSelectViewerProgress}
|
||||
sx={hasProgress ? loadingStyles : undefined}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ViewerToolbarModeButtons />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectIsDisabledToolbarImageButtons } from 'features/viewer/store/viewerSelectors';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiFlowArrowBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
export const ViewerToolbarImageButtons = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const isDisabled = useAppSelector(selectIsDisabledToolbarImageButtons);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { recallBothPrompts, recallSeed, recallWidthAndHeight, recallAllParameters } = useRecallParameters();
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(lastSelectedImage?.image_name);
|
||||
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!lastSelectedImage || !lastSelectedImage.has_workflow) {
|
||||
return;
|
||||
}
|
||||
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
|
||||
|
||||
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
|
||||
|
||||
const handleClickUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(metadata);
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('a', handleClickUseAllParameters, [metadata]);
|
||||
|
||||
const handleUseSeed = useCallback(() => {
|
||||
recallSeed(metadata?.seed);
|
||||
}, [metadata?.seed, recallSeed]);
|
||||
|
||||
useHotkeys('s', handleUseSeed, [metadata]);
|
||||
|
||||
const handleUsePrompt = useCallback(() => {
|
||||
recallBothPrompts(
|
||||
metadata?.positive_prompt,
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt
|
||||
);
|
||||
}, [
|
||||
metadata?.negative_prompt,
|
||||
metadata?.positive_prompt,
|
||||
metadata?.positive_style_prompt,
|
||||
metadata?.negative_style_prompt,
|
||||
recallBothPrompts,
|
||||
]);
|
||||
|
||||
useHotkeys('p', handleUsePrompt, [metadata]);
|
||||
|
||||
const handleRemixImage = useCallback(() => {
|
||||
// Recalls all metadata parameters except seed
|
||||
recallAllParameters({
|
||||
...metadata,
|
||||
seed: undefined,
|
||||
});
|
||||
}, [metadata, recallAllParameters]);
|
||||
|
||||
useHotkeys('r', handleRemixImage, [metadata]);
|
||||
|
||||
const handleUseSize = useCallback(() => {
|
||||
recallWidthAndHeight(metadata?.width, metadata?.height);
|
||||
}, [metadata?.width, metadata?.height, recallWidthAndHeight]);
|
||||
|
||||
useHotkeys('d', handleUseSize, [metadata]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(initialImageSelected(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||
|
||||
const handleClickUpscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(upscaleRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useHotkeys(
|
||||
'Shift+U',
|
||||
() => {
|
||||
handleClickUpscale();
|
||||
},
|
||||
{
|
||||
enabled: () => Boolean(isUpscalingEnabled && !isDisabled && isConnected),
|
||||
},
|
||||
[isUpscalingEnabled, imageDTO, isDisabled, isConnected]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'delete',
|
||||
() => {
|
||||
handleDelete();
|
||||
},
|
||||
[dispatch, imageDTO]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={isDisabled || !imageDTO?.has_workflow}
|
||||
onClick={handleLoadWorkflow}
|
||||
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={isDisabled || !metadata?.positive_prompt}
|
||||
onClick={handleRemixImage}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={isDisabled || !metadata?.positive_prompt}
|
||||
onClick={handleUsePrompt}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={isDisabled || metadata?.seed === null || metadata?.seed === undefined}
|
||||
onClick={handleUseSeed}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
isDisabled={
|
||||
isDisabled ||
|
||||
metadata?.height === null ||
|
||||
metadata?.height === undefined ||
|
||||
metadata?.width === null ||
|
||||
metadata?.width === undefined
|
||||
}
|
||||
onClick={handleUseSize}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={isDisabled || !metadata}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isUpscalingEnabled && (
|
||||
<ButtonGroup isDisabled={isQueueMutationInProgress || isDisabled}>
|
||||
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
<DeleteImageButton onClick={handleDelete} isDisabled={isDisabled} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbarImageButtons.displayName = 'ViewerToolbarImageButtons';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectIsDisabledToolbarImageButtons } from 'features/viewer/store/viewerSelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDotsThreeOutlineFill } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
export const ViewerToolbarImageMenu = memo(() => {
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const isDisabled = useAppSelector(selectIsDisabledToolbarImageButtons);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
return (
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label={t('parameters.imageActions')}
|
||||
tooltip={t('parameters.imageActions')}
|
||||
isDisabled={!imageDTO || isDisabled}
|
||||
icon={<PiDotsThreeOutlineFill />}
|
||||
/>
|
||||
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbarImageMenu.displayName = 'ViewerToolbarImageMenu';
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, IconButton, spinAnimation } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsProcessing } from 'features/queue/hooks/useIsProcessing';
|
||||
import { viewerModeChanged } from 'features/viewer/store/viewerSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCircleNotchBold, PiEyeBold, PiHourglassMediumFill, PiInfoBold } from 'react-icons/pi';
|
||||
|
||||
const loadingStyles: SystemStyleObject = {
|
||||
svg: { animation: spinAnimation },
|
||||
};
|
||||
|
||||
export const ViewerToolbarModeButtons = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isProcessing = useIsProcessing();
|
||||
const viewerMode = useAppSelector((s) => s.viewer.viewerMode);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelectViewerImage = useCallback(() => {
|
||||
dispatch(viewerModeChanged('image'));
|
||||
}, [dispatch]);
|
||||
// TODO: hotkey
|
||||
|
||||
const handleSelectViewerInfo = useCallback(() => {
|
||||
dispatch(viewerModeChanged('info'));
|
||||
}, [dispatch]);
|
||||
useHotkeys('i', handleSelectViewerInfo, [handleSelectViewerInfo]);
|
||||
|
||||
const handleSelectViewerProgress = useCallback(() => {
|
||||
dispatch(viewerModeChanged('progress'));
|
||||
}, [dispatch]);
|
||||
// TODO: hotkey
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiEyeBold />}
|
||||
tooltip={`${t('viewer.viewerModeImage')}`}
|
||||
aria-label={`${t('viewer.viewerModeImage')}`}
|
||||
isChecked={viewerMode === 'image'}
|
||||
onClick={handleSelectViewerImage}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiInfoBold />}
|
||||
tooltip={`${t('viewer.viewerModeInfo')} (I)`}
|
||||
aria-label={`${t('viewer.viewerModeInfo')} (I)`}
|
||||
isChecked={viewerMode === 'info'}
|
||||
onClick={handleSelectViewerInfo}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={`${t('viewer.viewerModeProgress')}`}
|
||||
tooltip={`${t('viewer.viewerModeProgress')}`}
|
||||
icon={isProcessing ? <PiCircleNotchBold /> : <PiHourglassMediumFill />}
|
||||
isChecked={viewerMode === 'progress'}
|
||||
onClick={handleSelectViewerProgress}
|
||||
sx={isProcessing ? loadingStyles : undefined}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbarModeButtons.displayName = 'ViewerToolbarModeButtons';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectViewerSlice } from 'features/viewer/store/viewerSlice';
|
||||
|
||||
export const selectIsDisabledToolbarImageButtons = createSelector(
|
||||
selectViewerSlice,
|
||||
selectLastSelectedImage,
|
||||
(viewer, lastSelectedImage) => {
|
||||
return viewer.viewerMode === 'progress' || !lastSelectedImage;
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user