mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): rework image viewer
- Rework styling - Replace "CurrentImageDisplay" entirely - Add a super short fade to reduce jarring transition - Make the viewer a singleton component, overlaid on everything else - reduces change when switching tabs
This commit is contained in:
parent
c05e52ebae
commit
33617fc06a
@ -1,24 +0,0 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
|
||||||
|
|
||||||
const CurrentImageDisplay = () => {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
position="relative"
|
|
||||||
flexDirection="column"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
rowGap={4}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<CurrentImageButtons />
|
|
||||||
<CurrentImagePreview />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(CurrentImageDisplay);
|
|
@ -1,79 +0,0 @@
|
|||||||
import { Button, Flex } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import CurrentImageButtons from 'features/gallery/components/CurrentImage/CurrentImageButtons';
|
|
||||||
import CurrentImagePreview from 'features/gallery/components/CurrentImage/CurrentImagePreview';
|
|
||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiArrowLeftBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
|
|
||||||
generation: 'ui.tabs.generation',
|
|
||||||
canvas: 'ui.tabs.canvas',
|
|
||||||
workflows: 'ui.tabs.workflows',
|
|
||||||
models: 'ui.tabs.models',
|
|
||||||
queue: 'ui.tabs.queue',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ImageViewer = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
const activeTabLabel = useMemo(
|
|
||||||
() => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }),
|
|
||||||
[t, activeTabName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
|
||||||
dispatch(isImageViewerOpenChanged(false));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const onOpen = useCallback(() => {
|
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useHotkeys('esc', onClose, { enabled: isOpen }, [isOpen]);
|
|
||||||
useHotkeys('i', onOpen, { enabled: !isOpen }, [isOpen]);
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
layerStyle="first"
|
|
||||||
borderRadius="base"
|
|
||||||
position="absolute"
|
|
||||||
flexDirection="column"
|
|
||||||
top={0}
|
|
||||||
right={0}
|
|
||||||
bottom={0}
|
|
||||||
left={0}
|
|
||||||
p={2}
|
|
||||||
rowGap={4}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
|
||||||
>
|
|
||||||
<CurrentImageButtons />
|
|
||||||
<CurrentImagePreview />
|
|
||||||
<Button
|
|
||||||
aria-label={activeTabLabel}
|
|
||||||
tooltip={activeTabLabel}
|
|
||||||
onClick={onClose}
|
|
||||||
leftIcon={<PiArrowLeftBold />}
|
|
||||||
position="absolute"
|
|
||||||
top={2}
|
|
||||||
insetInlineEnd={2}
|
|
||||||
>
|
|
||||||
{t('common.back')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ImageViewer.displayName = 'ImageViewer';
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Button } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowLeftBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
import { TAB_NAME_TO_TKEY, useImageViewer } from './useImageViewer';
|
||||||
|
|
||||||
|
export const BackToEditorButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { onClose } = useImageViewer();
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const tooltip = useMemo(
|
||||||
|
() => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }),
|
||||||
|
[t, activeTabName]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button aria-label={tooltip} tooltip={tooltip} onClick={onClose} leftIcon={<PiArrowLeftBold />} variant="ghost">
|
||||||
|
{t('common.back')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,6 @@
|
|||||||
import { ButtonGroup, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
|
||||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
|
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
@ -17,7 +16,6 @@ import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUps
|
|||||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
import { setShouldShowImageDetails, setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
|
|
||||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -27,8 +25,6 @@ import {
|
|||||||
PiAsteriskBold,
|
PiAsteriskBold,
|
||||||
PiDotsThreeOutlineFill,
|
PiDotsThreeOutlineFill,
|
||||||
PiFlowArrowBold,
|
PiFlowArrowBold,
|
||||||
PiHourglassHighBold,
|
|
||||||
PiInfoBold,
|
|
||||||
PiPlantBold,
|
PiPlantBold,
|
||||||
PiQuotesBold,
|
PiQuotesBold,
|
||||||
PiRulerBold,
|
PiRulerBold,
|
||||||
@ -48,15 +44,12 @@ const selectShouldDisableToolbarButtons = createSelector(
|
|||||||
const CurrentImageButtons = () => {
|
const CurrentImageButtons = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
|
||||||
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
|
|
||||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||||
const selection = useAppSelector((s) => s.gallery.selection);
|
const selection = useAppSelector((s) => s.gallery.selection);
|
||||||
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
|
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
|
||||||
|
|
||||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||||
const toaster = useAppToaster();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||||
@ -120,28 +113,6 @@ const CurrentImageButtons = () => {
|
|||||||
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
|
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickShowImageDetails = useCallback(
|
|
||||||
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
|
|
||||||
[dispatch, shouldShowImageDetails]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'i',
|
|
||||||
() => {
|
|
||||||
if (imageDTO) {
|
|
||||||
handleClickShowImageDetails();
|
|
||||||
} else {
|
|
||||||
toaster({
|
|
||||||
title: t('toast.metadataLoadFailed'),
|
|
||||||
status: 'error',
|
|
||||||
duration: 2500,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[imageDTO, shouldShowImageDetails, toaster]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'delete',
|
'delete',
|
||||||
() => {
|
() => {
|
||||||
@ -150,106 +121,80 @@ const CurrentImageButtons = () => {
|
|||||||
[dispatch, imageDTO]
|
[dispatch, imageDTO]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickProgressImagesToggle = useCallback(() => {
|
|
||||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
|
||||||
}, [dispatch, shouldShowProgressInViewer]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
|
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
<Menu isLazy>
|
||||||
<Menu isLazy>
|
<MenuButton
|
||||||
<MenuButton
|
as={IconButton}
|
||||||
as={IconButton}
|
aria-label={t('parameters.imageActions')}
|
||||||
aria-label={t('parameters.imageActions')}
|
tooltip={t('parameters.imageActions')}
|
||||||
tooltip={t('parameters.imageActions')}
|
isDisabled={!imageDTO}
|
||||||
isDisabled={!imageDTO}
|
icon={<PiDotsThreeOutlineFill />}
|
||||||
icon={<PiDotsThreeOutlineFill />}
|
/>
|
||||||
/>
|
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
|
||||||
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
|
</Menu>
|
||||||
</Menu>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<PiFlowArrowBold />}
|
icon={<PiFlowArrowBold />}
|
||||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||||
isDisabled={!imageDTO?.has_workflow}
|
isDisabled={!imageDTO?.has_workflow}
|
||||||
onClick={handleLoadWorkflow}
|
onClick={handleLoadWorkflow}
|
||||||
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
isLoading={isLoadingMetadata}
|
isLoading={isLoadingMetadata}
|
||||||
icon={<PiArrowsCounterClockwiseBold />}
|
icon={<PiArrowsCounterClockwiseBold />}
|
||||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||||
isDisabled={!hasMetadata}
|
isDisabled={!hasMetadata}
|
||||||
onClick={remix}
|
onClick={remix}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
isLoading={isLoadingMetadata}
|
isLoading={isLoadingMetadata}
|
||||||
icon={<PiQuotesBold />}
|
icon={<PiQuotesBold />}
|
||||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||||
isDisabled={!hasPrompts}
|
isDisabled={!hasPrompts}
|
||||||
onClick={recallPrompts}
|
onClick={recallPrompts}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
isLoading={isLoadingMetadata}
|
isLoading={isLoadingMetadata}
|
||||||
icon={<PiPlantBold />}
|
icon={<PiPlantBold />}
|
||||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||||
isDisabled={!hasSeed}
|
isDisabled={!hasSeed}
|
||||||
onClick={recallSeed}
|
onClick={recallSeed}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
isLoading={isLoadingMetadata}
|
isLoading={isLoadingMetadata}
|
||||||
icon={<PiRulerBold />}
|
icon={<PiRulerBold />}
|
||||||
tooltip={`${t('parameters.useSize')} (D)`}
|
tooltip={`${t('parameters.useSize')} (D)`}
|
||||||
aria-label={`${t('parameters.useSize')} (D)`}
|
aria-label={`${t('parameters.useSize')} (D)`}
|
||||||
onClick={handleUseSize}
|
onClick={handleUseSize}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
isLoading={isLoadingMetadata}
|
isLoading={isLoadingMetadata}
|
||||||
icon={<PiAsteriskBold />}
|
icon={<PiAsteriskBold />}
|
||||||
tooltip={`${t('parameters.useAll')} (A)`}
|
tooltip={`${t('parameters.useAll')} (A)`}
|
||||||
aria-label={`${t('parameters.useAll')} (A)`}
|
aria-label={`${t('parameters.useAll')} (A)`}
|
||||||
isDisabled={!hasMetadata}
|
isDisabled={!hasMetadata}
|
||||||
onClick={recallAll}
|
onClick={recallAll}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
{isUpscalingEnabled && (
|
{isUpscalingEnabled && (
|
||||||
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
||||||
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
||||||
</ButtonGroup>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ButtonGroup>
|
|
||||||
<IconButton
|
|
||||||
icon={<PiInfoBold />}
|
|
||||||
tooltip={`${t('parameters.info')} (I)`}
|
|
||||||
aria-label={`${t('parameters.info')} (I)`}
|
|
||||||
isChecked={shouldShowImageDetails}
|
|
||||||
onClick={handleClickShowImageDetails}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<DeleteImageButton onClick={handleDelete} />
|
||||||
aria-label={t('settings.displayInProgress')}
|
</ButtonGroup>
|
||||||
tooltip={t('settings.displayInProgress')}
|
|
||||||
icon={<PiHourglassHighBold />}
|
|
||||||
isChecked={shouldShowProgressInViewer}
|
|
||||||
onClick={handleClickProgressImagesToggle}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup>
|
|
||||||
<DeleteImageButton onClick={handleDelete} />
|
|
||||||
</ButtonGroup>
|
|
||||||
</Flex>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -5,7 +5,6 @@ import { useAppSelector } from 'app/store/storeHooks';
|
|||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||||
import ProgressImage from 'features/gallery/components/CurrentImage/ProgressImage';
|
|
||||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||||
@ -17,6 +16,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { PiImageBold } from 'react-icons/pi';
|
import { PiImageBold } from 'react-icons/pi';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
import ProgressImage from './ProgressImage';
|
||||||
|
|
||||||
const selectLastSelectedImageName = createSelector(
|
const selectLastSelectedImageName = createSelector(
|
||||||
selectLastSelectedImage,
|
selectLastSelectedImage,
|
||||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
(lastSelectedImage) => lastSelectedImage?.image_name
|
@ -0,0 +1,90 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||||
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import type { AnimationProps } from 'framer-motion';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
import { BackToEditorButton } from './BackToEditorButton';
|
||||||
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
|
|
||||||
|
const initial: AnimationProps['initial'] = {
|
||||||
|
opacity: 0,
|
||||||
|
};
|
||||||
|
const animate: AnimationProps['animate'] = {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.07 },
|
||||||
|
};
|
||||||
|
const exit: AnimationProps['exit'] = {
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.07 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
||||||
|
|
||||||
|
export const ImageViewer = memo(() => {
|
||||||
|
const { isOpen, onToggle, onClose } = useImageViewer();
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
|
||||||
|
const shouldShowViewer = useMemo(() => {
|
||||||
|
if (!isViewerEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isOpen;
|
||||||
|
}, [isOpen, isViewerEnabled]);
|
||||||
|
|
||||||
|
useHotkeys('shift+s', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
||||||
|
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{shouldShowViewer && (
|
||||||
|
<Flex
|
||||||
|
as={motion.div}
|
||||||
|
initial={initial}
|
||||||
|
animate={animate}
|
||||||
|
exit={exit}
|
||||||
|
layerStyle="first"
|
||||||
|
borderRadius="base"
|
||||||
|
position="absolute"
|
||||||
|
flexDirection="column"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
p={2}
|
||||||
|
rowGap={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||||
|
>
|
||||||
|
<Flex w="full" gap={2}>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
<ToggleMetadataViewerButton />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
|
<CurrentImageButtons />
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
|
<BackToEditorButton />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<CurrentImagePreview />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageViewer.displayName = 'ImageViewer';
|
@ -0,0 +1,42 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiInfoBold } from 'react-icons/pi';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
export const ToggleMetadataViewerButton = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||||
|
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||||
|
const toaster = useAppToaster();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||||
|
|
||||||
|
const toggleMetadataViewer = useCallback(
|
||||||
|
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
|
||||||
|
[dispatch, shouldShowImageDetails]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails, toaster]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={<PiInfoBold />}
|
||||||
|
tooltip={`${t('parameters.info')} (I)`}
|
||||||
|
aria-label={`${t('parameters.info')} (I)`}
|
||||||
|
onClick={toggleMetadataViewer}
|
||||||
|
isDisabled={!imageDTO}
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleMetadataViewerButton.displayName = 'ToggleMetadataViewerButton';
|
@ -0,0 +1,29 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiHourglassHighBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const ToggleProgressButton = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||||
|
}, [dispatch, shouldShowProgressInViewer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('settings.displayInProgress')}
|
||||||
|
tooltip={t('settings.displayInProgress')}
|
||||||
|
icon={<PiHourglassHighBold />}
|
||||||
|
onClick={onClick}
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleProgressButton.displayName = 'ToggleProgressButton';
|
@ -0,0 +1,31 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
|
||||||
|
generation: 'ui.tabs.generation',
|
||||||
|
canvas: 'ui.tabs.canvas',
|
||||||
|
workflows: 'ui.tabs.workflows',
|
||||||
|
models: 'ui.tabs.models',
|
||||||
|
queue: 'ui.tabs.queue',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useImageViewer = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onToggle = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(!isOpen));
|
||||||
|
}, [dispatch, isOpen]);
|
||||||
|
|
||||||
|
return { isOpen, onOpen, onClose, onToggle };
|
||||||
|
};
|
@ -4,6 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||||
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
||||||
@ -253,10 +254,11 @@ const InvokeTabs = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Panel id="main-panel" order={1} minSize={20}>
|
<Panel style={{ position: 'relative' }} id="main-panel" order={1} minSize={20}>
|
||||||
<TabPanels w="full" h="full">
|
<TabPanels w="full" h="full">
|
||||||
{tabPanels}
|
{tabPanels}
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
|
<ImageViewer />
|
||||||
</Panel>
|
</Panel>
|
||||||
{shouldShowGalleryPanel && (
|
{shouldShowGalleryPanel && (
|
||||||
<>
|
<>
|
||||||
|
@ -1,30 +1,14 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer';
|
|
||||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
const NodesTab = () => {
|
const NodesTab = () => {
|
||||||
const mode = useAppSelector((s) => s.workflow.mode);
|
|
||||||
|
|
||||||
if (mode === 'edit') {
|
|
||||||
return (
|
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
|
||||||
<ReactFlowProvider>
|
|
||||||
<NodeEditor />
|
|
||||||
</ReactFlowProvider>
|
|
||||||
<ImageViewer />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<Flex w="full" h="full">
|
<ReactFlowProvider>
|
||||||
<CurrentImageDisplay />
|
<NodeEditor />
|
||||||
</Flex>
|
</ReactFlowProvider>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<ControlLayersEditor />
|
<ControlLayersEditor />
|
||||||
<ImageViewer />
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,6 @@ import { CANVAS_TAB_TESTID } from 'features/canvas/store/constants';
|
|||||||
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
|
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
|
||||||
import type { CanvasInitialImageDropData } from 'features/dnd/types';
|
import type { CanvasInitialImageDropData } from 'features/dnd/types';
|
||||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -42,7 +41,6 @@ const UnifiedCanvasTab = () => {
|
|||||||
>
|
>
|
||||||
<IAICanvasToolbar />
|
<IAICanvasToolbar />
|
||||||
<IAICanvas />
|
<IAICanvas />
|
||||||
<ImageViewer />
|
|
||||||
{isValidDrop(droppableData, active) && (
|
{isValidDrop(droppableData, active) && (
|
||||||
<IAIDropOverlay isOver={isOver} label={t('toast.setCanvasInitialImage')} />
|
<IAIDropOverlay isOver={isOver} label={t('toast.setCanvasInitialImage')} />
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user