feat(ui): rough out viewer components

The toolbar is largely copied from `CurrentImageButtons.tsx`
This commit is contained in:
psychedelicious
2024-02-02 16:51:28 +11:00
parent 521d91ea58
commit 07ce7685b1
7 changed files with 362 additions and 5 deletions

View File

@@ -1609,6 +1609,11 @@
"showProgressImages": "Show Progress Images",
"swapSizes": "Swap Sizes"
},
"viewer": {
"viewerModeImage": "Image",
"viewerModeInfo": "Info",
"viewerModeProgress": "Progress"
},
"unifiedCanvas": {
"accept": "Accept",
"activeLayer": "Active Layer",

View File

@@ -1,13 +1,11 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
import { Box } from '@invoke-ai/ui-library';
import { Viewer } from 'features/viewer/components/Viewer';
import { memo } from 'react';
const TextToImageTab = () => {
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<Flex w="full" h="full">
<CurrentImageDisplay />
</Flex>
<Viewer />
</Box>
);
};

View File

@@ -0,0 +1,30 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ViewerImage } from 'features/viewer/components/ViewerImage';
import { ViewerInfo } from 'features/viewer/components/ViewerInfo';
import { ViewerProgress } from 'features/viewer/components/ViewerProgress';
import { ViewerToolbar } from 'features/viewer/components/ViewerToolbar';
import { memo } from 'react';
export const Viewer = memo(() => {
const viewerMode = useAppSelector((s) => s.viewer.viewerMode);
return (
<Flex
position="relative"
flexDirection="column"
height="full"
width="full"
gap={4}
>
<ViewerToolbar />
<Flex height="full" width="full">
{viewerMode === 'image' && <ViewerImage />}
{viewerMode === 'info' && <ViewerInfo />}
{viewerMode === 'progress' && <ViewerProgress />}
</Flex>
</Flex>
);
});
Viewer.displayName = 'Viewer';

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
export const ViewerImage = memo(() => {
return null;
});
ViewerImage.displayName = 'ViewerImage';

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
export const ViewerInfo = memo(() => {
return null;
});
ViewerInfo.displayName = 'ViewerInfo';

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
export const ViewerProgress = memo(() => {
return null;
});
ViewerProgress.displayName = 'ViewerProgress';

View File

@@ -0,0 +1,303 @@
import { ButtonGroup, Flex, IconButton, Menu, MenuButton, MenuList } 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 { selectGallerySlice } from 'features/gallery/store/gallerySlice';
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 { selectSystemSlice } from 'features/system/store/systemSlice';
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,
PiHourglassHighBold,
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(
selectSystemSlice,
selectGallerySlice,
selectLastSelectedImage,
(system, gallery, lastSelectedImage) => {
const hasProgressImage = Boolean(system.denoiseProgress?.progress_image);
return hasProgressImage || !lastSelectedImage;
}
);
export const ViewerToolbar = memo(() => {
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
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>
</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>
</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={<PiHourglassHighBold />}
isChecked={viewerMode === 'progress'}
onClick={handleSelectViewerProgress}
/>
</ButtonGroup>
</Flex>
</Flex>
);
});
ViewerToolbar.displayName = 'ViewerToolbar';