mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 15:04:59 -05:00
refactor(ui): rip out image viewer as modal
This commit is contained in:
@@ -11,11 +11,9 @@ import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
|
||||
import { toggleImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import i18n from 'i18n';
|
||||
@@ -72,12 +70,6 @@ export const GlobalHookIsolator = memo(
|
||||
useWorkflowBuilderWatcher();
|
||||
useDynamicPromptsWatcher();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'toggleViewer',
|
||||
category: 'viewer',
|
||||
callback: toggleImageViewer,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { ImageViewerModal } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
||||
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
||||
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
@@ -61,7 +60,6 @@ export const GlobalModalIsolator = memo(() => {
|
||||
<CanvasPasteModal />
|
||||
</CanvasManagerProviderGate>
|
||||
<LoadWorkflowFromGraphModal />
|
||||
<ImageViewerModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
@@ -94,7 +93,6 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
store.dispatch(canvasReset());
|
||||
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
store.dispatch(sentImageToCanvas());
|
||||
$imageViewer.set(false);
|
||||
toast({
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'info',
|
||||
@@ -164,12 +162,10 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
|
||||
store.dispatch(paramsReset());
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
$imageViewer.set(true);
|
||||
break;
|
||||
case 'canvas':
|
||||
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
|
||||
store.dispatch(canvasReset());
|
||||
$imageViewer.set(false);
|
||||
break;
|
||||
case 'workflows':
|
||||
// Go to the workflows tab
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreview
|
||||
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@@ -48,9 +47,6 @@ export const DndImage = memo(
|
||||
getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
if ($imageViewer.get()) {
|
||||
$imageViewer.set(false);
|
||||
}
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/exter
|
||||
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $focusedRegion } from 'common/hooks/focus';
|
||||
@@ -12,7 +11,6 @@ import { useClientSideUpload } from 'common/hooks/useClientSideUpload';
|
||||
import { setFileToPaste } from 'features/controlLayers/components/CanvasPasteModal';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -70,7 +68,6 @@ export const FullscreenDropzone = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndState, setDndState] = useState<DndTargetState>('idle');
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
const isImageViewerOpen = useStore($imageViewer);
|
||||
const isClientSideUploadEnabled = useAppSelector(selectIsClientSideUploadEnabled);
|
||||
const clientSideUpload = useClientSideUpload();
|
||||
|
||||
@@ -96,13 +93,7 @@ export const FullscreenDropzone = memo(() => {
|
||||
// While on the canvas tab and when pasting a single image, canvas may want to create a new layer. Let it handle
|
||||
// the paste event.
|
||||
const [firstImageFile] = files;
|
||||
if (
|
||||
focusedRegion === 'canvas' &&
|
||||
!isImageViewerOpen &&
|
||||
activeTab === 'canvas' &&
|
||||
files.length === 1 &&
|
||||
firstImageFile
|
||||
) {
|
||||
if (focusedRegion === 'canvas' && activeTab === 'canvas' && files.length === 1 && firstImageFile) {
|
||||
setFileToPaste(firstImageFile);
|
||||
return;
|
||||
}
|
||||
@@ -125,7 +116,7 @@ export const FullscreenDropzone = memo(() => {
|
||||
uploadImages(uploadArgs);
|
||||
}
|
||||
},
|
||||
[activeTab, isImageViewerOpen, t, isClientSideUploadEnabled, clientSideUpload]
|
||||
[activeTab, t, isClientSideUploadEnabled, clientSideUpload]
|
||||
);
|
||||
|
||||
const onPaste = useCallback(
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -16,56 +15,51 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewCanvasWithControlLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
|
||||
@@ -18,7 +17,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
|
||||
const onClickNewRasterLayerFromImage = useCallback(() => {
|
||||
@@ -26,65 +24,60 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewControlLayerFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'control_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewInpaintMaskFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'inpaint_mask', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewRegionalGuidanceFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewRegionalReferenceImageFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance_with_reference_image', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,10 +7,10 @@ import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
const onClick = useCallback(() => {
|
||||
imageViewer.openImageInViewer(imageDTO);
|
||||
}, [imageDTO, imageViewer]);
|
||||
// TODO
|
||||
imageDTO.image_name;
|
||||
}, [imageDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -14,20 +13,18 @@ export const ImageMenuItemUseAsRefImage = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiImageBold />} onClickCapture={onClickNewGlobalReferenceImageFromImage}>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
@@ -203,9 +202,6 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
);
|
||||
|
||||
const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
|
||||
// Use the atom here directly instead of the `useImageViewer` to avoid re-rendering the gallery when the viewer
|
||||
// opened state changes.
|
||||
$imageViewer.set(true);
|
||||
store.dispatch(imageToCompareChanged(null));
|
||||
}, [store]);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
@@ -10,12 +9,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) => {
|
||||
const imageViewer = useImageViewer();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
imageViewer.openImageInViewer(imageDTO);
|
||||
}, [imageDTO, imageViewer]);
|
||||
// TODO
|
||||
imageDTO.image_name;
|
||||
}, [imageDTO]);
|
||||
|
||||
return (
|
||||
<DndImageIcon
|
||||
|
||||
@@ -1,40 +1,12 @@
|
||||
import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common';
|
||||
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
||||
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
||||
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
import { memo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
import { useImageViewer } from './useImageViewer';
|
||||
|
||||
// type Props = {
|
||||
// closeButton?: ReactNode;
|
||||
// };
|
||||
|
||||
// const useFocusRegionOptions = {
|
||||
// focusOnMount: true,
|
||||
// };
|
||||
|
||||
// const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
// display: 'flex',
|
||||
// width: 'full',
|
||||
// height: 'full',
|
||||
// position: 'absolute',
|
||||
// flexDirection: 'column',
|
||||
// inset: 0,
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// overflow: 'hidden',
|
||||
// };
|
||||
|
||||
export const ImageViewer = memo(() => {
|
||||
const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
|
||||
const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken);
|
||||
@@ -48,78 +20,3 @@ export const ImageViewer = memo(() => {
|
||||
});
|
||||
|
||||
ImageViewer.displayName = 'ImageViewer';
|
||||
|
||||
const imageViewerContainerSx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
transition: 'opacity 0.15s ease',
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto',
|
||||
'&[data-hidden="true"]': {
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
backdropFilter: 'blur(10px) brightness(70%)',
|
||||
};
|
||||
|
||||
export const ImageViewerModal = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const imageViewer = useImageViewer();
|
||||
useOutsideClick({
|
||||
ref,
|
||||
handler: imageViewer.close,
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
imageViewer.close,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: imageViewer.isOpen,
|
||||
},
|
||||
[imageViewer.isOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={imageViewerContainerSx} data-hidden={!imageViewer.isOpen}>
|
||||
<Flex
|
||||
ref={ref}
|
||||
flexDir="column"
|
||||
position="absolute"
|
||||
bg="base.900"
|
||||
borderRadius="base"
|
||||
top={16}
|
||||
right={16}
|
||||
bottom={16}
|
||||
left={16}
|
||||
>
|
||||
<ViewerToolbar />
|
||||
<ImageViewer />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
ImageViewerModal.displayName = 'GatedImageViewer';
|
||||
|
||||
const ImageViewerCloseButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageViewer = useImageViewer();
|
||||
useAssertSingleton('ImageViewerCloseButton');
|
||||
useHotkeys('esc', imageViewer.close);
|
||||
return (
|
||||
<IconButton
|
||||
tooltip={t('gallery.closeViewer')}
|
||||
aria-label={t('gallery.closeViewer')}
|
||||
icon={<PiXBold />}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageViewer.close}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common';
|
||||
import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview2';
|
||||
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
||||
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
|
||||
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
import { memo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
import { useImageViewer } from './useImageViewer';
|
||||
|
||||
// type Props = {
|
||||
// closeButton?: ReactNode;
|
||||
// };
|
||||
@@ -48,78 +40,3 @@ export const ImageViewer = memo(() => {
|
||||
});
|
||||
|
||||
ImageViewer.displayName = 'ImageViewer';
|
||||
|
||||
const imageViewerContainerSx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
transition: 'opacity 0.15s ease',
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto',
|
||||
'&[data-hidden="true"]': {
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
backdropFilter: 'blur(10px) brightness(70%)',
|
||||
};
|
||||
|
||||
export const ImageViewerModal = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const imageViewer = useImageViewer();
|
||||
useOutsideClick({
|
||||
ref,
|
||||
handler: imageViewer.close,
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
imageViewer.close,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: imageViewer.isOpen,
|
||||
},
|
||||
[imageViewer.isOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={imageViewerContainerSx} data-hidden={!imageViewer.isOpen}>
|
||||
<Flex
|
||||
ref={ref}
|
||||
flexDir="column"
|
||||
position="absolute"
|
||||
bg="base.900"
|
||||
borderRadius="base"
|
||||
top={16}
|
||||
right={16}
|
||||
bottom={16}
|
||||
left={16}
|
||||
>
|
||||
<ViewerToolbar />
|
||||
<ImageViewer />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
ImageViewerModal.displayName = 'GatedImageViewer';
|
||||
|
||||
const ImageViewerCloseButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageViewer = useImageViewer();
|
||||
useAssertSingleton('ImageViewerCloseButton');
|
||||
useHotkeys('esc', imageViewer.close);
|
||||
return (
|
||||
<IconButton
|
||||
tooltip={t('gallery.closeViewer')}
|
||||
aria-label={t('gallery.closeViewer')}
|
||||
icon={<PiXBold />}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageViewer.close}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
/**
|
||||
* There's a race condition that causes the canvas to not fit to layers on the very first app startup.
|
||||
*
|
||||
* The canvas stage uses a resize observer to fit the stage to the container, and on the first resize event, it also
|
||||
* fits the layers to the stage. Subsequent resize events only fit the stage to the container, they do not fit layers
|
||||
* to the stage.
|
||||
*
|
||||
* On the very first app startup (new user or after they reset all web UI state), the resizable panels library needs
|
||||
* to do one extra resize as it initializes and figures out its target size. At this time, the canvas stage has already
|
||||
* done its one-time fit layers to stage, so the canvas stage does not fit layers to the stage again.
|
||||
*
|
||||
* For the end user, this means that the bbox is not centered in the canvas stage on the very first app startup. On
|
||||
* all subsequent app startups, the bbox is centered in the canvas stage.
|
||||
*
|
||||
* We can hack around this, thanks to the fact that the image viewer is always opened on the first app startup. By the
|
||||
* time the user closes it, the resizable panels library has already done its one extra resize and the DOM layout has
|
||||
* stabilized. So we can track the first time the image viewer is closed and fit the layers to the stage at that time,
|
||||
* ensuring that the bbox is centered in the canvas stage on that first app startup.
|
||||
*
|
||||
* TODO(psyche): Figure out a better way to do handle this...
|
||||
*/
|
||||
let didCloseImageViewer = false;
|
||||
const api = buildUseBoolean(false);
|
||||
const useImageViewerState = api[0];
|
||||
export const $imageViewer = api[1];
|
||||
export const toggleImageViewer = () => $imageViewer.set(!$imageViewer.get());
|
||||
|
||||
export const useImageViewer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManagerSafe();
|
||||
const imageViewerState = useImageViewerState();
|
||||
const close = useCallback(() => {
|
||||
if (!didCloseImageViewer && canvasManager) {
|
||||
didCloseImageViewer = true;
|
||||
canvasManager.stage.fitLayersToStage();
|
||||
}
|
||||
imageViewerState.setFalse();
|
||||
}, [canvasManager, imageViewerState]);
|
||||
const openImageInViewer = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO));
|
||||
imageViewerState.setTrue();
|
||||
},
|
||||
[dispatch, imageViewerState]
|
||||
);
|
||||
|
||||
return {
|
||||
isOpen: imageViewerState.isTrue,
|
||||
open: imageViewerState.setTrue,
|
||||
close,
|
||||
toggle: imageViewerState.toggle,
|
||||
openImageInViewer,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user