From ff2b2fad83a548cf20aa84709e23fd40174333fb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 1 Jun 2024 11:43:49 +1000 Subject: [PATCH] feat(ui): revise drop zones The main viewer area has two drop zones: - Select for Viewer - Select for Compare These do what you'd imagine they would do. --- invokeai/frontend/web/public/locales/en.json | 1 + .../listeners/imageDropped.ts | 4 +- .../web/src/features/dnd/types/index.ts | 2 +- .../web/src/features/dnd/util/isValidDrop.ts | 6 +-- .../ImageViewer/CurrentImagePreview.tsx | 3 -- .../ImageViewer/ImageComparison.tsx | 53 ++----------------- .../ImageViewer/ImageComparisonDroppable.tsx | 21 ++++++-- .../ImageViewer/ImageComparisonSlider.tsx | 8 +-- .../components/ImageViewer/ImageViewer.tsx | 7 ++- .../ImageViewer/ImageViewerWorkflows.tsx | 45 ---------------- .../components/ImageViewer/ViewerToolbar.tsx | 15 +++++- .../features/ui/components/tabs/NodesTab.tsx | 6 ++- .../ui/components/tabs/TextToImageTab.tsx | 2 + 13 files changed, 58 insertions(+), 115 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 351e483fcc..9a10cde6ce 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -380,6 +380,7 @@ "problemDeletingImagesDesc": "One or more images could not be deleted", "viewerImage": "Viewer Image", "compareImage": "Compare Image", + "selectForViewer": "Select for Viewer", "selectForCompare": "Select for Compare", "selectAnImageToCompare": "Select an Image to Compare", "slider": "Slider", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 84823407e9..7cb0703af8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -15,7 +15,7 @@ import { } from 'features/controlLayers/store/controlLayersSlice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; -import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice'; +import { imageSelected, imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -54,6 +54,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { dispatch(imageSelected(activeData.payload.imageDTO)); + dispatch(isImageViewerOpenChanged(true)); return; } @@ -195,6 +196,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { imageDTO } = activeData.payload; dispatch(imageToCompareChanged(imageDTO)); + dispatch(isImageViewerOpenChanged(true)); return; } diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index f66fec0ea1..6fcf18421e 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -18,7 +18,7 @@ type BaseDropData = { id: string; }; -type CurrentImageDropData = BaseDropData & { +export type CurrentImageDropData = BaseDropData & { actionType: 'SET_CURRENT_IMAGE'; }; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index d8e9d98e10..6dec862345 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -30,11 +30,7 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? case 'SET_NODES_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': - return ( - payloadType === 'IMAGE_DTO' && - activeData.id !== 'image-compare-first-image' && - activeData.id !== 'image-compare-second-image' - ); + return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 5de4f28d2a..a812391992 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -6,7 +6,6 @@ import IAIDndImage from 'common/components/IAIDndImage'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import type { TypesafeDraggableData } from 'features/dnd/types'; import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer'; -import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import type { AnimationProps } from 'framer-motion'; @@ -75,12 +74,10 @@ const CurrentImagePreview = () => { isUploadDisabled={true} fitContainer useThumbailFallback - dropLabel={t('gallery.setCurrentImage')} noContentFallback={} dataTestId="image-preview" /> )} - {shouldShowImageDetails && imageDTO && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx index 74acdfa13f..a0dc48bd5d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx @@ -1,16 +1,12 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import IAIDroppable from 'common/components/IAIDroppable'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import type { SelectForCompareDropData } from 'features/dnd/types'; import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide'; import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; -import type { PropsWithChildren } from 'react'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiImagesBold } from 'react-icons/pi'; -import type { ImageDTO } from 'services/api/types'; const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => { const firstImage = gallerySlice.selection.slice(-1)[0] ?? null; @@ -24,56 +20,17 @@ export const ImageComparison = memo(() => { const { firstImage, secondImage } = useAppSelector(selector); if (!firstImage || !secondImage) { - return ( - - - - ); + // Should rarely/never happen - we don't render this component unless we have images to compare + return ; } if (comparisonMode === 'slider') { - return ( - - - - ); + return ; } if (comparisonMode === 'side-by-side') { - return ( - - - - ); + return ; } }); ImageComparison.displayName = 'ImageComparison'; - -type Props = PropsWithChildren<{ - firstImage: ImageDTO | null; - secondImage: ImageDTO | null; -}>; - -const ImageComparisonWrapper = memo((props: Props) => { - const droppableData = useMemo( - () => ({ - id: 'image-comparison', - actionType: 'SELECT_FOR_COMPARE', - context: { - firstImageName: props.firstImage?.image_name, - secondImageName: props.secondImage?.image_name, - }, - }), - [props.firstImage?.image_name, props.secondImage?.image_name] - ); - - return ( - <> - {props.children} - - - ); -}); - -ImageComparisonWrapper.displayName = 'ImageComparisonWrapper'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx index 6f163f63cf..9639daac10 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx @@ -1,7 +1,8 @@ +import { Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import IAIDroppable from 'common/components/IAIDroppable'; -import type { SelectForCompareDropData } from 'features/dnd/types'; +import type { CurrentImageDropData, SelectForCompareDropData } from 'features/dnd/types'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,10 +13,15 @@ const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => { return { firstImage, secondImage }; }); +const setCurrentImageDropData: CurrentImageDropData = { + id: 'current-image', + actionType: 'SET_CURRENT_IMAGE', +}; + export const ImageComparisonDroppable = memo(() => { const { t } = useTranslation(); const { firstImage, secondImage } = useAppSelector(selector); - const droppableData = useMemo( + const selectForCompareDropData = useMemo( () => ({ id: 'image-comparison', actionType: 'SELECT_FOR_COMPARE', @@ -27,7 +33,16 @@ export const ImageComparisonDroppable = memo(() => { [firstImage?.image_name, secondImage?.image_name] ); - return ; + return ( + + + + + + + + + ); }); ImageComparisonDroppable.displayName = 'ImageComparisonDroppable'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx index 39cb1d43ae..3ced364b64 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx @@ -132,7 +132,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) = justifyContent="center" > { const { isOpen, onToggle, onClose } = useImageViewer(); const activeTabName = useAppSelector(activeTabNameSelector); + const workflowsMode = useAppSelector((s) => s.workflow.mode); const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null); const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]); const shouldShowViewer = useMemo(() => { + if (activeTabName === 'workflows' && workflowsMode === 'view') { + return true; + } if (!isViewerEnabled) { return false; } return isOpen; - }, [isOpen, isViewerEnabled]); + }, [isOpen, isViewerEnabled, workflowsMode, activeTabName]); useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]); @@ -45,7 +49,6 @@ export const ImageViewer = memo(() => { rowGap={4} alignItems="center" justifyContent="center" - zIndex={10} // reactflow puts its minimap at 5, so we need to be above that > {isComparing && } {!isComparing && } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx deleted file mode 100644 index fe09f11be6..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; -import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; -import { memo } from 'react'; - -import CurrentImageButtons from './CurrentImageButtons'; -import CurrentImagePreview from './CurrentImagePreview'; - -export const ImageViewerWorkflows = memo(() => { - return ( - - - - - - - - - - - - - - - - - - ); -}); - -ImageViewerWorkflows.displayName = 'ImageViewerWorkflows'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx index 6310874030..21d3ba59d4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx @@ -1,12 +1,23 @@ 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 { memo } from 'react'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { memo, useMemo } from 'react'; import CurrentImageButtons from './CurrentImageButtons'; import { ViewerToggleMenu } from './ViewerToggleMenu'; export const ViewerToolbar = memo(() => { + const workflowsMode = useAppSelector((s) => s.workflow.mode); + const activeTabName = useAppSelector(activeTabNameSelector); + const shouldShowToggleMenu = useMemo(() => { + if (activeTabName !== 'workflows') { + return true; + } + return workflowsMode === 'edit'; + }, [workflowsMode, activeTabName]); + return ( @@ -20,7 +31,7 @@ export const ViewerToolbar = memo(() => { - + {shouldShowToggleMenu && } diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx index b4f473ae03..256a4331cd 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx @@ -1,6 +1,7 @@ import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { ImageViewerWorkflows } from 'features/gallery/components/ImageViewer/ImageViewerWorkflows'; +import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import NodeEditor from 'features/nodes/components/NodeEditor'; import { memo } from 'react'; import { ReactFlowProvider } from 'reactflow'; @@ -10,7 +11,8 @@ const NodesTab = () => { if (mode === 'view') { return ( - + + ); } diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx index 1c1c9c24a4..5583624823 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -1,5 +1,6 @@ import { Box } from '@invoke-ai/ui-library'; import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor'; +import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable'; import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import { memo } from 'react'; @@ -8,6 +9,7 @@ const TextToImageTab = () => { + ); };