diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 375f691ab2..cfed635570 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -142,9 +142,11 @@
"blue": "Blue",
"alpha": "Alpha",
"selected": "Selected",
- "viewer": "Viewer",
"tab": "Tab",
- "close": "Close"
+ "viewing": "Viewing",
+ "viewingDesc": "Review images in a large gallery view",
+ "editing": "Editing",
+ "editingDesc": "Edit on the Control Layers canvas"
},
"controlnet": {
"controlAdapter_one": "Control Adapter",
@@ -365,10 +367,7 @@
"bulkDownloadRequestFailed": "Problem Preparing Download",
"bulkDownloadFailed": "Download Failed",
"problemDeletingImages": "Problem Deleting Images",
- "problemDeletingImagesDesc": "One or more images could not be deleted",
- "switchTo": "Switch to {{ tab }} (Z)",
- "openFloatingViewer": "Open Floating Viewer",
- "closeFloatingViewer": "Close Floating Viewer"
+ "problemDeletingImagesDesc": "One or more images could not be deleted"
},
"hotkeys": {
"searchHotkeys": "Search Hotkeys",
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts
index 6b8c9b4ea3..67c6d076ee 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts
@@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
-import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
+import { selectionChanged } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util';
@@ -62,7 +62,6 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
} else {
dispatch(selectionChanged([imageDTO]));
}
- dispatch(isImageViewerOpenChanged(true));
},
});
};
diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
index 01107c21b4..2712334e1e 100644
--- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
@@ -70,6 +70,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
onMouseOver,
onMouseOut,
dataTestId,
+ ...rest
} = props;
const [isHovered, setIsHovered] = useState(false);
@@ -138,6 +139,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
minH={minSize ? minSize : undefined}
userSelect="none"
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
+ {...rest}
>
{imageDTO && (
{
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
- }
- isChecked={tool === 'move' || isStaging}
- onClick={handleSelectMoveTool}
- />
- : }
- onClick={handleSetShouldShowBoundingBox}
- isDisabled={isStaging}
- />
- }
- onClick={handleClickResetCanvasView}
- />
-
+
+ }
+ isChecked={tool === 'move' || isStaging}
+ onClick={handleSelectMoveTool}
+ />
+ : }
+ onClick={handleSetShouldShowBoundingBox}
+ isDisabled={isStaging}
+ />
+ }
+ onClick={handleClickResetCanvasView}
+ />
+
-
+
+ }
+ onClick={handleMergeVisible}
+ isDisabled={isStaging}
+ />
+ }
+ onClick={handleSaveToGallery}
+ isDisabled={isStaging}
+ />
+ {isClipboardAPIAvailable && (
}
- onClick={handleMergeVisible}
+ aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
+ tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
+ icon={}
+ onClick={handleCopyImageToClipboard}
isDisabled={isStaging}
/>
- }
- onClick={handleSaveToGallery}
- isDisabled={isStaging}
- />
- {isClipboardAPIAvailable && (
- }
- onClick={handleCopyImageToClipboard}
- isDisabled={isStaging}
- />
- )}
- }
- onClick={handleDownloadAsImage}
- isDisabled={isStaging}
- />
-
-
-
-
-
+ )}
+ }
+ onClick={handleDownloadAsImage}
+ isDisabled={isStaging}
+ />
+
+
+
+
+
-
- }
- isDisabled={isStaging}
- {...getUploadButtonProps()}
- />
-
- }
- onClick={handleResetCanvas}
- colorScheme="error"
- isDisabled={isStaging}
- />
-
-
-
-
-
-
-
-
-
-
+
+ }
+ isDisabled={isStaging}
+ {...getUploadButtonProps()}
+ />
+
+ }
+ onClick={handleResetCanvas}
+ colorScheme="error"
+ isDisabled={isStaging}
+ />
+
+
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
index b78910700d..b087d8dc70 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
@@ -4,7 +4,7 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
-import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
+import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import { memo } from 'react';
export const ControlLayersToolbar = memo(() => {
@@ -21,7 +21,7 @@ export const ControlLayersToolbar = memo(() => {
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index 2788b1095d..2c53599ba3 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -11,6 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
+import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -102,6 +103,10 @@ const GalleryImage = (props: HoverableImageProps) => {
setIsHovered(true);
}, []);
+ const onDoubleClick = useCallback(() => {
+ dispatch(isImageViewerOpenChanged(true));
+ }, [dispatch]);
+
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
@@ -143,6 +148,7 @@ const GalleryImage = (props: HoverableImageProps) => {
>
= {
- generation: 'controlLayers.controlLayers',
- canvas: 'ui.tabs.canvas',
- workflows: 'ui.tabs.workflows',
- models: 'ui.tabs.models',
- queue: 'ui.tabs.queue',
-};
-
-export const EditorButton = () => {
- const { t } = useTranslation();
- const { onClose } = useImageViewer();
- const activeTabName = useAppSelector(activeTabNameSelector);
- const tooltip = useMemo(
- () => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY_SHORT[activeTabName]) }),
- [t, activeTabName]
- );
-
- return (
- }
- >
- {t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
-
- );
-};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index dcd4d4c304..7064e553dc 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -10,7 +10,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
-import { EditorButton } from './EditorButton';
+import { ViewerToggleMenu } from './ViewerToggleMenu';
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
@@ -60,7 +60,7 @@ export const ImageViewer = memo(() => {
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx
new file mode 100644
index 0000000000..fe09f11be6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerWorkflows.tsx
@@ -0,0 +1,45 @@
+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/ViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx
deleted file mode 100644
index edceb5099c..0000000000
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Button } from '@invoke-ai/ui-library';
-import { useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiArrowsDownUpBold } from 'react-icons/pi';
-
-import { useImageViewer } from './useImageViewer';
-
-export const ViewerButton = () => {
- const { t } = useTranslation();
- const { onOpen } = useImageViewer();
- const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]);
-
- return (
- }
- >
- {t('common.viewer')}
-
- );
-};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx
new file mode 100644
index 0000000000..c1277d142f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx
@@ -0,0 +1,67 @@
+import {
+ Button,
+ Flex,
+ Icon,
+ Popover,
+ PopoverArrow,
+ PopoverBody,
+ PopoverContent,
+ PopoverTrigger,
+ Text,
+} from '@invoke-ai/ui-library';
+import { useTranslation } from 'react-i18next';
+import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
+
+import { useImageViewer } from './useImageViewer';
+
+export const ViewerToggleMenu = () => {
+ const { t } = useTranslation();
+ const { isOpen, onClose, onOpen } = useImageViewer();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index 744dc09f3f..af19017486 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -1,8 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
-import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
-import { setActiveTab } from 'features/ui/store/uiSlice';
import { uniqBy } from 'lodash-es';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
@@ -23,7 +21,7 @@ const initialGalleryState: GalleryState = {
boardSearchText: '',
limit: INITIAL_IMAGE_LIMIT,
offset: 0,
- isImageViewerOpen: false,
+ isImageViewerOpen: true,
};
export const gallerySlice = createSlice({
@@ -83,12 +81,6 @@ export const gallerySlice = createSlice({
},
},
extraReducers: (builder) => {
- builder.addCase(setActiveTab, (state) => {
- state.isImageViewerOpen = false;
- });
- builder.addCase(rgLayerAdded, (state) => {
- state.isImageViewerOpen = false;
- });
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
const deletedBoardId = action.meta.arg.originalArgs;
if (deletedBoardId === state.selectedBoardId) {
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
index 2a08fb840e..93856a21c4 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
@@ -1,6 +1,5 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
@@ -23,7 +22,6 @@ const TopCenterPanel = () => {
-
);
};
diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
index 42df03872c..1968c64161 100644
--- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
@@ -4,7 +4,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
-import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
@@ -255,9 +254,8 @@ const InvokeTabs = () => {
>
)}
-
+
{tabPanels}
-
{shouldShowGalleryPanel && (
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx
index a7a401cde4..698be530f9 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx
@@ -1,8 +1,9 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
+import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
@@ -15,7 +16,7 @@ import { RefinerSettingsAccordion } from 'features/settingsAccordions/components
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
-import { memo, useMemo } from 'react';
+import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const overlayScrollbarsStyles: CSSProperties = {
@@ -37,6 +38,7 @@ const selectedStyles: ChakraProps['sx'] = {
const ParametersPanelTextToImage = () => {
const { t } = useTranslation();
+ const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length);
const controlLayersTitle = useMemo(() => {
@@ -46,6 +48,14 @@ const ParametersPanelTextToImage = () => {
return `${t('controlLayers.controlLayers')} (${controlLayersCount})`;
}, [controlLayersCount, t]);
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
+ const onChangeTabs = useCallback(
+ (i: number) => {
+ if (i === 1) {
+ dispatch(isImageViewerOpenChanged(false));
+ }
+ },
+ [dispatch]
+ );
return (
@@ -55,7 +65,15 @@ const ParametersPanelTextToImage = () => {
{isSDXL ? : }
-
+
{t('common.settingsLabel')}
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 2ee21bfadf..b4f473ae03 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx
@@ -1,9 +1,20 @@
import { Box } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { ImageViewerWorkflows } from 'features/gallery/components/ImageViewer/ImageViewerWorkflows';
import NodeEditor from 'features/nodes/components/NodeEditor';
import { memo } from 'react';
import { ReactFlowProvider } from 'reactflow';
const NodesTab = () => {
+ const mode = useAppSelector((s) => s.workflow.mode);
+ if (mode === 'view') {
+ return (
+
+
+
+ );
+ }
+
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 74845a9ca9..1c1c9c24a4 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx
@@ -1,11 +1,13 @@
import { Box } from '@invoke-ai/ui-library';
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { memo } from 'react';
const TextToImageTab = () => {
return (
+
);
};