feat(ui): viewer is a modal (wip)

This commit is contained in:
psychedelicious
2025-05-16 12:00:39 +10:00
parent 23b0a4a7f4
commit 53a3dc52bc
28 changed files with 114 additions and 146 deletions

View File

@@ -10,9 +10,11 @@ import type { PartialAppConfig } from 'app/types/invokeai';
import { useFocusRegionWatcher } from 'common/hooks/focus';
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';
@@ -61,6 +63,12 @@ export const GlobalHookIsolator = memo(
useWorkflowBuilderWatcher();
useDynamicPromptsWatcher();
useRegisteredHotkeys({
id: 'toggleViewer',
category: 'viewer',
callback: toggleImageViewer,
});
return null;
}
);

View File

@@ -11,6 +11,7 @@ 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';
@@ -58,6 +59,7 @@ export const GlobalModalIsolator = memo(() => {
<CanvasPasteModal />
</CanvasManagerProviderGate>
<LoadWorkflowFromGraphModal />
<ImageViewerModal />
</>
);
});

View File

@@ -2,7 +2,6 @@ import { Grid, GridItem } from '@invoke-ai/ui-library';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { newCanvasEntityFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -19,13 +18,8 @@ const addGlobalReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDn
export const CanvasDropArea = memo(() => {
const { t } = useTranslation();
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
if (imageViewer.isOpen) {
return null;
}
return (
<>
<Grid

View File

@@ -26,7 +26,6 @@ import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasT
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { GatedImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -131,7 +130,6 @@ export const CanvasMainPanelContent = memo(() => {
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
<GatedImageViewer />
</Flex>
</FocusRegionWrapper>
);

View File

@@ -11,7 +11,6 @@ import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -32,11 +31,8 @@ export const CanvasRightPanel = memo(() => {
}, [activeTab]);
const onClickViewerToggleButton = useCallback(() => {
if (activeTab !== 'gallery') {
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}
imageViewer.toggle();
}, [imageViewer, activeTab, dispatch]);
imageViewer.open();
}, [imageViewer]);
const onChangeTab = useCallback(
(index: number) => {
@@ -49,20 +45,13 @@ export const CanvasRightPanel = memo(() => {
[dispatch]
);
useRegisteredHotkeys({
id: 'toggleViewer',
category: 'viewer',
callback: imageViewer.toggle,
dependencies: [imageViewer],
});
return (
<Tabs index={tabIndex} onChange={onChangeTab} w="full" h="full" display="flex" flexDir="column">
<TabList alignItems="center">
<PanelTabs />
<Spacer />
<Button size="sm" variant="ghost" onClick={onClickViewerToggleButton}>
{imageViewer.isOpen ? t('gallery.closeViewer') : t('gallery.openViewer')}
{t('gallery.openViewer')}
</Button>
</TabList>
<TabPanels w="full" h="full">

View File

@@ -3,7 +3,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -17,15 +16,13 @@ const [useNewCanvasSessionDialog] = buildUseBoolean(false);
export const useNewGallerySession = () => {
const dispatch = useAppDispatch();
const imageViewer = useImageViewer();
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
dispatch(newGallerySessionRequested());
imageViewer.open();
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch, imageViewer]);
}, [dispatch]);
const newGallerySessionWithDialog = useCallback(() => {
if (shouldConfirmOnNewSession) {
@@ -40,15 +37,13 @@ export const useNewGallerySession = () => {
export const useNewCanvasSession = () => {
const dispatch = useAppDispatch();
const imageViewer = useImageViewer();
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
dispatch(newCanvasSessionRequested());
imageViewer.close();
dispatch(activeTabCanvasRightPanelChanged('layers'));
}, [dispatch, imageViewer]);
}, [dispatch]);
const newCanvasSessionWithDialog = useCallback(() => {
if (shouldConfirmOnNewSession) {

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolBboxButton = memo(() => {
const { t } = useTranslation();
const selectBbox = useSelectTool('bbox');
const isSelected = useToolIsSelected('bbox');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectBboxTool',
category: 'canvas',
callback: selectBbox,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [selectBbox, isSelected, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [selectBbox, isSelected],
});
return (

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolBrushButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('brush');
const selectBrush = useSelectTool('brush');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectBrushTool',
category: 'canvas',
callback: selectBrush,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [isSelected, selectBrush, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [isSelected, selectBrush],
});
return (

View File

@@ -16,7 +16,6 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { selectCanvasSettingsSlice, settingsBrushWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
@@ -69,7 +68,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(50);
export const ToolBrushWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const imageViewer = useImageViewer();
const isSelected = useToolIsSelected('brush');
const width = useAppSelector(selectBrushWidth);
const [localValue, setLocalValue] = useState(width);
@@ -133,15 +131,15 @@ export const ToolBrushWidth = memo(() => {
id: 'decrementToolWidth',
category: 'canvas',
callback: decrement,
options: { enabled: isSelected && !imageViewer.isOpen },
dependencies: [decrement, isSelected, imageViewer.isOpen],
options: { enabled: isSelected },
dependencies: [decrement, isSelected],
});
useRegisteredHotkeys({
id: 'incrementToolWidth',
category: 'canvas',
callback: increment,
options: { enabled: isSelected && !imageViewer.isOpen },
dependencies: [increment, isSelected, imageViewer.isOpen],
options: { enabled: isSelected },
dependencies: [increment, isSelected],
});
return (

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolColorPickerButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('colorPicker');
const selectColorPicker = useSelectTool('colorPicker');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectColorPickerTool',
category: 'canvas',
callback: selectColorPicker,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [selectColorPicker, isSelected, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [selectColorPicker, isSelected],
});
return (

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolEraserButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('eraser');
const selectEraser = useSelectTool('eraser');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectEraserTool',
category: 'canvas',
callback: selectEraser,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [isSelected, selectEraser, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [isSelected, selectEraser],
});
return (

View File

@@ -19,7 +19,6 @@ import {
selectCanvasSettingsSlice,
settingsEraserWidthChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
@@ -72,7 +71,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(50);
export const ToolEraserWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const imageViewer = useImageViewer();
const isSelected = useToolIsSelected('eraser');
const width = useAppSelector(selectEraserWidth);
const [localValue, setLocalValue] = useState(width);
@@ -136,15 +134,15 @@ export const ToolEraserWidth = memo(() => {
id: 'decrementToolWidth',
category: 'canvas',
callback: decrement,
options: { enabled: isSelected && !imageViewer.isOpen },
dependencies: [decrement, isSelected, imageViewer.isOpen],
options: { enabled: isSelected },
dependencies: [decrement, isSelected],
});
useRegisteredHotkeys({
id: 'incrementToolWidth',
category: 'canvas',
callback: increment,
options: { enabled: isSelected && !imageViewer.isOpen },
dependencies: [increment, isSelected, imageViewer.isOpen],
options: { enabled: isSelected },
dependencies: [increment, isSelected],
});
return (

View File

@@ -14,7 +14,6 @@ import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { selectCanvasSettingsSlice, settingsColorChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -31,14 +30,13 @@ export const ToolColorPicker = memo(() => {
},
[dispatch]
);
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'setFillToWhite',
category: 'canvas',
callback: () => dispatch(settingsColorChanged({ r: 255, g: 255, b: 255, a: 1 })),
options: { preventDefault: true, enabled: !imageViewer.isOpen },
dependencies: [dispatch, imageViewer.isOpen],
options: { preventDefault: true },
dependencies: [dispatch],
});
return (

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolMoveButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('move');
const selectMove = useSelectTool('move');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectMoveTool',
category: 'canvas',
callback: selectMove,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [isSelected, selectMove, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [isSelected, selectMove],
});
return (

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolRectButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('rect');
const selectRect = useSelectTool('rect');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectRectTool',
category: 'canvas',
callback: selectRect,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [isSelected, selectRect, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [isSelected, selectRect],
});
return (

View File

@@ -1,6 +1,5 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,14 +9,13 @@ export const ToolViewButton = memo(() => {
const { t } = useTranslation();
const isSelected = useToolIsSelected('view');
const selectView = useSelectTool('view');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'selectViewTool',
category: 'canvas',
callback: selectView,
options: { enabled: !isSelected && !imageViewer.isOpen },
dependencies: [selectView, isSelected, imageViewer.isOpen],
options: { enabled: !isSelected },
dependencies: [selectView, isSelected],
});
return (

View File

@@ -1,7 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,49 +10,48 @@ export const CanvasToolbarResetViewButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isCanvasFocused = useIsRegionFocused('canvas');
const imageViewer = useImageViewer();
useRegisteredHotkeys({
id: 'fitLayersToCanvas',
category: 'canvas',
callback: canvasManager.stage.fitLayersToStage,
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasFocused, imageViewer.isOpen],
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'fitBboxToCanvas',
category: 'canvas',
callback: canvasManager.stage.fitBboxToStage,
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasFocused, imageViewer.isOpen],
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo100Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(1),
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasFocused, imageViewer.isOpen],
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo200Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(2),
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasFocused, imageViewer.isOpen],
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo400Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(4),
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasFocused, imageViewer.isOpen],
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
useRegisteredHotkeys({
id: 'setZoomTo800Percent',
category: 'canvas',
callback: () => canvasManager.stage.setScale(8),
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasFocused, imageViewer.isOpen],
options: { enabled: isCanvasFocused, preventDefault: true },
dependencies: [isCanvasFocused],
});
return (

View File

@@ -3,7 +3,6 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { selectActiveTab, selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
@@ -16,8 +15,6 @@ export function useCanvasDeleteLayerHotkey() {
const canvasRightPanelTab = useAppSelector(selectActiveTabCanvasRightPanel);
const appTab = useAppSelector(selectActiveTab);
const imageViewer = useImageViewer();
const deleteSelectedLayer = useCallback(() => {
if (selectedEntityIdentifier === null) {
return;
@@ -34,7 +31,7 @@ export function useCanvasDeleteLayerHotkey() {
id: 'deleteSelected',
category: 'canvas',
callback: deleteSelectedLayer,
options: { enabled: isDeleteEnabled && !imageViewer.isOpen },
dependencies: [isDeleteEnabled, deleteSelectedLayer, imageViewer.isOpen],
options: { enabled: isDeleteEnabled },
dependencies: [isDeleteEnabled, deleteSelectedLayer],
});
}

View File

@@ -5,7 +5,6 @@ import {
selectSelectedEntityIdentifier,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useCallback, useEffect, useState } from 'react';
@@ -15,7 +14,6 @@ export const useCanvasEntityQuickSwitchHotkey = () => {
const [current, setCurrent] = useState<CanvasEntityIdentifier | null>(null);
const selected = useAppSelector(selectSelectedEntityIdentifier);
const bookmarked = useAppSelector(selectBookmarkedEntityIdentifier);
const imageViewer = useImageViewer();
// Update prev and current when selected entity changes
useEffect(() => {
@@ -49,7 +47,6 @@ export const useCanvasEntityQuickSwitchHotkey = () => {
id: 'quickSwitch',
category: 'canvas',
callback: onQuickSwitch,
options: { enabled: !imageViewer.isOpen },
dependencies: [onQuickSwitch, imageViewer.isOpen],
dependencies: [onQuickSwitch],
});
};

View File

@@ -6,7 +6,6 @@ import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocke
import { entityReset } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isMaskEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useCallback, useMemo } from 'react';
@@ -17,7 +16,6 @@ export function useCanvasResetLayerHotkey() {
const isBusy = useCanvasIsBusy();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isLocked = useEntityIsLocked(entityIdentifier);
const imageViewer = useImageViewer();
const resetSelectedLayer = useCallback(() => {
if (entityIdentifier === null || adapter === null) {
@@ -36,7 +34,7 @@ export function useCanvasResetLayerHotkey() {
id: 'resetSelected',
category: 'canvas',
callback: resetSelectedLayer,
options: { enabled: isResetAllowed && !isBusy && !isLocked && !imageViewer.isOpen },
dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer, imageViewer.isOpen],
options: { enabled: isResetAllowed && !isBusy && !isLocked },
dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer],
});
}

View File

@@ -3,7 +3,6 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
@@ -12,7 +11,6 @@ export const useCanvasUndoRedoHotkeys = () => {
useAssertSingleton('useCanvasUndoRedo');
const dispatch = useDispatch();
const isBusy = useCanvasIsBusy();
const imageViewer = useImageViewer();
const mayUndo = useAppSelector(selectCanvasMayUndo);
const handleUndo = useCallback(() => {
@@ -22,8 +20,8 @@ export const useCanvasUndoRedoHotkeys = () => {
id: 'undo',
category: 'canvas',
callback: handleUndo,
options: { enabled: mayUndo && !isBusy && !imageViewer.isOpen, preventDefault: true },
dependencies: [mayUndo, isBusy, handleUndo, imageViewer.isOpen],
options: { enabled: mayUndo && !isBusy, preventDefault: true },
dependencies: [mayUndo, isBusy, handleUndo],
});
const mayRedo = useAppSelector(selectCanvasMayRedo);
@@ -34,7 +32,7 @@ export const useCanvasUndoRedoHotkeys = () => {
id: 'redo',
category: 'canvas',
callback: handleRedo,
options: { enabled: mayRedo && !isBusy && !imageViewer.isOpen, preventDefault: true },
dependencies: [mayRedo, handleRedo, isBusy, imageViewer.isOpen],
options: { enabled: mayRedo && !isBusy, preventDefault: true },
dependencies: [mayRedo, handleRedo, isBusy],
});
};

View File

@@ -5,13 +5,11 @@ import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty'
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
@@ -52,9 +50,8 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
if (!adapter) {
return;
}
imageViewer.close();
adapter.filterer.start();
}, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
}, [isDisabled, entityIdentifier, canvasManager]);
return { isDisabled, start } as const;
};

View File

@@ -5,13 +5,11 @@ import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty'
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
@@ -52,9 +50,8 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
if (!adapter) {
return;
}
imageViewer.close();
adapter.segmentAnything.start();
}, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
}, [isDisabled, entityIdentifier, canvasManager]);
return { isDisabled, start } as const;
};

View File

@@ -5,13 +5,11 @@ import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty'
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
@@ -52,9 +50,8 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (!adapter) {
return;
}
imageViewer.close();
await adapter.transformer.startTransform();
}, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
}, [isDisabled, entityIdentifier, canvasManager]);
const fitToBbox = useCallback(async () => {
if (isDisabled) {
@@ -70,11 +67,10 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (!adapter) {
return;
}
imageViewer.close();
await adapter.transformer.startTransform({ silent: true });
adapter.transformer.fitToBboxContain();
await adapter.transformer.applyTransform();
}, [canvasManager, entityIdentifier, imageViewer, isDisabled]);
}, [canvasManager, entityIdentifier, isDisabled]);
return { isDisabled, start, fitToBbox } as const;
};

View File

@@ -8,6 +8,7 @@ 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 { memo, useEffect, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -47,6 +48,9 @@ export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: DndImage.Props
getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
onDragStart: () => {
setIsDragging(true);
if ($imageViewer.get()) {
$imageViewer.set(false);
}
},
onDrop: () => {
setIsDragging(false);

View File

@@ -1,4 +1,4 @@
import { Box, IconButton, type SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
@@ -9,7 +9,7 @@ import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewe
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
import type { ReactNode } from 'react';
import { memo } from 'react';
import { memo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -43,7 +43,7 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
return (
<FocusRegionWrapper region="viewer" sx={FOCUS_REGION_STYLES} layerStyle="first" {...useFocusRegionOptions}>
<FocusRegionWrapper region="viewer" sx={FOCUS_REGION_STYLES} {...useFocusRegionOptions}>
{hasImageToCompare && <CompareToolbar />}
{!hasImageToCompare && <ViewerToolbar closeButton={closeButton} />}
<Box ref={containerRef} w="full" h="full" p={2} overflow="hidden">
@@ -57,17 +57,50 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
ImageViewer.displayName = 'ImageViewer';
export const GatedImageViewer = memo(() => {
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%)',
};
const imageViewerModalSx: SystemStyleObject = {
position: 'absolute',
bg: 'base.800',
borderRadius: 'base',
top: 16,
right: 16,
bottom: 16,
left: 16,
};
export const ImageViewerModal = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const imageViewer = useImageViewer();
useOutsideClick({
ref,
handler: imageViewer.close,
});
if (!imageViewer.isOpen) {
return null;
}
return <ImageViewer closeButton={<ImageViewerCloseButton />} />;
return (
<Box sx={imageViewerContainerSx} data-hidden={!imageViewer.isOpen}>
<Box ref={ref} sx={imageViewerModalSx}>
<ImageViewer closeButton={<ImageViewerCloseButton />} />
</Box>
</Box>
);
});
GatedImageViewer.displayName = 'GatedImageViewer';
ImageViewerModal.displayName = 'GatedImageViewer';
const ImageViewerCloseButton = memo(() => {
const { t } = useTranslation();
@@ -87,15 +120,3 @@ const ImageViewerCloseButton = memo(() => {
});
ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';
const GatedImageViewerCloseButton = memo(() => {
const imageViewer = useImageViewer();
if (!imageViewer.isOpen) {
return null;
}
return <ImageViewerCloseButton />;
});
GatedImageViewerCloseButton.displayName = 'GatedImageViewerCloseButton';

View File

@@ -27,9 +27,10 @@ import type { ImageDTO } from 'services/api/types';
* TODO(psyche): Figure out a better way to do handle this...
*/
let didCloseImageViewer = false;
const api = buildUseBoolean(true);
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();

View File

@@ -2,7 +2,6 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
@@ -30,12 +29,11 @@ type Props = {
const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
const { t } = useTranslation();
const tab = useAppSelector(selectActiveTab);
const imageViewer = useImageViewer();
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
return (
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
{tab === 'canvas' && !imageViewer.isOpen && (
{tab === 'canvas' && (
<CanvasManagerProviderGate>
<ToolChooser />
</CanvasManagerProviderGate>