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 ( - - ); -}; 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 ( - - ); -}; 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 ( + ); };